目次
概要
Pythonの email モジュールを使用して、受信したメール(EmailMessage オブジェクト)から添付ファイルを抽出し、ローカルディスクに保存する方法を解説します。
画像やPDFなどのバイナリファイルだけでなく、テキスト形式の添付ファイルが自動デコードされた場合の再エンコード処理についても触れます。
仕様(入出力)
- 入力:
email.message.EmailMessageオブジェクト- (添付ファイルを含んでいる前提)
- 出力:
- ディスクへのファイル保存
- 前提:
policy.defaultを使用してパースされたメールオブジェクトであること。
基本の使い方
メールオブジェクトから添付ファイル部分のみをループで回し、ファイル名と中身を取り出して保存する基本的な実装です。
# msg は EmailMessageオブジェクト
for part in msg.iter_attachments():
# ファイル名を取得
filename = part.get_filename()
if filename:
# 中身を取得(画像ならbytes、テキストならstrで返る場合がある)
content = part.get_content()
# 保存(バイナリモード)
with open(filename, "wb") as f:
if isinstance(content, str):
# 文字列ならエンコードして書き込み
charset = part.get_content_charset() or "utf-8"
f.write(content.encode(charset))
else:
# バイナリならそのまま書き込み
f.write(content)
コード全文
擬似的な「添付ファイル付きメールデータ」を作成し、それを解析して添付ファイルを保存するまでの完全なシミュレーションコードです。
import os
from email import message_from_bytes, policy
from email.message import EmailMessage
def extract_attachments_demo():
"""
EmailMessageオブジェクトから添付ファイルを抽出・保存するデモ関数。
"""
# --- 1. テストデータの準備 (添付付きメールを擬似作成) ---
# 通常は imaplib 等で受信したデータを使用します
raw_msg = EmailMessage()
raw_msg["Subject"] = "Test Email"
raw_msg.set_content("添付ファイルを確認してください。")
# 擬似添付ファイル1: テキストファイル
raw_msg.add_attachment(
"これは添付テキストです".encode("utf-8"),
maintype="text", subtype="plain", filename="note.txt"
)
# 擬似添付ファイル2: バイナリファイル(例として中身は適当なバイト列)
raw_msg.add_attachment(
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR...',
maintype="image", subtype="png", filename="image.png"
)
# バイト列に変換(受信データシミュレーション)
email_bytes = raw_msg.as_bytes()
# --- 2. 解析と抽出の実装 ---
# EmailMessageオブジェクトへの変換
msg = message_from_bytes(email_bytes, policy=policy.default)
print(f"件名: {msg['Subject']}")
# 保存用ディレクトリ
save_dir = "downloaded_attachments"
os.makedirs(save_dir, exist_ok=True)
# 添付ファイルのループ処理 (iter_attachments)
# 本文以外のパート(添付ファイル)を順番に取得します
for part in msg.iter_attachments():
filename = part.get_filename()
if not filename:
print("ファイル名がないパートをスキップしました。")
continue
print(f"添付ファイルを発見: {filename}")
# コンテンツの取得
# get_content() は、Content-Typeに従って自動的に
# bytes(画像等) または str(テキスト) に変換して返します
content = part.get_content()
save_path = os.path.join(save_dir, filename)
try:
with open(save_path, "wb") as f:
if isinstance(content, str):
# str型で返ってきた場合は、元のエンコーディングに戻して保存
charset = part.get_content_charset() or 'utf-8'
print(f" - テキストデータを {charset} でエンコードして保存")
f.write(content.encode(charset))
else:
# bytes型ならそのまま書き込み
print(" - バイナリデータを保存")
f.write(content)
print(f" -> 保存完了: {save_path}")
except Exception as e:
print(f" -> 保存エラー: {e}")
if __name__ == "__main__":
extract_attachments_demo()
カスタムポイント
EmailMessage オブジェクトにおける添付ファイル操作の主要メソッドです。
| メソッド・構文 | 処理内容 | 戻り値の型 |
msg.iter_attachments() | メール内の「添付ファイル」として扱われるパートを順番に取り出すイテレータを返します。 | generator (EmailMessage parts) |
part.get_filename() | そのパートのファイル名を取得します。デコード済みの文字列が返ります。ファイル名がない場合は None。 | str or None |
part.get_content() | そのパートのデータ中身を取得します。画像ならバイト列、テキストなら(policy.default の場合)文字列に変換されます。 | bytes or str |
part.get_content_charset() | テキストパートの場合、指定されている文字コード(utf-8 等)を取得します。 | str or None |
注意点
- テキスト添付の罠
get_content()は親切設計になっており、添付ファイルがtext/plainやtext/csvの場合、勝手にデコードして Python のstr型にして返してくることがあります。- ファイルとして保存する際は
open(..., "wb")(バイナリ書き込み)を使うのが一般的ですが、strをwbモードで書き込むとエラーになります。 - そのため、コード例のように
isinstance(content, str)で型判定を行い、必要に応じて.encode()する処理が必須です。
- ファイル名の安全性
get_filename()で取得したファイル名に../などのパス操作文字が含まれていると、意図しない場所に保存されるセキュリティリスク(ディレクトリトラバーサル)があります。os.path.basename()を通すなどして、ファイル名のみを抽出する対策を推奨します。
- 同名ファイルの上書き
- 添付ファイル名が固定のメールを複数受信した場合、単純に保存すると上書きされます。タイムスタンプを付与するなどの対策が必要です。
応用
特定の拡張子(例えばPDFとPNG)だけを選んで保存するフィルタリングの実装です。
import os
def save_specific_attachments(msg, allowed_extensions={".pdf", ".png"}):
for part in msg.iter_attachments():
filename = part.get_filename()
if not filename:
continue
# 拡張子の抽出とチェック
_, ext = os.path.splitext(filename)
if ext.lower() not in allowed_extensions:
print(f"スキップ: {filename}")
continue
# 保存処理 (簡略化)
content = part.get_content()
with open(filename, "wb") as f:
if isinstance(content, str):
f.write(content.encode('utf-8'))
else:
f.write(content)
print(f"保存: {filename}")
まとめ
msg.iter_attachments() を使うことで、マルチパートメールの構造を解析する手間なく、添付ファイルだけにアクセスできます。
ただし、get_content() の戻り値が str になるか bytes になるかは Content-Type 次第であるため、保存時には必ず型チェックを行うのがバグを防ぐポイントです。
