PythonのEmailエコシステム ― Tokyo Python Meetupでの登壇記録

2026年4月のTokyo Python Meetupで、メールプロトコルとPythonのメールツールキットについて登壇した。これはそのトークの拡張版で、コード例、スキップせざるを得なかった背景説明、そしてスライド操作でもたついた部分も含めて書き起こしたものである。


スライドは31枚、持ち時間は20分。誰にとっても良い比率ではない。2026年4月15日、Tokyo Python Meetup(一部では「Python Anonymous」とも呼ばれている)で、私は合理的な人間がやることをやった。一番面白い部分を駆け足で通過し、本題に入る前に時間切れになった。

この記事は、本来やりたかったバージョンである。同じ内容、同じコード、同じ意見だが、ディスプレイの設定に手間取りながらタイマーが刻まれていくプレッシャーはない。

スライドをダウンロード(PDF)

P.S Meetupについてご興味があるなら、ちょっとしたレポート記事をKafkai AIブログに掲載してるので読んでほしい。

Talking About KaiMail at Tokyo Python Meetup

なぜ私がメールの話をするのか

理由は2つある。どちらも利己的なものだ。

まず、私はKaiMailというカスタムドメイン向けのメール転送・送信サービスを作った。ほぼすべてがPythonで構築されている。パースとメッセージ生成には標準ライブラリ、DKIM、SPF、ARCにはPyPIパッケージを使っている。この記事に出てくるプロトコル、ライブラリ、落とし穴のすべてが、本番環境で実際にぶつかったものだ。

そしてもう1つ、こちらが本当のきっかけである。私がPyCon JPPyCon MYの運営をしていたとき、私たちの「メールシステム」は一人のGmailだった。スポンサーからの問い合わせは、その担当者が休暇中は何日も放置された。セキュリティ報告は個人のメールボックスに届いていた。新しい共同議長が就任するとき、パスワードをLINEで転送していた。恥ずかしい話だ。

本当に必要だったのはシンプルなことだった。自分たちのドメインにcontact@sponsors@security@を設定し、それぞれ適切な担当者の受信箱に転送する。メールサーバー不要、ユーザーごとの課金なし、パスワード共有なし。DNSと転送サービスだけで済む話だった。その不満がKaiMailの誕生につながり、現在ではオープンソースコミュニティ向けにプロジェクトメール設定の無料プログラムも提供している。

私のキャリアはSMTPとメールサーバーから始まった。メールには特別な思い入れがある。古い技術だが、Pythonがあればそのすべてを扱える。

メールの簡単な歴史

トークの冒頭で、聴衆にいくつかの数字を投げかけて、意味がわかれば手を挙げてもらった。1971、821、1995、993、587、25。ポート25で何人か手が挙がった。それ以外はほぼ反応なし。それも当然だろう。ほとんどの開発者はAPIやsend_mail()関数を通じてメールに触れるだけで、その下で何が起きているかなど考えない。

要約するとこうなる。

  • 1971年: Ray Tomlinson がARPANET上で最初のネットワークメールを送信。@記号を選んだのは、人名にあまり使われていなかったから。最初のメッセージは「QWERTYUIOP」だったとされている。テストメッセージとしては十分に深遠だ。
  • 1976年: エリザベス女王が国家元首として初のメールを送信。(会場で「1976年に生きていた人は?」と聞いたら、2人だけ手が挙がった。全員の年齢がバレた瞬間だった。)
  • 1982年: RFC 821(SMTP)とRFC 822(メッセージフォーマット)が標準化。
  • 2001年: RFC 2822がメッセージフォーマットを現代化。
  • 2008年: RFC 5322、現行のメールメッセージ標準。

要点はこうだ。メールはWorld Wide Webよりおよそ20年先に存在していた。HTTP、HTML、ブラウザ、そのどれもメールが動いていたときには存在していなかった。全世界で40億人以上がメールを使っており、彼らが依存しているプロトコルは、小規模で信頼関係のある学術ネットワーク向けに設計されたものだ。

メールの歴史を最初から詳しく辿りたい方は、メールの長い旅路をお読みいただきたい。ARPANETから1日4000億通に至るまでの全容を書いている。

メールプロトコル ― SMTP、IMAP、POP3とMIME

インターネット上のメールについて語るとき、それはRFCで定義されたプロトコルの話になる。主要なものは以下のとおりだ。

プロトコル ポート 用途
SMTP 25 / 587 メッセージの送信とリレー
IMAP 993 メールの閲覧(サーバー側に保管)
POP3 995 メールの閲覧(ダウンロードして削除)

SMTPは郵便サービスだと考えるとわかりやすい。差出人から受取人へ手紙を届ける。IMAPとPOP3は郵便受けだ。届いたものを読むために使う。

ポート25はサーバー間のリレーで、メール転送エージェント(MTA)同士の通信に使われる。ポート587は認証付きのクライアント投稿(submission)用で、メールクライアント(MUA)がサーバーにメッセージを渡すときに使う。どちらもSMTPを話すが、同じものではない。

そしてMIME(Multipurpose Internet Mail Extensions)。これが現代のメールを可能にしている技術だ。メールはHTTPよりはるかに古いプロトコルで、文字ベースであってバイナリではない。では、プレーンASCIIテキスト用に設計されたもので画像やPDFやHTMLをどうやって送るのか。MIMEがその答えだ。

MIMEはコンテンツタイプ(text/plaintext/htmlapplication/pdf)、文字エンコーディング、そして1つのメッセージに同一コンテンツの複数表現を含められるマルチパート構造を導入した。生のメールは以下のようになる。

From: [email protected]
To: [email protected]
Subject: Hello!
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="abc123"

--abc123
Content-Type: text/plain
Hello, Bob!

--abc123
Content-Type: text/html
<html><body><b>Hello, Bob!</b></body></html>

--abc123--

boundary文字列が各パートを区切る。multipart/alternative構造はメールクライアントに「これらは同じコンテンツの異なる表現だから、表示できる最適なものを選べ」と伝えている。クライアントは通常、HTMLが利用可能であればHTMLを選ぶ。

会場の全員に「帰宅したらメールの生データを見てほしい」と伝えた。Gmailなら「メッセージのソースを表示」というオプションがある。やってみてほしい。テキストエディタで開けばわかる。全部ただのテキスト、境界線、ヘッダーだ。まずはテキストのみのメールから始めて、次に画像やHTMLを含むメールを試すといい。RFCを何時間読むより、10分間Pythonで触ったほうがずっと多くのことを学べる。

メールの原罪 ― 送信者検証がない

これがメールの根本的な問題であり、解決策を見る前に理解しておく価値がある。

SMTPが1982年に設計されたとき、インターネットは小規模で信頼関係のある学術ネットワークだった。送信者を検証する理由がなかった。全員が互いを知っているネットワークで、他人になりすます人などいなかった。

今日、これはTCPコネクションを持つ誰もが誰にでもなりすませることを意味する。

MAIL FROM: <[email protected]>    <- 誰でも何でも書ける
RCPT TO: <[email protected]>
DATA
From: CEO <[email protected]>     <- ここにも
Subject: Wire $50,000 immediately

SMTPエンベロープのMAIL FROMとメッセージのFrom:ヘッダーは完全に独立している。一致する必要すらない。文字通り誰にでもなりすませる。トーク中に言ったとおり、[email protected]と名乗ってメールを送ることだって実際にできる。フィッシングはこうして成り立っている。それくらい簡単なのだ。

元のSMTPプロトコルは本質的に性善説で動いている。だからこそ認証が必要になった。

認証スタック

メール認証は、SMTPの上に重ねられた一連のプロトコルで、3つの問いに答えるものだ。送信サーバーは許可されているか? メッセージは改竄されていないか? チェックが失敗したらどうすべきか?

SPF ― Sender Policy Framework

答える問い: 「このドメインから送信を許可されているサーバーはどれか?」

SPFはDNSを通じて機能する。ドメイン所有者がTXTレコードを公開し、代理でメールを送信する権限を持つIPアドレスとサーバーを列挙する。

example.com.  IN TXT  "v=spf1 ip4:192.0.2.0/24 include:mailprovider.example.com -all"

受信サーバーはメールを受け取ると、送信サーバーのIPがDNSレコードと一致するかチェックする。結果はpassfailsoftfailneutralnoneのいずれかだ。

SPFはドアでIDをチェックするバウンサーのようなものだ。送信サーバーがゲストリストに載っているかを確認する。

DKIM ― DomainKeys Identified Mail

答える問い: 「このメッセージは本当にこのドメインから送信され、改竄されていないか?」

DKIMは公開鍵暗号を使う。送信者がメッセージヘッダーと本文を秘密鍵で署名し、受信者はDNSに公開された公開鍵で検証する。

DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=selector;
    h=from:to:subject:date; bh=abc123...; b=XYZ789...
selector._domainkey.example.com.  IN TXT  "v=DKIM1; k=rsa; p=MIIBIjAN..."

DKIMは手紙の封蝋のようなものだ。メッセージが改竄されておらず、主張どおりのドメインから送られたことを証明する。

個人的な話をすると、DKIMはもともとYahooが始めたプロトコルだ。私は当時Yahooに在籍しており、このプロトコルが構築される過程を見ていた。だから特別な思い入れがある。SPFもDKIMもDNSを通じて動作する。DNSはすでにインターネットの分散データベースだからこそ、この仕組みは賢い。

DMARC ― Domain-Based Message Authentication

答える問い: 「SPFやDKIMが失敗したとき、受信者は何をすべきか?」

DMARCは署名や検証の仕組みそのものではない。ポリシーだ。DNSに公開して、自分のメール送信ポリシーを世界に伝える。

_dmarc.example.com.  IN TXT  "v=DMARC1; p=reject; rua=mailto:[email protected]"

ポリシーはnone(監視のみ)、quarantine(迷惑メールフォルダへ)、reject(バウンス)のいずれかだ。鍵となる概念はアライメントで、SPFまたはDKIMのドメインがFrom:ヘッダーのドメインと一致しなければならない。DMARCがなければ、SPFが失敗してもそれに対して何をすべきかの指示がない。

もちろん、受信サーバーは好きなように処理できる。DMARCを通じて自分の希望を伝えるだけだ。

ARC ― Authenticated Received Chain

答える問い: 「転送を通じて認証をどう保持するか?」

4つの中で最も新しいプロトコルであり、メール転送サービスを構築する人にとっては必須だ。メールを転送すると、転送サーバーのIPが元の送信者のSPFレコードに含まれていないため、SPFが壊れる。ヘッダーを変更したりフッターを追加したりすると、DKIMも壊れることがある。

ARCは信頼のチェーンを作ることでこの問題を解決する。各転送者が、メッセージ到着時に確認した結果に署名する。

ARC-Seal: i=1; a=rsa-sha256; cv=none; d=forwarder.example.com; ...
ARC-Message-Signature: i=1; a=rsa-sha256; d=forwarder.example.com; ...
ARC-Authentication-Results: i=1; mx.forwarder.example.com;
    dkim=pass; spf=pass; dmarc=pass

cvフィールドはチェーンの状態を示す。cv=noneは「私が最初のホップで、それ以前は何もない」、cv=passは「前のチェーンを確認したところ有効だった」という意味だ。

KaiMailのような本番メール転送サービスでは、ARC署名は配信性にとって不可欠だ。これがなければ、転送されたメールは宛先でDMARCチェックに失敗する。ARCプロトコルについてPythonのコード例付きで詳しく書いた記事がある。また、メッセージが受信箱に届く前の段階でSMTPレベルの受信メールセキュリティ対策も複数実装している。

PythonのEmailツールキット

標準ライブラリ ― バッテリー同梱

Pythonはメールに対する包括的な標準ライブラリサポートを持つ、数少ない言語の1つだ。

モジュール 用途
email メールメッセージのパースと生成
smtplib SMTP経由でのメール送信
imaplib IMAP経由でのメール読み取り
poplib POP3経由でのメール読み取り

私自身はemailsmtplibを頻繁に使い、poplibをたまに、imaplibはほとんど使わない。emailモジュールはメッセージのパースと生成を扱い、MIME、エンコーディング、マルチパート構造のすべてを理解する。smtplibは低レベルのSMTP制御を提供する。この2つで大半のことが可能だ。

サードパーティライブラリ(PyPI)

認証(DKIM、SPF、ARC)にはサードパーティパッケージが必要だが、いずれも成熟し、メンテナンスもしっかりしている。

パッケージ 用途
dkimpy DKIMおよびARCの署名・検証
pyspf SPFレコードチェック
authres Authentication-Resultsヘッダー(RFC 8601)
cryptography DKIM用RSA鍵生成
dnspython DNSルックアップ(MX、TXTレコード)

KaiMailではこれらのライブラリをすべて組み合わせて使っている。生のバイト列を通じて相互運用しており、それがメール処理における共通通貨だ。

Pythonでメールを扱う

トークではこの部分で「プロジェクターのコードが小さすぎて見えない」と言われた箇所だ。今回はちゃんとやろう。

メールのパース

from email import message_from_bytes
from email.utils import parseaddr

# 生のメールバイト列をパースする(常にバイト列を使い、文字列は使わない!)
raw_email = open("message.eml", "rb").read()
msg = message_from_bytes(raw_email)

# ヘッダーにアクセス
sender_name, sender_addr = parseaddr(msg["From"])
subject = msg["Subject"]

# マルチパート構造をウォーク
if msg.is_multipart():
    for part in msg.walk():
        content_type = part.get_content_type()
        filename = part.get_filename()

        if filename:
            # 添付ファイル
            attachment_data = part.get_payload(decode=True)
        elif content_type == "text/plain":
            charset = part.get_content_charset() or "utf-8"
            text = part.get_payload(decode=True).decode(charset)
        elif content_type == "text/html":
            charset = part.get_content_charset() or "utf-8"
            html = part.get_payload(decode=True).decode(charset)

walk()メソッドはマルチパートツリーを平坦化し、すべてのパートを反復処理できるようにする。get_payload(decode=True)はContent-Transfer-Encoding(base64、quoted-printable)を自動で処理してくれる。

重要なポイント: 常にmessage_from_bytes()を使い、message_from_string()は使わないこと。 理由は2つあり、どちらも本番環境で重要だ。

まず、メールは異なるパートに複数の文字エンコーディングを含むことがある。message_from_bytes()はこれを正しく処理する。実運用のメール処理では、文字セットの扱いが最も厄介な部分だ。不正な文字セット宣言、文字セットの欠落、エキゾチックなエンコーディングに遭遇することになる。

そしてもう1つ、こちらのほうが痛い目を見る。DKIMやARCの署名はメッセージの生のバイト列に対して計算される。メールを文字列として読み込み、後から署名検証や転送のためにバイト列に再エンコードすると、微妙な差異(改行コードの変化、ヘッダーの再エンコード)が入り込み、暗号署名が無効になるリスクがある。最初からバイト列で扱えば、検証するバイト列と転送・保存するバイト列が同一であることが保証され、署名はそのまま維持される。

つまり、バイト列はメッセージのエンコーディングの忠実性と暗号的完全性の両方を保持する。文字列はそのどちらも保証しない。

メールの作成

from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "Alice <[email protected]>"
msg["To"] = "[email protected]"
msg["Subject"] = "Meeting Tomorrow"

# プレーンテキストコンテンツ
msg.set_content("Hi Bob,\n\nLet's meet at 2pm.\n\nAlice")

# HTML版を追加
msg.add_alternative(
    "<html><body>"
    "<p>Hi Bob,</p>"
    "<p>Let's meet at <b>2pm</b>.</p>"
    "<p>Alice</p>"
    "</body></html>",
    subtype="html",
)

# 添付ファイルを追加
with open("agenda.pdf", "rb") as f:
    msg.add_attachment(
        f.read(),
        maintype="application",
        subtype="pdf",
        filename="agenda.pdf",
    )

# バイト列にシリアライズ(署名や送信用)
raw_bytes = msg.as_bytes()

EmailMessageはPython 3.6で追加されたモダンなAPIだ。古いMIMEMultipart/MIMETextのアプローチはまだ動くが冗長になる。set_content()が主コンテンツを設定し、add_alternative()が自動的にmultipart/alternative構造を作る。順序が重要で、プレーンテキストを先に、HTMLを後にする。メールクライアントがサポートする最もリッチなフォーマットを選べるようにするためだ。

as_bytes()を呼ぶと、送信や署名の準備が整ったRFC 5322準拠の完全なメッセージが得られる。

smtplibでメールを送信

import smtplib
from email.message import EmailMessage

def send_email(msg: EmailMessage, mailfrom: str, recipients: list[str]):
    with smtplib.SMTP("smtp.example.com", 587) as smtp:
        smtp.starttls()
        smtp.login("username", "password")
        smtp.sendmail(
            from_addr=mailfrom,        # <- エンベロープ MAIL FROM
            to_addrs=recipients,
            msg=msg.as_bytes(),
        )

ポート587でSTARTTLSを使った標準的なSMTP投稿だ。カスタムドメインでのこうした処理をすべて(自動DKIM署名を含めて)サービスとして任せたいなら、それがまさにKaiMailのSMTP送信機能の役割だ。

エンベロープとヘッダーの違い

これはメールプログラミングで最も重要な概念の1つであり、プロトコルレベルでメールを扱ったことがない人はほぼ全員がつまずく。

エンベロープ(SMTP):                メッセージ(ヘッダー):
  MAIL FROM: <bounces+123@         From: [email protected]
              example.com>          To: [email protected]
  RCPT TO: <[email protected]>

エンベロープのMAIL FROM はバウンスや配信失敗の送り先を制御する。受信者には見えない。ヘッダーのFrom: は受信者のメールクライアントに表示されるものだ。

メール転送サービスでは異なる値が必要になる。From:ヘッダーは元の送信者を保持しつつ、エンベロープのMAIL FROMには特別なバウンス追跡用アドレスを使う。どの転送メッセージがバウンスしたかを把握するためだ。

会場で「Djangoを日常的に使っている人は?」と聞いたところ、かなりの手が挙がった。ここに問題がある。Djangoのsend_mail()はエンベロープとヘッダーを分離できない。send_mail()で使用するFromアドレスがエンベロープの送信者とFrom:ヘッダーの両方に使われる。基本的な通知メールには問題ないが、適切なバウンスハンドリング、転送、あるいは本格的なメール処理が必要になった瞬間に破綻する。

これが、本番のメールシステムがsmtplibを直接使う主な理由だ。この1つの概念で、メールの「なぜこうなるのか」の半分が説明できる。

PythonでのDKIM署名

鍵の生成

DKIM署名はRSA鍵ペアの生成から始まる。秘密鍵はサーバーに保管し、公開鍵はDNS TXTレコードに公開する。

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import base64

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
)

# 秘密鍵(秘密に保管 - 署名に使用)
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.TraditionalOpenSSL,
    encryption_algorithm=serialization.NoEncryption(),
)

# 公開鍵(DNS TXTレコードとして公開)
public_der = private_key.public_key().public_bytes(
    encoding=serialization.Encoding.DER,
    format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
dns_record = f"v=DKIM1; k=rsa; p={base64.b64encode(public_der).decode()}"

2048ビットが標準だ。2048未満は今では弱すぎるとされている。それ以上(4096ビットなど)にすると、公開鍵がDNS TXTレコードに収まる必要があるため、DNS UDPパケットサイズの制限に引っかかることがある。2048で問題ない。

メッセージへの署名

import dkim

signature = dkim.sign(
    message=msg.as_bytes(),
    selector=b"selector1",
    domain=b"example.com",
    privkey=private_pem,
)
# 署名をメッセージの先頭に追加
signed_message = signature + msg.as_bytes()

dkimpyライブラリが正規化、ヘッダー選択、署名計算の複雑さをすべて処理してくれる。結果としてDKIM-Signatureヘッダーが得られ、それをメッセージの先頭に追加する。

重要な注意点が1つ。dkimpyはCRLF改行を出力するが、一部のメールサーバーはLFのみでメッセージを保管する。改行コードの正規化が必要になることがある。そして、これは何度でも強調する。署名後にメッセージを再シリアライズしてはならない。 バイト列が少しでも変わると署名が無効になる。本番環境では、バイト列を取得してDKIM署名したら、まさにそのバイト列をそのまま送信する。

PythonでのSPFチェックとARC署名

SPFチェック

SPFチェックは単純だ。関数呼び出し1回で済む。

import spf

result, code, explanation = spf.check2(
    i="192.0.2.1",            # 送信サーバーのIP
    s="[email protected]",     # エンベロープ送信者
    h="mail.example.com",      # HELOホスト名
)
# result: "pass", "fail", "softfail", "neutral", "none"

送信サーバーのIP、エンベロープの送信者、HELOホスト名を渡すと、そのサーバーがそのドメインからの送信を許可されているかをライブラリが教えてくれる。

ARC署名(メール転送向け)

ARC署名はSPFとDKIMの検証の上に構築されるため、より複雑だ。正直に言うと、私が初めてARCを扱ったとき、かなりつまずいた。直感的ではない。

フローは次のとおり。まずSPFとDKIMを検証し、結果をAuthentication-Resultsヘッダーに記録し、その後全体をARC署名する。

import dkim
import authres

# 1. Authentication-Resultsヘッダーを構築
auth_results = str(authres.AuthenticationResultsHeader(
    authserv_id="mx.yourdomain.com",
    results=[
        authres.SPFAuthenticationResult(result="pass",
            smtp_mailfrom="[email protected]"),
        authres.DKIMAuthenticationResult(result="pass",
            d="example.com", s="selector1"),
    ],
))

# 2. メッセージをARC署名(認証結果を含む)
arc_headers = dkim.arc_sign(
    message=msg_bytes,
    selector=b"arc1",
    domain=b"yourdomain.com",
    privkey=private_pem,
    authserv_id=b"mx.yourdomain.com",
)

# 3. ARCヘッダーを逆順で先頭に追加(RFC 8617)
for header in reversed(arc_headers):
    msg_bytes = header + msg_bytes

ARCヘッダーは3つ一組(Seal、Message-Signature、Authentication-Results)で、RFC 8617に従い逆順で先頭に追加しなければならない。

このreversed()呼び出しが、私をつまずかせた部分だ。なぜ必要なのかを説明しよう。

dkimpyarc_sign()は、3つのヘッダーを計算が必要な順序で生成する。

  1. まずAuthentication-Results。SPF、DKIM、DMARCのチェック結果を記録するだけだからだ。
  2. 次にMessage-Signature。先ほど作ったAuthentication-Resultsを含むメッセージヘッダーと本文に署名するからだ。
  3. 最後にSeal。Message-Signatureを含むARCチェーン全体に署名するからだ。

各ヘッダーは前のものに依存するため、この順序で生成する必要がある。しかし、ヘッダーを1つずつ先頭に追加すると、最後に追加したものが一番上に来る。生成順に追加すると、Authentication-Resultsが一番上になってしまう。リストを逆順にしてから追加することで、正しい最終順序(Sealが一番上、次にMessage-Signature、最後にAuthentication-Results)になる。

以下は、実際のメールが2ホップを経て転送された例だ(Yahoo → Google Groups → KaiMail)。

ARC-Seal: i=2; a=rsa-sha256; t=1712217127; cv=pass;        <- cv=pass: 前のチェーンは有効
        d=google.com; s=arc-20160816;
        b=iD67k7qMUG25SzPI3bpk...
ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed;
        d=google.com; s=arc-20160816;                         ホップ2
        h=subject:message-id:to:from:date:dkim-signature;     (Google Groups
        b=NJKFf8IVE8U8jXrAaBki...                              -> KaiMail)
ARC-Authentication-Results: i=2; mx.google.com;
       dkim=pass [email protected]; spf=pass; dmarc=pass

ARC-Seal: i=1; a=rsa-sha256; t=1712217126; cv=none;         <- cv=none: チェーンの最初
        d=google.com; s=arc-20160816;
        b=f9CpKU5mEMgn1vXT3R32...
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed;
        d=google.com; s=arc-20160816;                         ホップ1
        h=subject:message-id:to:from:date:dkim-signature;     (Yahoo
        b=eWaBjG77uH6bJnXBKrg+...                              -> Google Groups)
ARC-Authentication-Results: i=1; mx.google.com;
       dkim=pass [email protected]; spf=pass; dmarc=pass

最新のホップ(i=2)がヘッダーの一番上に来るのは、各転送者がヘッダーをメッセージの先頭に追加するからだ。メールの仕組み(ヘッダーは上から積み重なる)を理解すれば納得がいく。しかし、初めて扱うときはつまずく。これは間違いない。

Pythonにおけるメールのライフサイクル全体像

全体像を示そう。すべてのステップでPythonを使う。構造には標準ライブラリ、暗号にはPyPIだ。

    生のバイト列(ネットワークまたはファイルから)
         |
         v
    +---------------------+
    |  email.message_     |   生のバイト列を
    |  from_bytes()       |   構造化されたメッセージに変換
    +---------+-----------+
              |
              v
    +---------------------+
    |  spf.check2()       |   SPFの検証
    |  dkim.verify()      |   DKIM署名の検証
    |  dkim.arc_verify()  |   ARCチェーンの検証
    +---------+-----------+
              |
              v
    +---------------------+
    |  authres.            |   検証結果を標準的な
    |  Authentication      |   ヘッダー形式で記録
    |  ResultsHeader()     |
    +---------+-----------+
              |
              v
    +---------------------+
    |  dkim.sign()        |   送信メッセージに署名
    |  dkim.arc_sign()    |   転送用にARCチェーンを追加
    +---------+-----------+
              |
              v
    +---------------------+
    |  smtplib.SMTP()     |   SMTPで配信
    |  .sendmail()        |   (エンベロープとヘッダーを分離)
    +---------------------+

インスペクション用にはメッセージオブジェクトにパースするが、暗号処理には常に生のバイト列を保持すること。一度署名したら、そのまさにそのバイト列を送信しなければならない。再シリアライズすれば署名は壊れる。

主なポイント

  1. Pythonの標準ライブラリは基本的なメール処理に必要なすべてを備えている。 パースと生成にはemailモジュール、送信にはsmtplib。メールの送受信だけならサードパーティライブラリは不要だ。

  2. 認証が重要。DKIM + SPF + ARC = 配信性。 適切な認証がなければ、メールは迷惑メールフォルダに入るか拒否される。GmailとYahooは2024年に送信者要件を厳格化した。「だいたい動く」ではもう通用しない。

  3. 実運用のメールは雑然としている。 エンコーディング、マルチパート構造、壊れたヘッダー、バウンス、50年分の後方互換性。今どき誰がそんなことをするか。誰もやらない。しかしメールはやっている。Pythonがそこを乗り越える手助けをしてくれる。

  4. Pythonが扱いやすくしてくれる。 dkimpypyspfauthres。成熟し、しっかりメンテナンスされたライブラリが難しい部分を引き受けてくれる。

  5. エンベロープとヘッダーの違いは根本的だ。 この1つの概念で、メールの癖の半分が説明できる。この記事から1つだけ持ち帰るなら、これであってほしい。

メールは古いが、退屈ではない。.emlファイルをダウンロードして、message_from_bytes()を実行して、構造を探ってみてほしい。RFCを何時間読むより、Pythonで10分間触ったほうがずっと多くを学べる。

おまけ ― PythonAsia 2026 マニラ

トークの最後に、PythonAsia 2026について少し紹介した。年に一度の地域別Pythonカンファレンスで、今年は3月21日から23日にマニラで開催された。

会場はマニラのマラテにあるデ・ラ・サール大学(DLSU)。フィリピン国外から18か国以上、500人を超える参加者が集まった。プログラムは3つの基調講演、7つのワークショップ、35のトーク、2つのライトニングトークセッション、1つのオープンスペースで構成された。3日目はEducation SummitとSprintsに充てられた。

Jay Miller氏が基調講演者の一人として登壇し、Python Asia Organisation(PAO)のブースも出展した。初日のVIPディナーはPAOの主催だった。スポンサーブースはホール全体を埋め尽くした。

PythonAsiaカンファレンスへの参加を強くお勧めする。この地域で最大規模の国際的なPythonカンファレンスの1つであり、欧州や米国まで飛ぶ必要がない。燃油サーチャージが上がり続ける昨今、アジアにワールドクラスのPythonカンファレンスがあることは重要だ。地域での他の選択肢として、6月にはPyCon SGもあり、東南アジア各地で年間を通じてカンファレンスが開催されている。

リソース

Pythonドキュメント

RFC

  • RFC 5321: SMTPプロトコル
  • RFC 5322: インターネットメッセージフォーマット
  • RFC 6376: DKIM署名
  • RFC 7208: SPF
  • RFC 8617: ARCプロトコル

PyPIパッケージ

  • dkimpy: DKIM/ARCの署名と検証
  • pyspf: SPFチェック
  • authres: Authentication-Resultsヘッダー
  • dnspython: DNSルックアップ

KaiMailブログの関連記事

オープンソースコミュニティの方へ

私たちは力になりたい。オープンソースプロジェクトやボランティアコミュニティが自分たちのドメインでメールを使えるよう支援する無料プログラムを提供している。メールサーバー不要、ユーザーごとの課金なし。プロジェクトやコミュニティを運営しているなら、教えてほしい。セットアップのお手伝いをする。

つながる

この記事に関する質問、あるいはメール、Python、その両方が交わる領域について話したい方は:

日本、AI、マーケティング、コミュニティについて書いている月刊ニュースレターもある。Kafkai Insightsの購読はこちら