【ITニュース解説】Putting Remote Email Images in a PDF From the Browser: CORS, Tracking Pixels, and Honest Failure Boxes
2026年10月30日に「Dev.to」が公開したITニュース「Putting Remote Email Images in a PDF From the Browser: CORS, Tracking Pixels, and Honest Failure Boxes」について初心者にもわかりやすく解説しています。
ITニュース概要
ブラウザでメールをPDF化する際、外部画像(ウェブ上の写真)の埋め込みは複雑だ。プライバシー保護(開封通知防止)やWebのセキュリティ規則(CORS)で、画像を安易に読み込めないからだ。そこで、プライバシーを守り、取得できなかった画像は「埋め込み不可」と理由を明記した代替表示をする仕組みを考案した。
ITニュース解説
システムエンジニアを目指す初心者の皆さんにとって、ウェブアプリケーション開発では、単に機能を実装するだけでなく、セキュリティやプライバシー、ユーザー体験といった多岐にわたる側面を考慮することが非常に重要である。ここでは、ブラウザ上で動作するメールビューアに「メールをPDFとして保存する」という機能を追加する際に直面した具体的な課題と、それを解決するための技術的なアプローチについて解説する。
この機能の基本的な目的は、ユーザーがブラウザで閲覧しているメールの内容をPDF形式で出力できるようにすることだ。メールのテキストやヘッダー情報、さらには添付ファイルなどは比較的容易にPDFに変換できる。しかし、メール本文中に含まれる画像、特に外部のウェブサーバーからリンクされている画像(例えば、HTMLの<img>タグで指定されるような画像)の取り扱いが、この機能開発における最大の難所であった。メールに直接埋め込まれている画像(例えば、cid:形式やdata:URI形式でメッセージ内に画像データが格納されているもの)は、そのデータ自体がすでに手元にあるため、PDFへの組み込みは比較的簡単だ。しかし、インターネット上のどこかに存在する画像を指し示すリンクの場合、状況は大きく異なる。
なぜ、外部の画像を単純にすべて取得してPDFに埋め込めないのだろうか。これには主に二つの大きな理由がある。一つは「プライバシー保護」に関わる問題、もう一つは「ウェブブラウザが持つセキュリティ上の制約」である。
まず、プライバシー保護の観点から考えると、リモート画像を読み込むこと自体が、ユーザーの行動を追跡する手段として使われる可能性がある。多くのニュースレターやプロモーションメールには、「トラッキングピクセル」と呼ばれる非常に小さな(例えば1ピクセル四方といった)画像が含まれていることがある。この画像を含むメールがユーザーによって開かれ、画像が読み込まれると、そのリクエストが送信者のサーバーに記録され、「いつ、どこで、誰がメールを開いたか」といった情報が相手に伝わってしまうのだ。ユーザーに対して「メールがあなたのデバイスから外に出ることはない」と約束するようなツールにとって、これは重大なプライバシー侵害につながりかねない。このため、開発者は非常に厳格なプライバシー保護ルールを設けた。具体的には、画像をリクエストする際に、開発者のサーバーを経由せず、ユーザーのブラウザから直接画像ホストへリクエストを送る方式を採用した。これにより、開発者側でユーザーの通信内容を監視することを防いでいる。さらに、画像リクエストにはユーザーの認証情報(ログイン情報を示すクッキーなど)や、そのリクエストがどのウェブページから来たかを示す参照元情報(referrerPolicy: "no-referrer")を含めない設定を適用し、追跡を困難にした。また、トラッキングピクセルの可能性が高いと判断される画像(例えば、幅や高さが2ピクセル以下の画像、またはCSSで表示されないように設定された画像)は、そもそもリクエスト自体を行わないようにしている。加えて、ユーザーのプライベートネットワークやローカルマシンへの不正なアクセスを防ぐため、そうしたIPアドレスへの画像リクエストは一切拒否する仕組みも導入されている。
二つ目の理由は、ウェブブラウザに備わるセキュリティ機能、特に「CORS(Cross-Origin Resource Sharing、クロスオリジンリソース共有)」の制約である。ウェブブラウザは、セキュリティのために、異なるオリジン(ウェブサイトのドメイン、プロトコル、ポート番号の組み合わせ)にあるリソースに、あるウェブページから直接アクセスするのを制限している。例えば、<img>タグを使えば、異なるオリジンにある画像でもウェブページに表示すること自体は可能だ。しかし、JavaScriptコードからその画像の中身(生の画像データ)を読み取ってPDFに埋め込もうとすると、話は別である。この場合、画像を提供しているサーバーが、その画像を他のオリジンからのアクセスに許可する特別なヘッダー(Access-Control-Allow-Origin)をHTTPレスポンスに含めて返す必要がある。もしこのヘッダーがなければ、ブラウザはセキュリティ上の理由からJavaScriptコードによる画像データの読み取りを拒否するのだ。開発者の調査によると、広く使われている画像ホストの中にはCORSヘッダーを送信しないものが少なくなく、結果として多くのニュースレター画像がブラウザから直接埋め込めない状況にあることが判明した。
このCORSの制約は、別の問題も引き起こす。CORSチェックに失敗した場合、ブラウザはアプリケーションコードに対して「一般的なネットワークエラー」として通知するため、「画像サーバーがダウンしている」のか「CORSが許可されていないためデータが取得できない」のか、あるいは「画像のURLが間違っている」のかといった具体的な原因を区別することができないのだ。これらはすべて、開発者にとっては同じようなエラーとして見えてしまう。
では、画像が取得できない場合にPDFには何を表示すれば良いだろうか。単純にその部分を空白にしてしまうと、PDFの見た目が不自然になり、まるでバグのように見えてしまう。そこで開発者は、ただの空白ではなく、理由を明確に示す「説明付きのボックス」を表示することを選択した。具体的には、PDF内の画像の代わりに灰色のボックスを表示し、「画像は含まれていません」というメッセージとともに、可能であればその画像の代替テキスト(HTMLのalt属性の値)と、画像が取得できなかった具体的な理由を表示する。区別できる理由としては、「ホストが他のウェブサイトからのダウンロードを許可しない、またはオフラインである(CORS失敗とネットワークエラーを区別できないため両方を記載)」「ホストがすでにこの画像を保持していない(HTTP 404などのエラー)」「返信された画像データが大きすぎる」「返信されたデータが画像ではない」「PDFに埋め込めないフォーマットである」「メール内に指定されたIDの画像が含まれていない」などがある。さらに、ユーザー体験を損なわないよう、同じ理由で取得できなかった隣接する画像は一つのボックスにまとめたり、明らかにトラッキングピクセルと思われる、サイズも代替テキストもない失敗画像は完全に省略したりする工夫も施されている。
実際に画像をフェッチする際には、以下のような詳細なルールとガードレールが設けられた。HTTPリクエストを送るfetchメソッドには、プライバシーを考慮した設定(mode: "cors", credentials: "omit", referrerPolicy: "no-referrer")に加えて、ネットワークの問題で処理が停止しないように10秒のタイムアウトを設定している。また、セキュリティと安定性のために、http:やプロトコル相対URLは自動的にhttps:にアップグレードされる。画像データのダウンロードは最大10MBに制限され、この制限はデータがストリーミングされている最中に適用されるため、巨大なデータが完全にダウンロードされてから破棄される無駄を防ぐ。さらに、サーバーからの応答が本当に画像データであるかを確認するため、HTTPヘッダーのContent-Typeだけでなく、データそのものの「マジックバイト」(ファイルの種類を識別するための先頭のバイト列)も検証する。これにより、サーバーが誤って、または意図的に画像ではないデータを返しても、それを適切に検出できる。一度に処理する画像の数を1メッセージあたり最大100枚、同時に実行されるリクエストを6件に制限することで、システムへの負荷を管理している。また、特定のホストがネットワークレベルでエラーを返した場合、同じWeb Worker内ではそのホストへの再リクエストを一定期間行わないようにして、無駄なタイムアウトやリソースの消費を防ぐ。頻繁に登場するロゴ画像などの重複ダウンロードを避けるため、512KBまでの画像はURLをキーとしてキャッシュする仕組みも導入されている。
複数のメール(例えばフォルダ内のすべてのニュースレター)を同時にPDF化する際には、並列処理の設計も重要となる。もし各メールを順番に処理し、その中に非常に遅い画像ホストへのリクエストが含まれていたら、全体の処理が大幅に遅延してしまう。このシステムでは、一度に4つのメールを並行してレンダリングすることで効率を高めている。しかし、単に並列処理するだけでなく、PDFの出力順序が元のメールの並び順と一致するように、処理の完了順序に関わらず、結果はインデックスに基づいて収集されるよう設計されている。これは、ユーザーにとって期待通りの出力順序を保証するために不可欠な工夫である。
この機能が意図した通りに動作するかを確認するためには、徹底したテストが不可欠である。アドレスルール(許可されるホストと拒否されるホスト)、リクエストオプション、各種類の失敗とその表示メッセージ、ストリーミング中のサイズ制限、ホストごとのリクエスト制御、そして同じ理由での失敗の統合など、多くの側面がユニットテストによって検証される。さらに、実際のブラウザ環境での動作を確認するため、CORSを許可するホストとCORSを拒否するホストに対して、エンドツーエンドのスクリプトを実行し、画像が正しく表示されるか、または正しい理由とともにボックスが表示されるか、そしてプライバシー要件(参照元情報なしでリクエストが送信されたこと)が満たされているかを確認している。
このような経験を通して得られた教訓は、将来的に同様のシステムを開発するシステムエンジニアを目指す皆さんにとって非常に有用である。まず、どのような機能を実装するにしても、「プライバシー」に関するルールを最初に明確に定義することである。この事例では、「プロキシを使わない」というルールが、画像取得失敗時のグレーボックス表示というデザイン選択に直接つながっている。次に、CORSエラーが発生した場合、ブラウザが具体的な原因を教えてくれない以上、「なぜ失敗したのか」を深追いするのではなく、「CORSの許可がないか、サーバーがオフラインであるか」といった可能性のある原因をまとめてユーザーに伝える方が適切である。また、トラッキングピクセルなどの問題のある画像は、リクエストを送信する前にフィルタリングすることが、プライバシー保護とリソース節約の両面で重要である。画像データのサイズ制限やコンテンツの検証は、データを受信し始める段階(ストリーミング中)で実施し、ヘッダー情報だけに頼らない堅牢なチェックを行うべきだ。最後に、複数の処理を並列で実行する場合、ユーザーが期待する出力順序が、個々の処理の完了順序に依存しないように設計することが、信頼性の高いシステム構築には不可欠である。