Как вы анализируете элементы почтового конверта с помощью aiosmtpd в Python? - PullRequest
0 голосов
/ 30 января 2020

Я начал с smtpd , чтобы обработать очередь почты, проанализировать входящие электронные письма и отправить их обратно получателям (используя smtpdlib.sendmail ). Я переключился на aiosmtpd , так как мне требовалась многопоточная обработка (в то время как smtpd однопоточен и, кроме того, выглядит как прекращенный).

Кстати, я озадачен aiosmtpd управление почтой содержимое конверта , которое кажется гораздо более детализированным, чем раньше, так хорошо, если вам нужна действительно точная настройка, но несколько больше, если вы просто хотите обрабатывать тело без изменения rest.

Например, smtpd process_message метод, который просто необходим data_decode = True параметр для обработки и декодирования тела письма, не касаясь чего-либо, а aiosmtpd HANDLE_data метод кажется неспособным автоматически декодировать почтовый конверт и часто дает исключения со встроенными изображениями, вложениями и т. Д. ...

РЕДАКТИРОВАТЬ добавлены примеры кода, Сначала smtpd: следующий код создаст экземпляр сервера smtp, ожидающего почту через порт 10025 и доставляющий до 10027 через smtplib (оба localhost). Безопасно работать с переменной data (в основном, выполнять подстановку строк, моя цель) для всех видов почты (на основе текста / 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()

Этот код хорошо работает для текстовых электронных писем, но дает исключения при декодировании envelope.content для любой сложной почты (mime-контент, вложения ...)

Как я могу анализировать и декодировать почтовый текст в aiosmtpd, выполнять подстановку строк, как я делал с smtpd, и повторно вводить через smtplib? Спасибо

--- решено ---

Это то, что я получил до сих пор, небольшие изменения все еще необходимы (в основном для отдельной обработки содержимого 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()

1 Ответ

1 голос
/ 31 января 2020

Вы звоните decode() на то, чью кодировку вы не можете знать или предсказать заранее. В любом случае, модификация необработанного сообщения RFC5322 крайне проблематична c, потому что вы не можете легко заглянуть внутрь частей тела для печати в кавычках или base64, если хотите изменить содержимое. Также следите за инкапсуляцией RFC2047 в видимых человеком заголовках, именами файлов в RFC2231 (или какими-то ужасно несовместимыми извращениями - многие клиенты не понимают этого даже почти правильно) et c. Ниже приведен пример.

Вместо этого, если я правильно угадываю, что вы хотите, я бы проанализировал его в 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, который заменяет устаревший класс email.message.Message с Python 3.2 и более ранних. (Многие онлайн-примеры по-прежнему рекламируют устаревший API; новый более логичен и универсален, поэтому вы можете использовать его, если можете.)

Это также добавляет отсутствующий оператор return, который каждый обработчик должен предоставить согласно документацию.


Вот пример сообщения, которое содержит строку "Hello" в двух местах. Поскольку кодировка передачи содержимого скрывает содержимое, вам необходимо проанализировать сообщение (например, проанализировав его в объекте 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--
...