0

我从smtpd开始,以便处理邮件队列、解析入站电子邮件并将它们发送回收件人(使用smtpdlib.sendmail)。我切换到aiosmtpd因为我需要多线程处理(而smtpd是单线程的,而且看起来已经停产了)。

顺便说一句,我对邮件信封内容的aiosmtpd管理感到困惑,这似乎比以前更加精细,如果您需要真正微调,那就太好了,但如果您只想处理正文而不修改其余部分,那就有点过大了。

举个例子,smtpd process_message方法只需要data_decode=True参数来处理和解码邮件正文而不接触任何东西,而 aiosmtpd HANDLE_data方法似乎无法自动解码邮件信封,并且经常出现嵌入图像、附件等的异常。 .

编辑添加了代码示例,首先是 smtpd:以下代码将实例化 smtp 服务器,等待端口 10025 上的邮件并通过 smtplib(都是本地主机)传递到 10027。为所有类型的邮件(基于文本/html,带有嵌入的图像、附件......)处理数据变量(基本上执行字符串替换,我的目标)是安全的

class PROXY_SMTP(smtpd.SMTPServer):
        def process_message(self, peer, mailfrom, rcpttos, data, decode_data=True):
        server = smtplib.SMTP('localhost', 10027)
        server.sendmail(mailfrom, rcpttos, data)
        server.quit()
server = PROXY_SMTP(('127.0.0.1', 10025), None)
asyncore.loop()

以前的代码运行良好,但采用单线程方式(= 一次 1 封邮件),所以我切换到 aiosmtpd 以进行并发邮件处理。与 aiosmtpd 相同的示例大致如下:

class MyHandler:
        async def handle_DATA(self, server, session, envelope):
                peer = session.peer
                mailfrom = envelope.mail_from
                rcpttos = envelope.rcpt_tos
                data = envelope.content.decode()
                server = smtplib.SMTP('localhost', 10027)
                server.sendmail(mailfrom, rcpttos, data)
                server.quit()

my_handler = MyHandler()

async def main(loop):
        my_controller = Controller(my_handler, hostname='127.0.0.1', port=10025)
        my_controller.start()
loop = asyncio.get_event_loop()
loop.create_task(main(loop=loop))
try:
     loop.run_forever()

此代码适用于文本电子邮件,但在使用任何复杂邮件(mime 内容、附件...)解码信封内容时会出现异常

如何在 aiosmtpd 中解析和解码邮件文本,像使用 smtpd 一样执行字符串替换,然后通过 smtplib 重新注入?

4

2 回答 2

2

您正在调用decode()您无法提前知道或预测其编码的东西。无论如何,修改原始 RFC5322 消息是非常有问题的,因为如果你想修改内容,你不能轻易地查看引用打印或 base64 正文部分。还要注意人类可见的标头中的 RFC2047 封装、RFC2231 中的文件名(或一些卑鄙的不合规的变态——许多客户端甚至几乎没有正确理解)等。请参见下面的示例。

相反,如果我猜对了你想要什么,我会将它解析为一个email对象,然后从那里获取它。

from email import message_from_bytes
from email.policy import default

class MyHandler:
    async def handle_DATA(self, server, session, envelope):
        peer = session.peer
        mailfrom = envelope.mail_from
        rcpttos = envelope.rcpt_tos
        message = message_from_bytes(envelope.content, policy=default)
        # ... do things with the message,
        # maybe look into the .walk() method to traverse the MIME structure
        server = smtplib.SMTP('localhost', 10027)
        server.send_message(message, mailfrom, rcpttos)
        server.quit()
        return '250 OK'

policy参数选择现代email.message.EmailMessage类,它替换 Python 3.2 及更早版本的遗留email.message.Message类。(许多在线示例仍在推广旧版 API;新版 API 更合乎逻辑且用途更广泛,因此如果可以的话,您希望将其作为目标。)

这还添加了return每个处理程序应根据文档提供的缺失语句。


这是一个示例消息,它在两个地方包含字符串“Hello”。因为 content-transfer-encoding 隐藏了内容,所以您需要分析消息(例如通过将其解析为email对象)才能正确操作它。

From: me <me@example.org>
To: you <recipient@example.net>
Subject: MIME encapsulation demo
Mime-Version: 1.0
Content-type: multipart/alternative; boundary="covfefe"

--covfefe
Content-type: text/plain; charset="utf-8"
Content-transfer-encoding: quoted-printable

You had me at "H=
ello."

--covfefe
Content-type: text/html; charset="utf-8"
Content-transfer-encoding: base64

PGh0bWw+PGhlYWQ+PHRpdGxlPkhlbGxvLCBpcyBpdCBtZSB5b3UncmUgbG9va2luZyBmb3I/PC
90aXRsZT48L2hlYWQ+PGJvZHk+PHA+VGhlIGNvdiBpbiB0aGUgZmUgZmU8L3A+PC9ib2R5Pjwv
aHRtbD4K

--covfefe--
于 2020-01-31T12:04:33.273 回答
0

OP 错误地将此文本添加到问题中;我将它作为(一半)答案移到这里。

- - 解决了 - -

这就是我到目前为止所得到的,仍然需要进行一些小的调整(主要是针对 mime 内容的单独处理和“重建”),但这解决了我的主要问题:在单独的线程上接收邮件,为文本处理腾出空间,休眠固定的时间最终交货前。感谢三人组的回答和评论,我找到了正确的方法。

import asyncio
from aiosmtpd.controller import Controller
import smtplib
from email import message_from_bytes
from email.policy import default
class MyHandler:
    async def handle_DATA(self, server, session, envelope):
        peer = session.peer
        mailfrom = envelope.mail_from
        rcpttos = envelope.rcpt_tos
        message = message_from_bytes(envelope.content, policy=default)
        #HERE MAYBE WOULD BE SAFER TO WALK CONTENTS AND PARSE/MODIFY ONLY MAIL BODY, BUT NO SIDE EFFECTS UNTIL NOW WITH MIME, ATTACHMENTS...
        messagetostring = message.as_string() ### smtplib.sendmail WANTED BYTES or STRING, NOT email OBJECT.
        ### HERE HAPPENS TEXT PROCESSING, STRING SUBSTITUTIONS...
        ### THIS WAS MY CORE NEED, ASYNCWAIT ON EACH THREAD
        await asyncio.sleep(15)
        server = smtplib.SMTP('localhost', 10027)
        server.send_message(mailfrom, rcpttos, messagetostring) ### NEEDED TO INVERT ARGS ORDER
        server.quit()
        return '250 OK' ### ADDED RETURN
    
 my_handler = MyHandler()
    
 async def main(loop):
        my_controller = Controller(my_handler, hostname='127.0.0.1', port=10025)
        my_controller.start()
 loop = asyncio.get_event_loop()
 loop.create_task(main(loop=loop))
 try:
        loop.run_forever()
于 2021-07-18T08:59:03.490 回答