2026年8月5日
2026年8月5日
メールのエンコーディング(Base64・QP)の仕組み:実際に変換して違いを確かめる
メールの本文や件名は、送る途中で壊れないように Base64 か Quoted-Printable(QP)という方式で「ASCII文字だけの形」に変換されています。この記事では、日本語の文字列を実際に Base64 と QP に変換・復元しながら、両者がどう違うのか、どちらが選ばれるのか、受信したメールのソースをどう読むのかを手を動かして確かめます。読み終えると、文字化けの相談を受けたときにメールのソースを開いて「どの層で何が起きているか」を自分で判断できるようになります。
なぜ変換が必要なのか
インターネットのメール(SMTP)は、もともと7ビットのASCII文字を前提に作られました。RFC 5321 では1行の長さにも上限(改行を含めて1,000オクテット)があります。現在は多くのサーバーが 8BITMIME 拡張に対応しており8ビットのデータを通せますが、途中の中継サーバーや古いシステムがすべて対応しているとは限りません。
そこで MIME(RFC 2045)では、本文を安全な文字だけで表すための「転送エンコーディング」を定めています。ヘッダーの Content-Transfer-Encoding に書かれる値がそれで、主に次の5種類です。
7bit:ASCIIだけで、1行が短いテキスト(変換なし)8bit:8ビットの文字を含むが、行の長さは制限内(変換なし)binary:制限なしの生データ(SMTPではほぼ使われない)base64:任意のバイト列を64種類の文字で表すquoted-printable:ASCIIはそのまま、それ以外を=XXで表す
ここで大事なのは、転送エンコーディングは「文字コード(charset)」とは別の層だという点です。charset=UTF-8 は「元のバイト列をどの文字コードで読むか」、Content-Transfer-Encoding: base64 は「そのバイト列をどう運ぶか」を表します。文字化けを調べるときは、この2つを分けて考えるのが出発点です。
Base64を手で追ってみる
Base64 は、バイト列を3バイト(24ビット)ずつ区切り、それを6ビット×4つに分けて、A-Z a-z 0-9 + / の64文字に置き換える方式です。最後が3バイトに満たないときは = で埋めます。
「あ」は UTF-8 では E3 81 82 の3バイトです。ビットに直して6ビットずつ区切ると、次のようになります。
E3 81 82
11100011 10000001 10000010
111000 111000 000110 000010
56 56 6 2
'4' '4' 'G' 'C' → 44GC
コマンドで確かめてみます。Linux や macOS のターミナル、Windows なら WSL や Git Bash で試せます。
printf 'あ' | base64
# 44GC
printf '日本語' | base64
# 5pel5pys6Kqe
echo '5pel5pys6Kqe' | base64 -d
# 日本語
printf を使っているのは、echo だと末尾に改行が付き、結果が変わってしまうためです。手元の計算と結果が合わないときは、まず改行や文字コードの違いを疑ってください。端末の文字コードが UTF-8 以外だと、同じ「日本語」でも別のバイト列になります。
Base64 の性質として、3バイトが4文字になるので、データ量は約4/3倍(約33%増)になります。さらに RFC 2045 では本文の1行を76文字以内とするため、途中に改行が入り、実際にはもう少し増えます。
Quoted-Printableを手で追ってみる
QP は「読める部分はそのまま残す」方式です。ルールは主に次の3つです。
- 印字可能な ASCII 文字はそのまま書く(ただし
=自体は=3Dにする) - それ以外のバイトは
=に続けて16進数2桁で書く - 1行は76文字以内。長い行は末尾に
=を付けて折り返す(ソフト改行)。受信側はこの=と改行を取り除いてつなげ直す
Python の標準ライブラリ quopri で確かめてみます。
python3 -c "import quopri; print(quopri.encodestring('日本語'.encode('utf-8')).decode())"
# =E6=97=A5=E6=9C=AC=E8=AA=9E
python3 -c "import quopri; print(quopri.encodestring('WordPressの通知'.encode('utf-8')).decode())"
# WordPress=E3=81=AE=E9=80=9A=E7=9F=A5
英字の「WordPress」はそのまま読めて、日本語の部分だけが =XX に変わっているのが分かります。
Base64とQP、どちらが選ばれるのか
同じ「日本語」を比べると、Base64 は12文字、QP は27文字でした。UTF-8 の日本語は1文字が3バイトなので、QP では1文字が9文字(=E6=97=A5)に膨らみます。
一般的な傾向は次のとおりです。
- 本文の大半が ASCII(英文、HTML のタグ、URL など)なら QP のほうが短く、ソースを見ても内容を読み取りやすい
- 日本語が多い本文や、画像・PDF などの添付ファイルは Base64 のほうが小さい
- 添付ファイルのようなバイナリは、事実上 Base64 一択
実際にどちらを使うかは、メールを組み立てるソフトやライブラリが決めます。たとえば PHPMailer では Encoding プロパティで指定でき、既定は 8bit ですが、行が長すぎる本文などでは自動で QP に切り替える処理があります(2026年10月現在)。バージョンによって挙動が異なる可能性があるため、利用中のバージョンのソースやドキュメントで確認してください。
件名や差出人名はRFC 2047の「encoded-word」
本文とは別に、件名(Subject)や差出人の表示名など、ヘッダーに日本語を入れるときは RFC 2047 の「encoded-word」という書き方を使います。形式は次のとおりです。
=?文字コード?方式?エンコード済みの文字列?=
Subject: =?UTF-8?B?5pel5pys6Kqe?=
Subject: =?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E?=
方式の B は Base64、Q は QP に似た「Q エンコーディング」です。Q エンコーディングでは、スペースを _ で表すなど本文の QP と細部が違います。また、1つの encoded-word は75文字以内と決められているため、長い件名は複数の encoded-word に分けて並べます。受信側は隣り合う encoded-word の間の空白を無視してつなげます。
PHP では mb_encode_mimeheader() や iconv_mime_encode()、Python では email.header.Header でこの形式を作れます。自前で文字列を組み立てると、マルチバイト文字の途中で分割してしまい、件名の一部だけが化ける原因になります。ライブラリに任せるのが無難です。
受信したメールのソースを読む
Gmail なら「メッセージのソースを表示」、Thunderbird なら「ソースを表示」など、多くのメールソフトに元のメッセージを表示する機能があります(メニュー名はソフトやバージョンによって異なります)。ソースを開いたら、次の順で確認します。
- 本文パートの
Content-Typeにあるcharset - 同じパートの
Content-Transfer-Encoding - 本文の見た目が、2で宣言した方式と一致しているか
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: base64
44GT44KT44Gr44Gh44Gv44CCCuODhuOCueODiOOBp+OBmeOAgg==
この本文を base64 -d で戻すと「こんにちは。テストです。」になります。HTML メールや添付付きのメールは multipart でパートが分かれており、パートごとに Content-Type と Content-Transfer-Encoding を持っている点に注意してください。
文字化けのよくある原因は、次のような「宣言と中身のずれ」です。
Content-Transfer-Encodingがbase64なのに、本文が変換されていない(または二重に変換されている)charsetは UTF-8 なのに、実際のバイト列が Shift_JIS や ISO-2022-JPContent-Transfer-Encoding: 8bitのまま、8BITMIME に対応していない経路を通った
デコードしてみて正しい日本語が出てくるなら、転送エンコーディングは正常で、問題は charset 側にあると切り分けられます。
WordPressから送るメールで意識すること
WordPress の wp_mail() は内部で PHPMailer を使っており、文字コードは基本的にサイトの設定(通常は UTF-8)が使われます。転送エンコーディングを明示的に変えたいときは、phpmailer_init アクションで PHPMailer のオブジェクトを受け取って設定できます。
<?php
// 子テーマの functions.php または mu-plugin に置く例
add_action( 'phpmailer_init', function ( $phpmailer ) {
$phpmailer->CharSet = 'UTF-8';
$phpmailer->Encoding = 'base64'; // 日本語の多い本文向け
} );
この設定はサイトから送られるすべてのメールに影響します。SMTP 系のプラグインが同じ箇所を設定している場合は、どちらの値が有効になるかを確認し、本番の前にステージング環境で自分宛てにテスト送信してソースを確かめてください。
まとめ
- 転送エンコーディング(Base64・QP)は「どう運ぶか」、charset は「どの文字コードで読むか」で、別の層の話です。
- Base64 は3バイトを4文字にする方式で約33%増。日本語や添付ファイルに向いています。
- QP は ASCII をそのまま残し、それ以外を
=XXにする方式。英文主体の本文に向いています。 - 件名や表示名は RFC 2047 の encoded-word(
=?UTF-8?B?...?=)で表し、組み立てはライブラリに任せます。 - 文字化けを調べるときは、メールのソースで
charsetとContent-Transfer-Encodingを確認し、実際にデコードして切り分けます。