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つです。

  1. 印字可能な ASCII 文字はそのまま書く(ただし = 自体は =3D にする)
  2. それ以外のバイトは = に続けて16進数2桁で書く
  3. 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 なら「ソースを表示」など、多くのメールソフトに元のメッセージを表示する機能があります(メニュー名はソフトやバージョンによって異なります)。ソースを開いたら、次の順で確認します。

  1. 本文パートの Content-Type にある charset
  2. 同じパートの Content-Transfer-Encoding
  3. 本文の見た目が、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-JP
  • Content-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 を確認し、実際にデコードして切り分けます。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲