スキップしてメイン コンテンツに移動

今日こそ終止符!iPhoneの標準メーラーで添付ファイル名が文字化けする原因とその対応

OutlookからiPhoneにメールを送ると、添付ファイル名が =?UTF-8?Q?...?= と化ける問題。長年「相性の問題」で片付けられてきたこの怪奇現象に、休日の半日を潰してトドメを刺してきました。

1. 境界線の発見:「5Nの法則」

実験の結果、ファイル名に日本語(非ASCII)が混じっている場合、ある一定の条件でエンコード方式が切り替わることが分かりました。それが以下の「5Nの法則」です。

5Nの法則:
非ASCII文字を N 個含むとき、ASCII文字(英数字)が 5N 個に達した瞬間にiPhoneで文字化けが発生する。
非ASCII数 (N) Base64限界 (ASCII) 文字化け発動 (ASCII)
1文字 (あ) 4文字 5文字以上
2文字 (あい) 9文字 10文字以上
3文字 (あいう) 14文字 15文字以上

この法則はUTF-8だけでなく、欧州圏の iso-8859-1 (Latin-1) でも全く同様に発動しました。

2. なぜ「5」なのか?数学的背景

Outlookはメールヘッダーを1バイトでも節約しようと、以下の2つのエンコードを動的に使い分けています。

  • MIME-B (Base64): 全体を1.33倍にする。
  • MIME-Q (Quoted-Printable): 英数字はそのまま、非ASCIIを =XX 形式にする。

Latin-1の1バイト文字 Á を例にとると、Qエンコードでは =C1 (3バイト) に膨らみます。計算すると、英数字が5文字を超えたあたりで、全体をBase64で包むよりも、英数字をそのまま送れるQエンコードの方がヘッダーサイズが短くなるのです。Outlookの「ケチな最適化」がこの「5」という数字を生んでいます。

3. 犯人は誰だ?PHPによる「パケット偽装」実験

「Outlookが生成するQエンコードが悪いのか、それともiPhoneが悪いのか」を特定するため、以下の検証スクリプトを作成しました。意図的に添付ファイル名をQエンコードで送りつける「嫌がらせ」コードです。

// --- 検証用添付ファイルパート抜粋 ---
switch ($attachment_type) {
    case "B": // RFC 2047 Base64
        $filename_encoded = mb_encode_mimeheader($filename, "UTF-8", "B");
        $head = "Content-Type: application/octet-stream; name=\"{$filename_encoded}\"\r\n";
        $head .= "Content-Disposition: attachment; filename=\"{$filename_encoded}\"\r\n";
        break;
    case "Q": // RFC 2047 Quoted-Printable
        $filename_encoded = mb_encode_mimeheader($filename, "UTF-8", "Q");
        $head = "Content-Type: application/octet-stream; name=\"{$filename_encoded}\"\r\n";
        $head .= "Content-Disposition: attachment; filename=\"{$filename_encoded}\"\r\n";
        break;
    default: // RFC 2231 (モダンな形式)
        $head = "Content-Type: application/octet-stream;\r\n";
        $head .= "Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename) . "\r\n";
}

検証結果

  • To/FromヘッダーのQエンコード: iPhoneでも正常表示。
  • 添付ファイル名のQエンコード: iPhoneで文字化け確定。

驚くべきことに、iPhoneはQエンコードそのものが解釈できないわけではありません。「ヘッダー(宛先など)なら読めるが、添付ファイル名の場所にあるQエンコードだけは無視する」という、極めて偏った実装をしていたのです。

4. 結論:我々が取るべき自衛策

Outlook先輩が「1バイトでも削って効率化しよう(Qを使おう)」とした善意が、iPhoneの「そこはBase64かRFC2231しか読まないよ」という偏食によって仇となる。これがこの問題の正体です。

現状、現場のエンジニアができる唯一の対策は、「英数字が5の倍数に達する前に、全角文字を1文字追加して、OutlookにBase64を強制させる」という、泥臭いファイル名ハックのみです。

もし、あなたの周りでファイル名が化けて困っている人がいたら、そっと「全角文字をもう一文字増やしてみて」と伝えてあげてください。

そして、欧州のお友達には、逆に「非ASCII文字を添付ファイル名に使わないで!」と教えてあげてください。

コメント

このブログの人気の投稿

Chatの「メッセージは投稿者によって削除されました」を非表示にする方法

Chrome拡張機能を自作してやってみよう! ♪できるかな できるかな ・・・ 無理ぽ (´・ω・`) iframeの中に、実際のメッセージのやり取りが表示されるので、 $(function(){ $('iframe[name^="spareFrame"]').contents().find('[data-is-tombstoned="true"]').hide(); }); って書いたけど An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. って言われてダメだったよ・・・

cron で実行されたコマンドから出力されたメッセージをメールで送信する方法

本題に入る前に、まずは、sh/bash系のシェルで標準出力と標準エラー出力をリダイレクトする方法から。 現在使用中のシェルを確認するには、 # echo $SHELL とすれば確認できる。 その他、利用できるシェルを確認するには # cat /etc/shells とする。 ■リダイレクトについて commandコマンドが出力を伴うコマンドの場合、commandコマンドの出力をresult.txtへ出力するには # command > result.txt コマンドの実効結果を別のコマンドの入力値とする場合は、|(パイプ)でつなげる。 # command1 | command2 ■標準出力と標準エラー出力について ・標準出力 正常結果やコマンド実行途中に出力されるメッセージの出力先。 ・標準エラー出力 異常終了時のメッセージやエラーメッセージなど、ユーザーに気づいてほしいメッセージの出力先。 ■標準出力と標準エラー出力の両方をリダイレクトする 先のcommandコマンドのリダイレクト例のうち、result.txtへのリダイレクトは、標準出力をリダイレクトしている。そのため、標準エラー出力はリダイレクトされず、仮にcommandコマンドが標準エラー出力へメッセージを出力した場合は、result.txtではなくコンソールへ出力(表示)される。 標準出力と標準エラー出力の両方をリダイレクトして、result.txtへ出力するには、 # command > result.txt 2>&1 とする。 なお、上の例を省略なしで記述すると # command 1> result.txt 2>&1 となる。 この「1」「2」の番号について。 ・1:標準出力。通常はコンソール画面。 ・2:標準エラー出力。通常はコンソール画面。 となっている。 ちなみに、「0」は「標準入力」。通常はキーボードからの入力。 例:標準エラー出力を error.log へ出力する。標準出力はコンソールへ表示する。 # command 2> error.log 例:標準出力は result.log 、標準エラー出力は error.log へ...

cron で bash を使うまでのお話

おー、ほぼ一年ぶりの更新だ・・・ 普段、何気に設定していた cron なんですが、 「PATHは通っていないから、フルパス書いて」 「#!/bin/bash はお呪い」 っていう程度の認識しかなかった。 ので、一からお勉強。 まず、cron の シェル等を確認するには、cron実行ユーザーで [root@localhost ~]# crontab -e * * * * * printenv >/var/tmp/env.txt ってやって1分待つ。 で、1分後に出来上がったファイルの中身を見てみる [root@localhost ~]# cat /var/tmp/env.txt ... SHELL=/bin/sh USER=root PATH=/usr/bin:/bin PWD=/root LANG=ja_JP.UTF-8 SHLVL=1 HOME=/root LOGNAME=root XDG_RUNTIME_DIR=/run/user/0 ... あー、shだ。 どおりで、/bin/bash って書かないと、動かない記述があるわけだ。