![[Python] GmailでHTMLメールの画像が表示されない問題と解決法【Python実装】](https://humanxai.info/images/uploads/python-gmail-html-img.webp)
はじめに
学生時代、Perlで作成した自動化スクリプトを10年ぐらい前にPythonへ移植しFreeBSDやLinux上で動作させていましたが、 2年ほど放置していた間にGmailの仕様が変わったのか、画像が
「PCでは見えるのにiPhoneでだけ表示されない」
問題を解決したので、備忘録情報をまとめてみました。
1. 症状まとめ
HTMLメールに商品画像を埋め込んで送信したところ、以下のような不思議な挙動が発生しました。
-
表示されるときと表示されないときがある PCブラウザ版Gmailでは普通に表示されるのに、iPhoneのGmailアプリでは画像が見えないケースが頻発。
-
本文ではなく「添付ファイル」として扱われることがある 本来は本文内に表示させたいのに、メール末尾に「添付」としてだけ出てしまう。
-
本文に
<img>
タグがそのまま文字列で表示されることがあるtext/plain
パートにHTMLタグが混ざってしまい、そのまま出力されてしまった。 -
転送すると画像が突然表示される 送信直後の受信メールでは画像が出なかったのに、「転送」画面を開くと画像が正しくプレビューされる。 → Gmailが転送用に本文を再レンダリングし、Googleの画像プロキシを通したためと考えられる。
-
「自動サイズ変更を元に戻す」と出ることがある iOS Gmailでは幅指定がない画像を勝手に縮小表示し、「自動サイズ変更を元に戻す」ボタンで本来サイズに戻る動作をする。
2. 調査でわかったこと
-
GmailはPCとiOSで挙動が違う PC版のGmail(Webブラウザ)はほぼ常に画像を表示できるのに対し、iOS版のGmailアプリでは表示されないケースが多い。 → メールのレンダリングエンジンが別で、挙動も異なる。
-
外部画像の取得方法が混在している PCでは Google の画像プロキシ経由で取得するため、表示できず。 一方、iOS Gmailは状況によって端末から直接取得しに行くため、ホットリンク制限やUA/Refererチェックでブロックされて表示されない。
-
text/plain
に HTMLタグが残ると「タグ文字列」として出てしまう 本来はプレーンテキスト用にタグを除去する必要があるが、その処理をしないと<img src=...>
が文字列としてそのまま表示される。 -
width
指定がないと iOS Gmail が勝手に縮小プレビューする 画像サイズがバラバラな場合、スマホ版Gmailが自動で小さく表示してしまい「自動サイズを元に戻す」ボタンが出る。 →<img>
にwidth
を明示し、height:auto
を指定することで安定して本文に表示できた。
3. 解決のポイント
試行錯誤の末、以下の実装ルールに揃えることで、iOS Gmailでも安定して画像が表示されるようになりました。
-
画像は CID インライン添付(
multipart/related
) 外部URLをそのまま埋め込むのではなく、送信時に画像を取得してインライン添付し、<img src="cid:...">
として本文に埋め込む。 → これで外部サーバのホットリンク規制や取得失敗の影響を受けなくなる。 -
<img>
タグはサイズを明示width
を指定し、height:auto
+max-width:100%
を組み合わせることで、PCでもスマホでもレイアウトが崩れにくくなった。 -
余計な属性は削除
loading="lazy"
やclass="..."
は Gmail のHTMLサニタイザに引っかかりやすく、表示が不安定になるため削除。 -
text/plain
は素のテキストだけにする タグを除去して本文テキストのみを残す。これを忘れると、プレーン部分に<img>
タグがそのまま出力され「タグ文字列」として見えてしまう。 -
画像取得失敗時は外部URLのままにフォールバック 添付が一枚もできなかった場合は HTML を書き換えずに送信し、少なくともPCのGmailで表示されるようにする。
-
件名はユニーク化してスレッドキャッシュ回避 Gmailは同一件名のメールを同じスレッドでまとめるため、過去の「画像が表示されない状態」をキャッシュして引きずることがある。 件名にタイムスタンプやシリアル番号を付けて毎回ユニーク化することで、キャッシュの影響を防げる。
4. 最終的なPythonコード(抜粋)
以下は実際に安定して画像を本文に埋め込めた最小構成の例です。
ポイントは multipart/related
をルートにして、その中に multipart/alternative
(plain+html)を入れ、画像を CID 添付することです。
環境
- OS: Debian GNU/Linux 12 (Kernel 6.1.0-37-amd64)
- Python: 3.11.2
- 使用ライブラリ: 標準ライブラリ (smtplib, email.mime) のみ
- SMTPサーバ: ISP提供の smtp (ポート465, SSL)
- 確認クライアント:
- Gmail (PC / Chrome ブラウザ版)
- Gmail (iOSアプリ)
import ssl, smtplib, re, urllib.request
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
SMTP_HOST = "---"
SMTP_PORT = 465
SMTP_USER = "---"
SMTP_PASS = "---"
# プレーンテキスト生成(タグを削除)
def make_plain(html: str) -> str:
text = re.sub(r'<br\s*/?>', '\n', html, flags=re.I)
text = re.sub(r'<[^>]+>', '', text)
return text.strip()
# 外部URLから画像を取得
def fetch_image(url: str):
req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=10) as r:
data = r.read()
ctype = r.info().get_content_type() or "image/jpeg"
return data, ctype.split("/")[-1]
def sendmail(From, To, subject, html):
# ルート multipart/related
root = MIMEMultipart('related')
root["From"] = From
root["To"] = To
root["Subject"] = subject
# multipart/alternative(plain + html)
alt = MIMEMultipart('alternative')
alt.attach(MIMEText(make_plain(html), 'plain', 'utf-8'))
alt.attach(MIMEText(html, 'html', 'utf-8'))
root.attach(alt)
# <img src="..."> を cid に置換して添付
pattern = re.compile(r'<img\s+[^>]*src=["\']([^"\']+)["\']', re.I)
cid_idx = 1
def repl(m):
nonlocal cid_idx
url = m.group(1)
try:
data, subtype = fetch_image(url)
except Exception:
return m.group(0) # 取得失敗→そのまま
cid = f"img{cid_idx}"
img = MIMEImage(data, _subtype=subtype, name=f"{cid}.{subtype}")
img.add_header('Content-ID', f'<{cid}>')
img.add_header('Content-Disposition', 'inline', filename=f'{cid}.{subtype}')
img.add_header('X-Attachment-Id', cid) # iOS Gmail対策
root.attach(img)
cid_idx += 1
return m.group(0).replace(url, f"cid:{cid}")
new_html = pattern.sub(repl, html)
# htmlパートを差し替え(添付できたときのみ)
if cid_idx > 1:
alt.get_payload()[-1] = MIMEText(new_html, 'html', 'utf-8')
# 送信
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=ssl.create_default_context()) as s:
s.login(SMTP_USER, SMTP_PASS)
s.sendmail(From, [To], root.as_string())
コードのポイント
- ルートは multipart/related
- 本文は multipart/alternative(text/plain と text/html を両方入れる)
- 画像は Content-ID を付けて添付し、本文から img src=“cid:…” で参照
- plain 部分にはタグを残さない → 「タグ文字列が本文に出る問題」を回避
- width + height:auto + max-width:100% を img に指定して、スマホでも安定表示
5. まとめ
-
Webのレスポンシブ知識だけでは解決しない ブラウザなら動くコードでも、メールクライアント(特にiOS Gmail)は独自の制限やサニタイザが働くため、そのままでは表示されないことが多い。
-
「HTMLメール=独自の古いブラウザ」と思って作る 余計な属性やCSSは極力避け、シンプルに
<table>
ベースのレイアウト+<img>
の width/height 明示が鉄則。 最新のWeb技術(loading="lazy"
や複雑なclass指定)は逆に不具合を生む。
💬 コメント