【问题标题】:Python aiosmtpd - what is missing for an Mail-Transfer-Agent (MTA)?Python aiosmtpd - 邮件传输代理 (MTA) 缺少什么?
【发布时间】:2017-08-17 12:19:44
【问题描述】:

我想用 aiosmtpd 在 python 中编写我自己的小型邮件服务器应用程序

a) 用于教育目的,以更好地了解邮件服务器
b) 实现自己的功能

所以我的问题是,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.orgtrainer@myclub.org 等等。
只有会员可以使用这个邮件服务器,并且只有会员才能收到来自这个邮件服务器的邮件。不允许其他人向此邮件服务器发送电子邮件,也不会收到来自它的电子邮件。成员电子邮件地址及其组存储在数据库中。

未来我想集成一些其他有用的功能,例如:

  • 自动提醒
  • 聊天机器人,成员可以控制服务并通过电子邮件请求信息

我不需要的:

  • 用户邮箱
  • POP/IMAP 访问
  • 网页界面

公开中继问题

  • 我想在 SMTP 协商期间拒绝任何不在成员数据库中的 [FROM] 电子邮件地址。
  • 我想检查发送邮件服务器的有效证书。
  • 电子邮件/会员/天的数量将受到限制。
  • 我不确定,我是否真的需要对收到的电子邮件进行垃圾邮件检测?

丢失电子邮件问题

我认为我需要一个“轻量级”重试机制。但是,如果在重试后无法发送外发电子邮件,它将被丢弃,并且只会通知管理员,而不是发件人。成员不应被电子邮件传递问题所困扰。是否有任何 Python 库 可以生成符合 RFC3464 的错误回复电子邮件?

重启问题

我不确定我是否真的需要永久存储尚未发送的电子邮件?在我的用例中,所有外发电子邮件通常应在几秒钟内送达(如果没有出现送达问题)。在(计划的)重启之前,我可以检查一个空的发送队列。

【问题讨论】:

  • 请澄清您是否只是为了教育目的而这样做,或者它是否真的会被您俱乐部的其他人使用。对于“真正的”邮件服务器,我建议在 postfix 后面运行 aiosmtpd。您甚至可能想在 docker 中运行 postfix -- 请参阅 this repo 以获取示例配置,其中三个不同的 aiosmtpd 程序在 localhost 端口 9000、9001 和 9002 上运行。
  • 它应该成为一个真正的邮件服务器。
  • 我已经考虑过在 docker 容器中运行我的邮件服务器。我知道有 Postfix、Mailman 等,但我想避免经历 Postfix 的配置地狱……大多数我不需要的功能……
  • 我添加了第二个答案,该答案针对生产级 aiosmtpd 的使用。我建议使用 postfix,即使正确配置它可能需要一段时间。一旦你学会了如何使用后缀,那就没那么糟糕了!

标签: python mail-server aiosmtpd


【解决方案1】:

aiosmtpd 是为电子邮件编写自定义路由和标头重写规则的出色工具。但是,aiosmtpd 不是 MTA,因为它不进行消息队列或 DSN 生成。一个流行的 MTA 选择是 postfix,由于 postfix 可以配置为将域的所有电子邮件中继到另一个本地 SMTP 服务器(例如 aiosmtpd),自然的选择是使用 postfix 作为面向互联网的前端和 aiosmtpd 作为业务-逻辑后端。

使用 postfix 作为中间人而不是让 aiosmtpd 面对公共互联网的好处:

  • 无需在 aiosmtpd 中处理 DNS MX 查找 -- 只需通过后缀 (localhost:25) 进行中继
  • 不用担心 aiosmtpd 中不兼容的 SMTP 客户端
  • 不用担心 aiosmtpd 中的 STARTTLS —— 改为在 postfix 中配置它(更简单,更有战斗力)
  • 无需担心重试失败的电子邮件投递和发送投递状态通知
  • aiosmtpd 可以配置为在编程错误时响应“暂时失败”(SMTP 4xx 代码),因此只要在 4 天内修复编程错误,就不会丢失电子邮件

以下是您可以如何配置 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 的目录中,并根据组列表(在GROUPS 中)转发具有有效From:-header(出现在MEMBERS 中)的电子邮件。

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()

【讨论】:

    【解决方案2】:

    运行您自己的 SMTP 服务器最重要的一点是您不能成为开放中继。这意味着您不得接受来自陌生人的邮件并将其转发到 Internet 上的任何目的地,因为这会使垃圾邮件发送者通过您的 SMTP 服务器发送垃圾邮件——这会很快让您被阻止。

    因此,您的服务器应该

    • 经过身份验证的用户/发件人中继到远程目的地,或
    • 从陌生人中继到您自己的域

    由于您的问题涉及解决外发电子邮件的 MX 记录,我假设您希望您的服务器接受来自经过身份验证的用户的电子邮件。因此,您需要考虑您的用户将如何向服务器验证自己的身份。 aiosmtpd 目前有一个open pull request 提供基本的 SMTP AUTH 实现;您可以使用它,也可以实现自己的(通过子类化aiosmtpd.smtp.SMTP 并实现smtp_AUTH() 方法)。


    关于运行您自己的 SMTP 服务器的第二个最重要的事情是您不能在未通知发件人的情况下丢失电子邮件。当您接受来自经过身份验证的用户的电子邮件以转发到外部目的地时,您应该让用户知道(通过电子邮件发送RFC 3464 Delivery Status Notification)消息是否延迟或根本没有送达。

    如果远程目的地未能收到电子邮件,您不应立即丢弃该电子邮件;你应该稍后再试,反复尝试,直到你认为你已经尝试了足够长的时间。例如,Postfix 在第一次传递尝试失败后等待 10 分钟再尝试传递电子邮件,然后如果第二次尝试失败则等待 20 分钟,依此类推,直到邮件尝试传递几天。

    您还应该注意允许重新启动运行邮件服务器的主机,这意味着您应该将排队的消息存储在磁盘上。为此,您也许可以使用mailbox module


    当然,我没有涵盖每一个小细节,但我认为以上两点是最重要的,而您的问题似乎没有涵盖它们。

    【讨论】:

    • 非常感谢您的详细解答!我将编辑我的帖子并添加一些细节。
    【解决方案3】:

    您可以考虑以下功能:

    • 消息线程
    • 支持交付状态
    • 支持 POP 和 IMAP 协议
    • 支持诸如 RFC 2821 SMTP 和 RFC 2033 LMTP 电子邮件消息传输等协议
    • 支持多条消息标记
    • 支持 PGP/MIME (RFC2015)
    • 支持列表-回复
    • 让每个用户管理自己的邮件列表支持
    • 在撰写过程中控制消息头
    • 支持地址组
    • 防止邮件列表循环
    • 垃圾邮件控制

    【讨论】:

    • AFAIK,即使像 Postfix 或 sendmail 这样的专业级 SMTP 服务器也没有提供 POP 或 IMAP,只是忽略 PGP/MIME,因为它们将正文视为不透明的有效负载,不提供合成界面或任何邮件列表支持。所有这些都是其他应用程序的问题。 MTA 不是网络邮件...
    • 同样,线程、邮件撰写和邮件列表管理根本不在 MTA 的范围内。
    猜你喜欢
    • 2010-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多