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
中的实现相同 - 所以您可以删除它, 它应该仍然可以正常工作。
首先,我正在使用 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
中的实现相同 - 所以您可以删除它, 它应该仍然可以正常工作。