はじめに
前回は、FastAPI と aiosmtpd を 1つのプロセスで動かす方法について解説しました。
今回は、SMTP サーバーが受け取った「生のデータ(bytes)」を、どうやって人間が読める形にパースしているのか。
Python 標準の email ライブラリを使いつつ、地味にハマるポイントをどう回避したかをお話しします。
1. そもそもメールのデータってどうなってるの?
SMTP サーバー(aiosmtpd)が受け取るデータは、ただの巨大な bytes です。
中身は MIME 規格に則ったテキストですが、日本語の件名は =?utf-8?b?...?= みたいにエンコードされていますし、画像や HTML が混ざると「マルチパート」という再帰的な構造になります。
これを自力でパースするのはさすがに地獄ですが、Python には伝統ある email ライブラリがあります。
2. 現代的なパース: policy=policy.default
古い記事だと email.parser.Parser を使う例が多いようですが、Python 3.6 以降なら policy を指定するのが今風かなと思います。
import email
from email import policy
# 生の bytes からメッセージオブジェクトを生成
msg = email.message_from_bytes(raw_data, policy=policy.default)
これだけで、ヘッダーのデコードや改行コードの処理をある程度よしなにやってくれます。……が、それでも「かゆいところ」は残ったりします。
3. 地味に気を使うポイント
(1) 重複ヘッダーの扱い
Received ヘッダーのように、同じ名前のヘッダーが複数存在することがあります。MailOrca では、以下のように処理しています。
- 1つしかないヘッダー:単一の文字列として保持
- 複数あるヘッダー:リストとして保持
これを msg.get_all(key) を使いつつ整理しています。
(2) MIMEデコードの「念のため」
policy.default を使っていても、たまに壊れたエンコードのメールが飛んでくると例外を吐いたりします。
MailOrca では email.header.decode_header と make_header を組み合わせて、より安全にデコードする処理を挟んでいます。
(3) マルチパートの再帰的な走査
メールの中身(本文)を探すには、木構造を歩き回る(walk)必要があったりします。みんながみんな、シンプルな構造してくれてなかったりするんですよね……。
body_text = None
body_html = None
def extract_part(part):
nonlocal body_text, body_html
content_type = part.get_content_type()
payload = part.get_payload(decode=True)
if payload:
# ... 文字コードを推測してデコード ...
charset = part.get_content_charset() or "utf-8"
try:
text = payload.decode(charset, errors="replace")
except LookupError:
text = payload.decode("utf-8", errors="replace")
if content_type == "text/plain":
body_text = text
elif content_type == "text/html":
body_html = text
if msg.is_multipart():
for part in msg.walk():
extract_part(part)
else:
extract_part(msg)
text/plain と text/html の両方が入っている場合、どちらも抽出して Web UI で切り替えられるように保持しておくのがポイントです。ただしそれぞれが複数ある場合はひとつだけです。あとは捨てています。少なくとも今私が使う範囲では、そんなメールは飛ばしませんので。将来は対応させるのも、さらに画像などの添付ファイルなんかも取り扱ってみるのも面白いかもしれません。
4. オンメモリ・データストアの設計
MailOrca は軽量さを重視して、データベースを使いません。すべてメモリ上のリスト (STORE.mails) に放り込んでいます。
「メモリが溢れないの?」という心配には、max_history (最大保持数)設定で対応。新しいメールが入るたびに insert(0, entry) して、上限を超えたら pop() するというシンプルなキュー構造です。開発用ツールなら、これで十分すぎるほど高速に動きます。
今回のまとめ
-
email.message_from_bytes(policy=policy.default)は必須。 - ヘッダーのデコードと重複処理は丁寧に。
- マルチパートは
walk()で走査して、Text と HTML を両方拾う。
さて、データが準備できたら、次はいよいよ Web UI での表示 です。
Jinja2 テンプレートの中で、どうやって安全に HTML メールを表示しているのか。「iframe 活用術と URL リンク化(Web UI編)」 に続きます!