【Python】EmailMessageオブジェクトから添付ファイルを取り出して保存する

目次

概要

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()そのパートのファイル名を取得します。デコード済みの文字列が返ります。ファイル名がない場合は Nonestr or None
part.get_content()そのパートのデータ中身を取得します。画像ならバイト列、テキストなら(policy.default の場合)文字列に変換されます。bytes or str
part.get_content_charset()テキストパートの場合、指定されている文字コード(utf-8 等)を取得します。str or None

注意点

  1. テキスト添付の罠
    • get_content() は親切設計になっており、添付ファイルが text/plaintext/csv の場合、勝手にデコードして Python の str 型にして返してくることがあります。
    • ファイルとして保存する際は open(..., "wb") (バイナリ書き込み)を使うのが一般的ですが、strwb モードで書き込むとエラーになります。
    • そのため、コード例のように isinstance(content, str) で型判定を行い、必要に応じて .encode() する処理が必須です。
  2. ファイル名の安全性
    • get_filename() で取得したファイル名に ../ などのパス操作文字が含まれていると、意図しない場所に保存されるセキュリティリスク(ディレクトリトラバーサル)があります。
    • os.path.basename() を通すなどして、ファイル名のみを抽出する対策を推奨します。
  3. 同名ファイルの上書き
    • 添付ファイル名が固定のメールを複数受信した場合、単純に保存すると上書きされます。タイムスタンプを付与するなどの対策が必要です。

応用

特定の拡張子(例えば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 次第であるため、保存時には必ず型チェックを行うのがバグを防ぐポイントです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次