Парсинг писем из предоставленных EML-файлов: заголовки и части MIME

Парсинг писем преобразует предоставленные байты EML в заголовки сообщения, части MIME и метаданные для проверки. Используйте парсер, учитывающий MIME, чтобы сохранять повторяющиеся заголовки и декодировать текст согласно объявленной кодировке; не разбивайте сообщение регулярными выражениями. Разбор поля From или Reply-To описывает предоставленное сообщение, но не подтверждает, кто его отправил.

Этот процесс начинается с разрешённого EML-файла, предоставленного для проверки. Подключение к почтовому ящику не требуется. Руководство по извлечению данных из документов рассматривает деловые поля читаемых документов; здесь же сначала нужно понять структуру заголовков и составных частей сообщения.

Пример конфигурации, а не отчёт о проведённом тестировании. Интерфейсы рабочего процесса описаны в каталоге; Ai Flowi не проверяла их в рабочей среде для этого процесса EML. Функция и примеры с искусственно созданными данными ниже определяют предлагаемый контракт приёмки предоставленных байтов сообщения.

1. Определите границы байтовых данных и результата

Вам нужны AI Flowi Workflow, предоставленный образец EML с искусственно созданными данными или иной разрешённый образец и проверяющий. Для детерминированного локального разбора не нужны учётная запись электронной почты или ключ API модели. Если позже вы добавите модель или внешний сервис, используйте собственную учётную запись или ключ API провайдера и оплачивайте использование отдельно.

Сохраните исходные байты. Перекодирование EML-файла произвольным средством чтения текста до разбора может изменить сведения о кодировке. Этот пример принимает только байты, ограничивает сообщение размером 1 МиБ, допускает посещение не более 64 узлов MIME и ограничивает каждую декодированную часть обычного текста размером 256 КиБ. Это выбранные ограничения демонстрации, а не ограничения продукта или заявление о всесторонней защите от вредоносных входных данных.

Предполагаемый результат содержит исходные повторяющиеся заголовки, декодированные значения заголовков, части обычного текста, метаданные других частей и предупреждения. Храните их раздельно. Предупреждение означает, что проверяющий должен изучить источник; успешно возвращённый объект JSON не является одобрением содержимого сообщения.

По теме: Транскрибация аудио в Google Docs.

2. Изучите искусственно созданное сообщение с повторяющимися заголовками

From: sender@example.invalid
Reply-To: replies@example.invalid
Subject: =?utf-8?b?Q2Fmw6k=?=
Message-ID: <synthetic@example.invalid>
X-Trace: first
X-Trace: second
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Caf=C3=A9

Тема в заголовке и тело кодируют «Café» разными способами. Сохраните оба вхождения X-Trace в исходном порядке. Адреса и идентификатор сообщения — специально заданные условные значения. Reply-To указывает адрес для ответа внутри сообщения, а не доказывает, что отправитель контролирует этот адрес.

Составное сообщение с альтернативными представлениями может содержать обычный текст и HTML-версию одного содержимого. Не объединяйте их как независимые факты. В этом примере HTML не отображается, а текстовые вложения не становятся основным текстовым телом. Вложенные сообщения остаются отдельными записями метаданных; их заголовки и тело не переносятся во внешнее сообщение.

3. Используйте стандартную библиотеку Python с явно заданной политикой разбора

Предлагаемая функция использует BytesParser(policy=policy.default), сохраняет raw_items() и записывает ошибки в сообщении, выявленные парсером. Сначала текст строго декодируется в соответствии с объявленной кодировкой; при неизвестной кодировке или недопустимой последовательности текста функция записывает предупреждение и создаёт текстовое представление в UTF-8 с заменами, явно помеченное как содержащее потери информации.

Функция проверки EML с заданными ограничениями
from email import policy
from email.parser import BytesParser


def parse_eml(raw):
    if not isinstance(raw, bytes):
        raise TypeError("Supply EML bytes")
    if len(raw) > 1024 * 1024:
        raise ValueError("EML exceeds example byte limit")
    # ponytail: materializes the capped message before the part check; use a streaming parser for stricter hostile-input resource limits.
    msg = BytesParser(policy=policy.default).parsebytes(raw)
    out = {"headers": list(msg.raw_items()), "decoded_headers": [],
           "plain_text_parts": [], "other_parts": [], "warnings": []}
    out["decoded_headers"] = [(name, str(value)) for name, value in msg.items()]
    for name in ("From", "Subject", "Message-ID"):
        if not msg.get_all(name):
            out["warnings"].append("missing " + name)
    pending, seen = [msg], 0
    while pending:
        part = pending.pop()
        seen += 1
        if seen > 64:
            raise ValueError("EML exceeds example part limit")
        kind, filename = part.get_content_type(), part.get_filename()
        for header in part.values():
            out["warnings"].extend(type(d).__name__ for d in getattr(header, "defects", ()))
        if part.get_content_disposition() == "attachment" or filename is not None or kind == "message/rfc822":
            out["other_parts"].append({"type": kind, "filename": filename})
            if filename and (filename in (".", "..") or any(c in filename for c in "/\\") or any(ord(c) < 32 or ord(c) == 127 for c in filename)):
                out["warnings"].append("unsafe attachment name")
        elif part.is_multipart():
            pending.extend(reversed(list(part.iter_parts())))
        elif kind == "text/plain":
            payload = part.get_payload(decode=True) or b""
            if len(payload) > 256 * 1024:
                raise ValueError("Text part exceeds example byte limit")
            charset = part.get_content_charset() or "ascii"
            try:
                text = payload.decode(charset, errors="strict")
            except (LookupError, UnicodeError):
                out["warnings"].append("charset failure: " + charset)
                text = payload.decode("utf-8", errors="replace")
            out["plain_text_parts"].append(text)
        else:
            out["other_parts"].append({"type": kind, "filename": filename})
        out["warnings"].extend(type(d).__name__ for d in part.defects)
    return out

Вызовите parse_eml(raw) с уже предоставленными байтами. Пример не открывает произвольный путь к файлу и не загружает содержимое из сети. Имена вложений остаются метаданными: даже имя, пытающееся выйти из папки повторениями двух точек и косой черты, никогда не используется как путь назначения. Функция сообщает о подозрительных разделителях и управляющих символах; эта проверка не очищает имена файлов и не обнаруживает вредоносное ПО.

4. Отдельно настройте передачу в рабочем процессе

В каталоге Python Interpreter описан так: Выполняйте код Python с необязательными импортами. Используйте print() для просмотра результата. Его обязательные поля global_imports и python_code предоставляют интерфейс выполнения. Экспорт не определяет доступ к файлам, доступные импорты или способ оборачивания напечатанного JSON в results.

На мобильных устройствах таблицы примеров и рабочего процесса находятся в областях с прокруткой. Клавиатурный фокус позволяет изучать столбцы за пределами первоначально видимой области.

Настройки каталога, которые ещё требуют проверки во время выполнения
ЭтапПредлагаемые входные данныеГраница проверки
Python InterpreterЗадайте global_imports для модулей стандартной библиотеки, разрешённых средой выполнения; поместите проверенную функцию в python_code.Подтвердите импорты и разрешённый механизм передачи байтов. После тестового вызова с искусственными данными изучите results с объявленным типом JSON.
JSON Operationsdata: проверенный JSON парсера; выберите "Select Keys" или проверенное "JQ Expression" через operations.Сохраняйте заголовки, части и предупреждения раздельно. Не предполагайте, что stdout становится разобранным объектом без обёртки.
JSON Object Builder, необязательноfield_names: один проверенный ключ записи, например parsed_message.Изучите сгенерированный подключаемый вход и json_object. Синтаксис нескольких ключей и типы сгенерированных входов не экспортированы.

Определите ожидаемый результат предлагаемыми проверками на искусственных данных, затем изучите импорты и результаты на безвредном примере в своей рабочей области. У Python Interpreter нет экспортированного входа загрузки EML. Не придумывайте соединение Read File с интерпретатором и не предполагайте наличие в нём файлового дескриптора. До передачи в рабочий процесс подтвердите доставку байтов и сопоставления напечатанного результата.

5. Сохраняйте небольшой набор проверок ошибок

Предлагаемые проверки приёмки на искусственных данных
Тестовый примерОжидаемый результат проверки
Повторяющийся X-Trace и закодированные тема и телоОба исходных значения сохраняются; декодированные тема и тело сохраняют символ с диакритическим знаком.
Отсутствуют From, Subject или Message-IDВыводите предупреждения об отсутствующих заголовках, не придумывая значения.
Составное альтернативное представление обычного текста и HTMLСохраняйте обычный текст как текст, а HTML — как метаданные части без отображения.
Неизвестная кодировка, недопустимый UTF-8 или повреждённый base64Записывайте предупреждения об ошибках декодирования или об ошибках в сообщении, выявленных парсером; текст с заменами не является точным подтверждением исходного содержимого.
Имя вложения с попыткой выхода из папки и вложенное сообщениеСохраняйте метаданные без записи файлов и переноса вложенного текста в основное тело.
Отсутствует граница составного сообщенияПокажите ошибку структуры сообщения, выявленную парсером, не считая повреждённую структуру обычным полным текстом.
Неверный тип входных данных или превышенный лимит сообщения, текста либо частейВыдайте указанную ошибку до принятия записи проверки.

До принятия контракта примените эти критерии к разрешённым примерам вашего формата сообщений. Храните ожидаемое предупреждение рядом с каждым примером, чтобы его отсутствие было видно при проверке. Парсер может принять повреждённые данные и показать ошибки структуры сообщения; отсутствие исключения не доказывает правильную структуру или подлинность письма.

Каковы пределы доверия к разбору писем и когда нужно остановиться?

From, Reply-To и Message-ID — предоставленные заголовки. Аутентификация при передаче требует подтверждающих данных от доверенной принимающей системы; вставленный заголовок об аутентификации всё равно остаётся предоставленным текстом. Эта страница не гарантирует обнаружение подмены или проверку почтового ящика и не содержит инструкций отправки или получения.

Используйте контролируемый локальный парсер, если сообщение содержит чувствительные данные и внешняя обработка не нужна. Не исполняйте вложения, не отображайте HTML и не загружайте изображения по ссылкам. Отклоняйте данные сверх лимитов до добавления последующих действий и требуйте проверки источника при любой потере информации во время декодирования.

Тарифы: Go от USD8 в месяц при годовой оплате (USD10 помесячно), Plus от USD16 (USD20 помесячно), Pro 100 от USD80 (USD100 помесячно), Pro 200 от USD160 (USD200 помесячно) и Pro 500 от USD400 (USD500 помесячно), от 5,000 до 250,000 Flowi Tokens в месяц с возможностью пополнения; 1 токен за каждый запуск узла; использование сторонних API и моделей не включено.

Подсчитывайте запуски интерпретатора и операций обработки лишь после настройки передач между этапами. Отклонённое сообщение должно сохранять причину и ссылку на источник, не становясь принятой записью дальнейшей обработки. Данные об измеренном расходе ресурсов рабочего процесса для этого предлагаемого варианта проверки EML не приведены.

Автор: Ai Flowi · Опубликовано .