メールに Base64 が必要な理由
インターネットのメールは、長さに制限のある行で構成された 7 ビット ASCII テキストのために作られました。RFC 5322 は今も 1 行を 998 文字に制限しており、SMTP が安全な配送を保証するのは、受信側サーバーが 8BITMIME 拡張を表明しない限り 7 ビットのデータだけです。8 ビットの転送であっても、メッセージに NUL バイトや単独の CR・LF を含めてはならず、バイナリファイルにはいずれ必ずそれらが含まれます。生のバイト列を送るには BINARYMIME 拡張が必要ですが、送信者はサーバーからサーバーへと続く経路全体でそれを当てにすることはできません。
MIME(RFC 2045)は、Content-Transfer-Encoding ヘッダーでこれを解決します。このヘッダーは、パートの本文がどのように安全なテキストに変換されたかを受信側に伝えます。値は 5 つあります。
| 値 | 意味 |
|---|---|
7bit | プレーンな ASCII の行。エンコードなし |
8bit | 短い行の 8 ビットテキスト。8BITMIME が必要 |
binary | 生のバイト列。BINARYMIME が必要 |
quoted-printable | ほぼ読めるテキスト。非 ASCII のバイトは =XX でエスケープ |
base64 | 1 行最大 76 文字の Base64 |
添付ファイルには、メールソフトはほぼ必ず base64 を選びます。アルゴリズム自体は Base64 の仕組み で解説しています。
Base64 と quoted-printable の比較
quoted-printable は ASCII 文字をそのまま残し、それ以外のバイトを = と 2 桁の 16進数で書きます。UTF-8 の é(バイト C3 A9)は =C3=A9 になります。76 文字を超える行は、行末の = によるソフト改行で分割されます。
そのため、ときどきアクセント記号が混じる英語のテキストには理想的ですが、それ以外には無駄が多く、非 ASCII のバイト 1 つにつき 3 文字かかります。Base64 は内容にかかわらず一律 4/3 倍です。3 つの言語の文を 20 回繰り返したものを、Python の quopri.encodestring と base64.encodebytes(どちらも行末は LF)でエンコードしてみました。
| サンプル(UTF-8) | バイト数 | quoted-printable | Base64 |
|---|---|---|---|
| 英語のパングラム | 900 | 926 | 1,216 |
| アクセント記号を含むフランス語の文 | 1,060 | 1,830 | 1,435 |
| ロシア語のパングラム | 1,600 | 4,602 | 2,165 |
この選択は、たいていメールクライアントが代わりに行います。ほぼ ASCII のテキスト本文には quoted-printable か 7bit、添付ファイルや Latin 文字以外の文字体系のテキストには Base64 です。quoted-printable を手作業で読み書きする必要があるなら、quoted-printable ツール で双方向に変換できます。
生のメッセージ内の添付ファイル
添付ファイル付きのメッセージは multipart/mixed の本文です。各パートには独自のヘッダー、空行、そして内容があります。以下は Python の email パッケージで生成した、完全かつ最小限のメッセージです(読みやすさのため境界文字列を短くしています)。
From: Ana <ana@example.com>
To: bo@example.org
Subject: =?utf-8?b?Q2Fmw6kgbWVudSDigJMgR3LDtsOfZQ==?=
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="b1"
--b1
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Hi Bo,
The caf=C3=A9 menu is attached.
--b1
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="menu.pdf"
JVBERi0xLjcKJeLjz9MKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyA+PgplbmRvYmoKdHJhaWxl
cgo8PCAvUm9vdCAxIDAgUiA+PgolJUVPRgo=
--b1--
注目すべき部分:
Content-Typeはファイルの種類を、Content-Dispositionはファイル名を示します。Content-Transfer-Encoding: base64で、本文が Base64 であることがわかります。- Base64 はパートの最初の空行の後から始まり、次の
--boundary行の手前で終わります。 - 行は最後の行を除いてちょうど 76 文字です。1 行にはファイルの 57 バイトが入っています。
- 先頭の
JVBERi0は%PDF-を Base64 にしたものなので、これは PDF です。ほかのシグネチャは よくある Base64 の接頭辞 にまとめています。
HTML メールに埋め込まれた画像も、multipart/related パートの中で同じ仕組みを使います。各画像には Content-ID ヘッダーが付き、HTML からは <img src="cid:..."> として参照されます。実際の通信ではこれらの行は CRLF で終わるため、76 文字の Base64 ごとに 78 バイトかかります。詳しい計算は Base64 のサイズ増加 をご覧ください。折り返された出力を自分で作るには、MIME Base64 エンコーダー を使ってください。
エンコードされたヘッダー:=?UTF-8?B?…?=
Subject、From の表示名、一部のファイル名などのヘッダーは ASCII でなければなりません。RFC 2047 は、それ以外のテキストを encoded-word としてエンコードします。
=?charset?encoding?encoded-text?=
encoding は、Base64 なら B、_ がスペースを表す quoted-printable の変種なら Q です。どちらの文字も大文字・小文字を区別しないので、上の例で Python は =?utf-8?b? と書いています。実例をいくつか挙げます。すべて Python の email.header.decode_header で確認しました。
| エンコード後 | デコード結果 |
|---|---|
=?UTF-8?B?0J/RgNC40LLQtdGC?= | Привет |
=?utf-8?q?Caf=C3=A9_menu?= | Café menu |
=?ISO-8859-1?Q?Caf=E9_menu?= | Café menu |
=?utf-8?b?Q2Fmw6kgbWVudSDigJMgR3LDtsOfZQ==?= | Café menu – Größe |
1 つの encoded-word は最大 75 文字なので、長い件名は複数に分割され、折り返されたヘッダー行 1 行に 1 つずつ入ります。RFC 2047 は、隣り合う encoded-word の間の空白は無視されると定めており、各ワードは完全な文字を含まなければならないので、マルチバイトの UTF-8 文字が 2 つのワードにまたがって分割されることはありません。44 文字のロシア語の件名は、Python で次のように出力されました。
Subject: =?utf-8?b?0KHRh9GR0YIg0LfQsCDQvtC60YLRj9Cx0YDRjCDQuCDQsNC60YIg0YE=?=
=?utf-8?b?0LLQtdGA0LrQuCDQtNC70Y8g0LHRg9GF0LPQsNC70YLQtdGA0LjQuA==?=
各ワードはそれぞれパディングを持つ完全な Base64 文字列です。個別にデコードすると、1 つ目は Счёт за октябрь и акт с、2 つ目は верки для бухгалтерии になり、デコーダーがこれらをつなげて元の件名に戻します。
添付ファイル名にはもう 1 つの規格 RFC 2231 があり、filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.pdf のような形をしています。多くのメールソフトは filename="..." の中に RFC 2047 の encoded-word を入れることもあるので、パーサーは両方に対応する必要があります。
.eml ファイルから添付ファイルを取り出す方法
ブラウザで。 .eml を EML デコーダー で開けば、メッセージの本文を読み、添付ファイルを保存できます。処理はページ内でローカルに行われるので、請求書や個人情報を含むメッセージでも安心です。
Python で。 標準ライブラリが MIME をパースし、Base64 の本文と RFC 2047 のヘッダーの両方をデコードします。
import sys
from email import policy
from email.parser import BytesParser
with open(sys.argv[1], "rb") as f:
msg = BytesParser(policy=policy.default).parse(f)
print("Subject:", msg["Subject"]) # encoded-words decoded
for part in msg.iter_attachments():
name = part.get_filename()
data = part.get_content() # Base64 already decoded to bytes
with open(name, "wb") as out:
out.write(data)
print("saved", name, len(data), "bytes")
上のサンプルメッセージに対して実行すると、Subject: Café menu – Größe と saved menu.pdf 83 bytes が出力されました。実際のコードでは、name は送信者から来る値なので、書き込む前にサニタイズしてください。
手作業で。 パートの Base64 の行をファイルにコピーしてデコードします。生の .eml ファイルは行末に CRLF を使っており、GNU の base64 -d は最初の CR で invalid input を出して止まります。先に CR を取り除くか、-i でアルファベット外の文字を読み飛ばしてください。
tr -d '\r' < block.txt | base64 -d > menu.pdf
私たちのテストでは、openssl base64 -d -in block.txt -out menu.pdf は CRLF 版をそのまま受け付けました。Base64 のテキストしか手元にないなら、先頭のバイト列から種類を判定する Base64 からファイルへの変換 に貼り付けるか、PDF なら直接 Base64 から PDF への変換 に貼り付けてください。
生のメールのヘッダー、本文、添付ファイルを、ブラウザ内でローカルにデコードして表示します。
よくある質問
メールのソースにある Base64 の添付ファイルを開くには?
パートの空行から次の境界線までの Base64 の行をコピーし、ファイルとしてデコードします。EML デコーダー なら、.eml ファイルから添付ファイルを取り出してくれます。
メールの件名にある =?UTF-8?B? とは何ですか?
RFC 2047 の encoded-word の始まりです。文字コードが UTF-8 で、B は次の ? から ?= までのテキストが Base64 であることを示します。B の代わりに Q なら、quoted-printable の変種です。
メールの Base64 の行が 76 文字なのはなぜですか?
RFC 2045 が、MIME での Base64 の行を 76 文字までに制限しているからです。76 文字にはちょうど 57 バイトが収まり、この制限によって行の長さはメールで許される最大 998 文字を大きく下回ります。
メールのテキストには quoted-printable と Base64 のどちらが向いていますか?
ほぼ ASCII のテキストなら quoted-printable です。読みやすさが保たれ、サイズもほとんど増えません。バイナリデータや、日本語のように Latin 文字以外の文字体系のテキストには Base64 が向いています。quoted-printable だとサイズが 3 倍近くになることがあります。