<p>aiosmtpd是为电子邮件编写自定义路由和头重写规则的优秀工具。但是,aiosmtpd不是MTA,因为它不执行消息队列或DSN生成。MTA的一个流行选择是postfix,由于postfix可以配置为将一个域的所有电子邮件转发到另一个本地SMTP服务器(如aiosmtpd),一个自然的选择是使用postfix作为面向互联网的前端,aiosmtpd作为业务逻辑后端。在</p>
<p>使用postfix作为中间人而不是让aiosmtpd面对公共互联网的优势:</p>
<ul>
<li>不需要在aiosmtpd中处理dnsmx查找,只需通过后缀进行中继(本地主机:25)在</li>
<li>不用担心aiosmtpd中不兼容的SMTP客户端</li>
<li>不用担心aiosmtpd中的STARTTLS,而是在postfix中配置它(更简单,更坚固)</li>
<li>不用担心重新尝试失败的电子邮件传递和发送传递状态通知</li>
<li>aiosmtpd可以配置为在编程错误时以“瞬时故障”(smtp4xx代码)响应,因此只要在4天内修复编程错误,就不会丢失电子邮件</li>
</ul>
<hr/>
<p>下面是如何将postfix配置为使用aiosmtpd等支持的本地SMTP服务器。在</p>
<p>我们将在端口25上运行postfix,在端口20381上运行aiosmtpd。在</p>
<p>要指定postfix应将<code>example.com</code>的电子邮件中继到运行在端口20381上的SMTP服务器,请将以下内容添加到<code>/etc/postfix/main.cf</code>:</p>
<pre><code>transport_maps = hash:/etc/postfix/smtp_transport
relay_domains = example.com
</code></pre>
<p>并使用以下内容创建<code>/etc/postfix/smtp_transport</code>:</p>
^{pr2}$
<p>创建该文件后(以及每次修改它时)运行<code>postmap /etc/postfix/smtp_transport</code>。在</p>
<hr/>
在AIOMTPD方面,有一些事情需要考虑。在</p>
<p>最重要的是你如何处理回邮邮件。简而言之,您应该将信封发件人设置为您控制的一个电子邮件地址,该地址专用于接收回执,例如<code>bounce@example.com</code>。当电子邮件到达此地址时,应将其存储在某个地方,以便您可以处理退回,例如从数据库中删除成员电子邮件地址。在</p>
<p>另一个需要考虑的重要事项是如何告诉您的成员的电子邮件提供商您正在进行邮件列表转发。在将电子邮件转发到<code>GROUP@example.com</code>时,可能需要添加以下标题:</p>
<pre><code>Sender: bounce@example.com
List-Name: GROUP
List-Id: GROUP.example.com
List-Unsubscribe: <mailto:postmaster@example.com?subject=unsubscribe%20GROUP>
List-Help: <mailto:postmaster@example.com?subject=list-help>
List-Subscribe: <mailto:postmaster@example.com?subject=subscribe%20GROUP>
Precedence: bulk
X-Auto-Response-Suppress: OOF
</code></pre>
<p>在这里,我使用<code>postmaster@example.com</code>作为列表取消订阅请求的收件人。这应该是转发给电子邮件管理员(即您)的地址。在</p>
<p>下面是一个骨架(未经测试)可以完成上述操作。它将bounce电子邮件存储在一个名为<code>bounces</code>的目录中,并通过发件人:-页眉(出现在<code>MEMBERS</code>中的一个)根据组列表(在<code>GROUPS</code>中)。在</p>
<pre><code>import os
import email
import email.utils
import mailbox
import smtplib
import aiosmtpd.controller
LISTEN_HOST = '127.0.0.1'
LISTEN_PORT = 20381
DOMAIN = 'example.com'
BOUNCE_ADDRESS = 'bounce'
POSTMASTER = 'postmaster'
BOUNCE_DIRECTORY = os.path.join(
os.path.dirname(__file__), 'bounces')
def get_extra_headers(list_name, is_group=True, skip=()):
list_id = '%s.%s' % (list_name, DOMAIN)
bounce = '%s@%s' % (BOUNCE_ADDRESS, DOMAIN)
postmaster = '%s@%s' % (POSTMASTER, DOMAIN)
unsub = '<mailto:%s?subject=unsubscribe%%20%s>' % (postmaster, list_name)
help = '<mailto:%s?subject=list-help>' % (postmaster,)
sub = '<mailto:%s?subject=subscribe%%20%s>' % (postmaster, list_name)
headers = [
('Sender', bounce),
('List-Name', list_name),
('List-Id', list_id),
('List-Unsubscribe', unsub),
('List-Help', help),
('List-Subscribe', sub),
]
if is_group:
headers.extend([
('Precedence', 'bulk'),
('X-Auto-Response-Suppress', 'OOF'),
])
headers = [(k, v) for k, v in headers if k.lower() not in skip]
return headers
def store_bounce_message(message):
mbox = mailbox.Maildir(BOUNCE_DIRECTORY)
mbox.add(message)
MEMBERS = ['foo@example.net', 'bar@example.org',
'clubadmin@example.org']
GROUPS = {
'group1': ['foo@example.net', 'bar@example.org'],
POSTMASTER: ['clubadmin@example.org'],
}
class ClubHandler:
def validate_sender(self, message):
from_ = message.get('From')
if not from_:
return False
realname, address = email.utils.parseaddr(from_)
if address not in MEMBERS:
return False
return True
def translate_recipient(self, local_part):
try:
return GROUPS[local_part]
except KeyError:
return None
async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
local, domain = address.split('@')
if domain.lower() != DOMAIN:
return '550 wrong domain'
if local.lower() == BOUNCE:
envelope.is_bounce = True
return '250 OK'
translated = self.translate_recipient(local.lower())
if translated is None:
return '550 no such user'
envelope.rcpt_tos.extend(translated)
return '250 OK'
async def handle_DATA(self, server, session, envelope):
if getattr(envelope, 'is_bounce', False):
if len(envelope.rcpt_tos) > 0:
return '500 Cannot send bounce message to multiple recipients'
store_bounce_message(envelope.original_content)
return '250 OK'
message = email.message_from_bytes(envelope.original_content)
if not self.validate_sender(message):
return '500 I do not know you'
for header_key, header_value in get_extra_headers('club'):
message[header_key] = header_value
bounce = '%s@%s' % (BOUNCE_ADDRESS, DOMAIN)
with smtplib.SMTP('localhost', 25) as smtp:
smtp.sendmail(bounce, envelope.rcpt_tos, message.as_bytes())
return '250 OK'
if __name__ == '__main__':
controller = aiosmtpd.controller.Controller(ClubHandler, hostname=LISTEN_HOST, port=LISTEN_PORT)
controller.start()
print("Controller started")
try:
while True:
input()
except (EOFError, KeyboardInterrupt):
controller.stop()
</code></pre>