我想用 aiosmtpd
在python中编写我自己的小型邮件服务器应用程序 a)出于教育目的,更好地了解邮件服务器所以我的问题是, Mail-Transfer-Agent 缺少什么(除了aiosmtpd),可以发送和接收来自其他完整MTA的电子邮件(gmail.com,yahoo.com) ...)?
我在猜测:
1。)当然是 域 和静态IP
2.)此域的有效 证书
......应该可以使用Lets Encrypt
3.) 加密
......应该可以使用SSL / Context / Starttls ...使用aiosmtpd本身
4.) 解析 外发电子邮件的MX DNS条目!
...应该可以使用python库dnspython
5.) 错误 处理SMTP通信错误,来自其他MTA的错误回复,弹跳!?
6.) 队列 ,用于处理入站和待处理的外发电子邮件!?
是否还缺少其他 “必要” 功能?
当然,我知道,垃圾邮件检查,恶意软件检查,证书验证,黑名单,规则,邮箱等邮件服务器还有更多“高级”功能......
感谢所有提示!
编辑:
让我澄清一下我的想法:
我想为俱乐部写一个邮件服务器。它的主要目的是邮件列表服务器。俱乐部的不同组将有不同的名单。
假设我的域名是 myclub.org ,那么会有例如 youth@myclub.org , trainer@myclub.org 等等。
只允许成员使用此邮件服务器,只有成员才会收到来自此邮件服务器的电子邮件。不允许其他人向此邮件服务器发送电子邮件,也不会收到其中的电子邮件。成员电子邮件地址及其组存储在数据库中。
将来我想整合其他一些有用的功能,例如:
我不需要的东西:
开放接力问题:
丢失电子邮件问题:
我想我需要一个“轻量级”重试机制。但是,如果在重试后无法发送外发电子邮件,则会将其删除,只会通知管理员,而不会通知发件人。成员不应该受到电子邮件递送问题的困扰。是否有 Python库可以生成符合RFC3464的错误回复电子邮件?
重新启动问题:
我不确定我是否真的需要持久存储电子邮件,尚未发送?在我的使用案例中,所有外发电子邮件通常应在几秒钟内发送(如果没有发生传送问题)。在(计划的)重启之前,我可以检查空的发送队列。
答案 0 :(得分:2)
aiosmtpd是编写电子邮件自定义路由和标头重写规则的绝佳工具。但是,aiosmtpd不是MTA,因为它不执行消息队列或DSN生成。 MTA的一个流行选择是postfix,并且由于postfix可以配置为将域的所有电子邮件中继到另一个本地SMTP服务器(例如aiosmtpd),因此自然选择使用postfix作为面向Internet的前端和aiosmtpd作为业务-logic后端。
使用postfix作为中间人的优势,而不是让aiosmtpd面对公共互联网:
以下是配置postfix以配合本地SMTP服务器的方法。 aiosmtpd。
我们将在端口25上运行postfix,在端口20381上运行aiosmtpd。
要指定postfix将example.com
的电子邮件转发到在端口20381上运行的SMTP服务器,请将以下内容添加到/etc/postfix/main.cf
:
transport_maps = hash:/etc/postfix/smtp_transport
relay_domains = example.com
使用内容创建/etc/postfix/smtp_transport
:
# Table of special transport method for domains in
# virtual_mailbox_domains. See postmap(5), virtual(5) and
# transport(5).
#
# Remember to run
# postmap /etc/postfix/smtp_transport
# and update relay_domains in main.cf after changing this file!
example.com smtp:127.0.0.1:20381
创建该文件后运行postmap /etc/postfix/smtp_transport
(每次修改时)。
在aiosmtpd方面,有几点需要考虑。
最重要的是如何处理退回电子邮件。简短的说法是,您应该将信封发件人设置为您控制的专门用于接收退回的电子邮件地址,例如: bounce@example.com
。当电子邮件到达此地址时,应将其存储在某处,以便您可以处理退回,例如删除数据库中的成员电子邮件地址。
另一个需要考虑的重要事项是如何告诉您的会员的电子邮件提供商您正在进行邮件列表转发。您可能希望在将电子邮件转发到GROUP@example.com
时添加以下标头:
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
在这里,我使用postmaster@example.com
作为列表取消订阅请求的收件人。这应该是转发给电子邮件管理员(即您)的地址。
以下是执行上述操作的骨架(未经测试)。它将退回电子邮件存储在名为bounces
的目录中,并根据组列表(在MEMBERS
中)转发带有有效From:-header(显示在GROUPS
中的一个)的电子邮件。 / p>
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()
答案 1 :(得分:0)
您可以考虑以下功能:
答案 2 :(得分:0)
运行自己的SMTP服务器最重要的一点是不能是开放中继。这意味着您不得接受来自陌生人的邮件并将其转发到互联网上的任何目的地,因为这会使垃圾邮件发送者通过您的SMTP服务器发送垃圾邮件 - 这很快就会阻止您。
因此,您的服务器应该
由于您的问题涉及解决外发电子邮件的MX记录问题,因此我假设您希望服务器接受来自经过身份验证的用户的电子邮件。因此,您需要考虑用户如何向服务器验证自己。 aiosmtpd目前有open pull request提供基本的SMTP AUTH实现;您可以使用它,或者您可以实现自己的(通过继承aiosmtpd.smtp.SMTP
并实现smtp_AUTH()
方法)。
运行自己的SMTP服务器的第二个最重要的事情是,不得在不通知发件人的情况下丢失电子邮件。当您接受来自经过身份验证的用户的电子邮件转发到外部目的地时,您应该让用户知道(通过电子邮件发送RFC 3464 Delivery Status Notification)如果邮件被延迟或者根本没有送达。< / p>
如果远程目标无法接收电子邮件,则不应立即丢弃该电子邮件;你应该稍后再试,并反复尝试,直到你认为你已经尝试了足够长的时间。例如,Postfix在第一次传递尝试失败后尝试传递电子邮件之前等待10分钟,然后如果第二次尝试失败则等待20分钟,依此类推,直到邮件尝试传递几天为止。
您还应该注意允许重新启动运行邮件服务器的主机,这意味着您应该将排队的邮件存储在磁盘上。为此,您可以使用mailbox module。
当然,我还没有涵盖每一个细节,但我认为上述两点是最重要的,你似乎没有在你的问题中涵盖它们。