aiosmtpd 与自定义控制器和 SMTP 工厂的怪异

aiosmtpd weirdness with custom controller and SMTP factory

首先,我正在使用 aiosmtpd,并尝试编写一个环绕它的 class 以使用 StartTLS 以编程方式启动 SMTP 服务器。现在,直到最近,这段代码与您可能传递给它的任何处理程序都按预期工作,例如我编写的用于调整消息参数的基本消息处理程序等,并将其作为消息的一部分传递 headers.

import asyncio

import aiosmtpd
import aiosmtpd.controller
import aiosmtpd.handlers
import aiosmtpd.smtp

import email

import regex
import logging

import ssl


EMPTYBYTES = b''
COMMASPACE = ', '
CRLF = b'\r\n'
NLCRE = regex.compile(br'\r\n|\r|\n')


class StartTLSServer(aiosmtpd.controller.Controller):
    def __init__(self, handler, ssl_cert_file, ssl_key_file, loop=None, hostname=None,
                 port=8025, *, ready_timeout=1.0, enable_SMTPUTF8=True, decode_data=False,
                 require_starttls=True, smtp_ident=None, data_size_limit=10485760,
                 smtp_timeout=300):
        context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        context.load_cert_chain(ssl_cert_file, ssl_key_file)
        self.tls_context = context
        self.require_starttls = require_starttls
        self.enable_SMTPUTF8 = enable_SMTPUTF8
        self.decode_data = decode_data
        self.smtp_ident = smtp_ident
        self.data_size_limit = data_size_limit
        self.smtp_timeout = smtp_timeout
        super().__init__(handler, loop=loop, hostname=hostname, port=port,
                         ready_timeout=ready_timeout, enable_SMTPUTF8=enable_SMTPUTF8)

    def factory(self):
        return aiosmtpd.smtp.SMTP(self.handler, data_size_limit=self.data_size_limit,
                                  enable_SMTPUTF8=self.enable_SMTPUTF8,
                                  decode_data=self.decode_data,
                                  require_starttls=self.require_starttls,
                                  hostname=self.smtp_ident,
                                  ident=self.smtp_ident,
                                  tls_context=self.tls_context,
                                  timeout=self.smtp_timeout)
                                 
                                  
class MessageHandler(aiosmtpd.handlers.Message):
    def __init__(self, message_class=None, *, loop=None):
        super().__init__(message_class)
        self.loop = loop or asyncio.get_event_loop()

    async def handle_DATA(self, server, session, envelope):
        message = self.prepare_message(session, envelope)
        await self.handle_message(message)
        return '250 OK'

    def prepare_message(self, session, envelope):
        # If the server was created with decode_data True, then data will be a
        # str, otherwise it will be bytes.
        data = envelope.content
        if isinstance(data, bytes):
            message = email.message_from_bytes(data, self.message_class)
        else:
            assert isinstance(data, str), (
              'Expected str or bytes, got {}'.format(type(data)))
            message = email.message_from_string(data, self.message_class)
        message['X-Peer'] = str(session.peer)
        message['X-Envelope-MailFrom'] = envelope.mail_from
        message['X-Envelope-RcptTo'] = COMMASPACE.join(envelope.rcpt_tos)
        return message  # This is handed off to handle_message directly.

    async def handle_message(self, message):
        print(message.as_string())
        return

这驻留在 custom_handlers.py 中,随后在测试中通过 Python 控制台调用,如下所示:

>>> from custom_handlers import StartTLSServer, MessageHandler
>>> server = StartTLSServer(MessageHandler, ssl_cert_file="valid cert path", ssl_key_file="valid key path", hostname="0.0.0.0", port=25, require_starttls=True, smtp_ident="StartTLSSMTPServer01")
>>> server.start()

当我想停止测试服务器时,我会简单地做一个 server.stop() 但是在处理任何消息的过程中,我们得到 hard-stopped 这个邪恶的错误:

Traceback (most recent call last):
  File "/home/sysadmin/.local/lib/python3.8/site-packages/aiosmtpd/smtp.py", line 728, in _handle_client
    await method(arg)
  File "/home/sysadmin/.local/lib/python3.8/site-packages/aiosmtpd/smtp.py", line 1438, in smtp_DATA
    status = await self._call_handler_hook('DATA')
  File "/home/sysadmin/.local/lib/python3.8/site-packages/aiosmtpd/smtp.py", line 465, in _call_handler_hook
    status = await hook(self, self.session, self.envelope, *args)
TypeError: handle_DATA() missing 1 required positional argument: 'envelope'

现在,我可以使用传递到 SMTP 工厂的任何处理程序来复制它。

但是,我无法使用带有调试处理程序的普通 aiosmtpd 复制它,如文档中定义的那样:

aiosmtpd -c aiosmtpd.handlers.Debugging stdout -l 0.0.0.0:8025

... 效果很好。将调试处理程序传递到 StartTLSServer 会导致与自定义 MessageHandler class 相同的错误,即使使用调试处理程序也是如此。

我是否遗漏了关于我的 class 的一些明显的东西,它在这里以一种不同于 aiosmtpd 预期的编程用法的方式爆炸?

您缺少 () 以实例化 MessageHandler 的对象 class:

>>> server = StartTLSServer(MessageHandler(), ...)

当您只传递 MessageHandler 而没有 () 时,aiosmtpd 将尝试调用常规函数 MessageHandler.handle_DATA(...)(与绑定方法函数 MessageHandler().handle_DATA(...) 相反)。

这个常规函数有四个参数:一个 MessageHandler 实例作为它的第一个参数,后面是通常的服务器、会话和信封参数。这解释了为什么错误消息抱怨缺少位置参数。

PS,请注意您的 handle_DATA 实现是多余的,因为它与基础 class aiosmtpd.handlers.Message 中的实现相同 - 所以您可以删除它, 它应该仍然可以正常工作。