Python | Web / API:IMAP 読み取り

Python Python
スポンサーリンク

概要(IMAPは「受信箱から安全に読み取る」ための標準プロトコル)

IMAPは、メールサーバ上の受信箱(INBOX)からメールを検索・取得・既読化・移動・削除するためのしくみです。Python標準のimaplibとemailモジュールを組み合わせれば、未読だけ拾う、差出人や件名で絞る、本文と添付を取り出す、といった処理を短いコードで実現できます。重要なのは、暗号化接続(IMAP4_SSL)、正しいログイン情報(多くのサービスはアプリパスワードを推奨)、検索条件(UNSEEN/FROM/SINCEなど)の使い方、そしてMIME構造の理解による本文・添付の正しい取り出しです。


仕組みと準備(IMAPサーバ・暗号化・モジュール)

接続先と暗号化の基本

IMAPは通常、SSL/TLSで暗号化されたポート993へ接続します。ホスト名はサービスごとに異なり、Gmailなら imap.gmail.com、Microsoft 365なら outlook.office365.com などです。ユーザー名はメールアドレス、パスワードはサービス側の方針に従い、2段階認証とアプリパスワードの利用が推奨されることが多いです。

使うモジュール(imaplib+email)

受信・検索はimaplib、メッセージの解析(件名・本文・添付の取り出し)はemailモジュールを使います。email.message_from_bytesでパースし、multipartをwalkしてパートごとのMIMEタイプで本文・添付を区別します。


基本の読み取りフロー(接続→ログイン→検索→取得→解析)

最短の受信・本文表示(テキストメール想定)

import imaplib, email

IMAP_HOST = "imap.example.com"
USERNAME = "user@example.com"
PASSWORD = "app_password"  # 実運用は環境変数などで安全に管理

# 1) 暗号化接続
mail = imaplib.IMAP4_SSL(IMAP_HOST, 993)

# 2) ログイン
mail.login(USERNAME, PASSWORD)

# 3) 受信箱を選択(既定はINBOX)
mail.select("INBOX")  # readonlyにしたいなら mail.select("INBOX", readonly=True)

# 4) 検索(未読)
status, ids = mail.search(None, "UNSEEN")
msg_ids = ids[0].split()  # b'1 2 3' → [b'1', b'2', b'3']

# 5) 取得&解析(先頭1通)
if msg_ids:
    mid = msg_ids[0]
    status, data = mail.fetch(mid, "(RFC822)")  # メッセージ全体
    raw = data[0][1]                            # bytes
    msg = email.message_from_bytes(raw)

    print("From:", msg.get("From"))
    print("Subject:", msg.get("Subject"))
    # 本文(プレーンテキストを優先)
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_type() == "text/plain":
                print(part.get_payload(decode=True).decode(part.get_content_charset() or "utf-8"))
                break
    else:
        print(msg.get_payload(decode=True).decode(msg.get_content_charset() or "utf-8"))

mail.logout()
Python

この型で「未読の中から1通を取り出して本文を表示」できます。multipartは本文が複数パート(text/plainとtext/html)になるため、優先順位を決めて取り出します。


ヘッダー・本文の解析(文字コード・multipart・HTML本文)

ヘッダーの文字化け対策(decode_header)

from email.header import decode_header, make_header

def decode_str(s):
    return str(make_header(decode_header(s))) if s else ""

print("Subject:", decode_str(msg.get("Subject")))
print("From:", decode_str(msg.get("From")))
Python

件名や差出人はエンコードされていることが多く、decode_headerで確実に可読化します。

本文の取り出し方(text/plain優先、次にtext/html)

def extract_body(msg):
    if msg.is_multipart():
        # text/plainを優先。なければtext/html
        text_part = None
        html_part = None
        for part in msg.walk():
            ctype = part.get_content_type()
            if ctype == "text/plain" and not text_part:
                text_part = part
            elif ctype == "text/html" and not html_part:
                html_part = part
        target = text_part or html_part
        if target:
            charset = target.get_content_charset() or "utf-8"
            return target.get_payload(decode=True).decode(charset, errors="replace")
        return ""
    else:
        charset = msg.get_content_charset() or "utf-8"
        return msg.get_payload(decode=True).decode(charset, errors="replace")

body = extract_body(msg)
print(body[:500])
Python

メール本文はMIME構造次第。常にcharsetを取得してデコードし、欠けている場合はUTF-8に倒します。


検索条件の深掘り(UNSEEN・FROM・SUBJECT・SINCE)

代表的な検索キーと使い方

  • 未読だけ拾う: UNSEEN
  • 差出人を限定: FROM “sender@example.com”
  • 件名の一部を検索: SUBJECT “キーワード”
  • 日付で絞る: SINCE “1-Jan-2025″(RFC準拠の英語日付表現)
  • 複合条件: UNSEEN FROM “a@example.com”
# 例:未読かつ差出人指定
status, ids = mail.search(None, 'UNSEEN', 'FROM', '"sender@example.com"')

# 例:日付以降(英語表記)
status, ids = mail.search(None, 'SINCE', '1-Dec-2025')

# 例:件名にキーワード(部分一致)
status, ids = mail.search(None, 'SUBJECT', '"注文確認"')
Python

検索はIMAPサーバ側で実行されるため効率的です。日付はRFC準拠の英語表記が基本で、ローカル形式は使えません。


添付ファイルの保存とHTML整形(MIMEパートの扱い)

添付の取り出しと保存

from pathlib import Path

def save_attachments(msg, out_dir="attachments"):
    Path(out_dir).mkdir(parents=True, exist_ok=True)
    saved = []
    for part in msg.walk():
        if part.get_content_disposition() == "attachment":
            fname = part.get_filename()
            data = part.get_payload(decode=True)
            if fname and data:
                path = Path(out_dir) / fname
                path.write_bytes(data)
                saved.append(str(path))
    return saved

files = save_attachments(msg)
print("Saved:", files)
Python

attachment判定で安全に抽出。ファイル名はエンコードされていることがあるため、必要ならdecode_headerで可読化します。

HTML本文をテキスト化(必要に応じて)

# 最低限のタグ除去(簡易)
import re
html = extract_body(msg)  # text/htmlを拾った場合
text = re.sub(r"<[^>]+>", "", html)  # 本格対応はBeautifulSoup推奨
print(text[:300])
Python

整形が必要ならBeautifulSoupでのパースを検討します(HTMLメールは装飾が多いため)。


既読・削除・移動・UID(状態操作の基礎)

既読フラグを付ける・外す

mid = msg_ids[0]
# 既読化(\Seen を追加)
mail.store(mid, "+FLAGS", r"(\Seen)")
# 未読へ戻す(\Seen を除去)
mail.store(mid, "-FLAGS", r"(\Seen)")
Python

フラグ操作で既読・未読を制御できます。readonly=Trueでselectしている場合は変更不可です。

削除と実削除(expunge)

# 削除フラグ付与
mail.store(mid, "+FLAGS", r"(\Deleted)")
# 実削除
mail.expunge()
Python

IMAPは削除フラグ→expungeで物理削除。安全のため、まずゴミ箱(Trash)へ移動する運用もあります。

別フォルダへ移動(コピー→削除が一般的)

# 例:INBOXからArchiveへ移動
mail.copy(mid, "Archive")
mail.store(mid, "+FLAGS", r"(\Deleted)")
mail.expunge()
Python

一部のサーバでは専用MOVEコマンドが使えることもありますが、コピー+削除が標準的です。

UIDで安定参照(message番号の変動対策)

# UIDベースの検索・取得(サーバ拡張によりサポート)
status, ids = mail.uid("search", None, "UNSEEN")
uid = ids[0].split()[0]
status, data = mail.uid("fetch", uid, "(RFC822)")
Python

UIDはメッセージの安定識別子。番号の変動に強く、繰り返し処理で安全です。


エラー対策と安全運用(認証・タイムアウト・再試行)

代表的な失敗への備え

  • 認証失敗: 対策: メールサービスのアプリパスワード利用、2段階認証設定の確認。
  • 接続失敗: 対策: ホスト名とポート(993)を確認、ネットワーク・ファイアウォールの例外設定。
  • タイムアウト: 対策: socket.setdefaulttimeoutで全体のデフォルトタイムアウトを設定し、長すぎる待ちを避ける。
  • 文字化け: 対策: ヘッダーはdecode_header、本文はget_content_charsetの利用とUTF-8へのフォールバック。
import socket
socket.setdefaulttimeout(20)  # 全てのソケット操作にデフォルトタイムアウト
Python

実務の安全設計

  • 認証情報の保護: 対策: 環境変数や秘密管理を利用し、コードに平文で書かない。
  • 過剰取得回避: 対策: 検索で絞り、取得は必要件数(先頭N件)に限定。
  • 再試行の設計: 対策: 一時的な接続失敗は短いバックオフで再試行。ただし削除・移動など操作系は重複に注意。

例題で身につける(定番から実務まで)

例題1:未読の差出人フィルタ+本文取り出し

import imaplib, email
from email.header import decode_header, make_header

def decode_str(s): return str(make_header(decode_header(s))) if s else ""

mail = imaplib.IMAP4_SSL("imap.example.com", 993)
mail.login("user@example.com", "app_password")
mail.select("INBOX")

status, ids = mail.search(None, 'UNSEEN', 'FROM', '"sender@example.com"')
for mid in ids[0].split()[:5]:
    status, data = mail.fetch(mid, "(RFC822)")
    msg = email.message_from_bytes(data[0][1])
    print("Subject:", decode_str(msg.get("Subject")))
    print("From:", decode_str(msg.get("From")))
    # 本文(テキスト優先)
    body = ""
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_type() == "text/plain":
                body = part.get_payload(decode=True).decode(part.get_content_charset() or "utf-8")
                break
    else:
        body = msg.get_payload(decode=True).decode(msg.get_content_charset() or "utf-8")
    print(body[:300], "\n---")
mail.logout()
Python

例題2:SINCEで日付以降+添付を保存

import imaplib, email
from email.header import decode_header
from pathlib import Path

mail = imaplib.IMAP4_SSL("imap.example.com", 993)
mail.login("user@example.com", "app_password")
mail.select("INBOX")

status, ids = mail.search(None, 'SINCE', '1-Dec-2025')
Path("attachments").mkdir(exist_ok=True)

for mid in ids[0].split()[:3]:
    status, data = mail.fetch(mid, "(RFC822)")
    msg = email.message_from_bytes(data[0][1])

    for part in msg.walk():
        if part.get_content_disposition() == "attachment":
            fname = part.get_filename()
            if fname:
                Path("attachments", fname).write_bytes(part.get_payload(decode=True))
mail.logout()
Python

例題3:既読化・移動・削除(安全な順序)

import imaplib

mail = imaplib.IMAP4_SSL("imap.example.com", 993)
mail.login("user@example.com", "app_password")
mail.select("INBOX")

status, ids = mail.search(None, 'UNSEEN')
for mid in ids[0].split()[:2]:
    mail.store(mid, "+FLAGS", r"(\Seen)")    # 既読化
    mail.copy(mid, "Archive")                # アーカイブへコピー
    mail.store(mid, "+FLAGS", r"(\Deleted)") # 削除フラグ
mail.expunge()  # まとめて実削除
mail.logout()
Python

例題4:UIDベースで未読本文を安定取得

import imaplib, email

mail = imaplib.IMAP4_SSL("imap.example.com", 993)
mail.login("user@example.com", "app_password")
mail.select("INBOX")

status, ids = mail.uid("search", None, "UNSEEN")
for uid in ids[0].split()[:5]:
    status, data = mail.uid("fetch", uid, "(RFC822)")
    msg = email.message_from_bytes(data[0][1])
    print(msg.get("Subject"))
mail.logout()
Python

まとめ

IMAP読み取りは「暗号化接続→ログイン→フォルダ選択→検索→取得→MIME解析」が基本線です。検索キー(UNSEEN/FROM/SUBJECT/SINCE)でサーバ側絞り込みを徹底し、emailモジュールでヘッダーのデコードと本文・添付の取り出しを正しく行う。既読・削除・移動はフラグ操作とexpungeの理解が要点で、安定参照にはUIDが有効です。認証はアプリパスワードを含むサービス側の推奨に従い、タイムアウトと例外対策を最初から入れる。これらの型を身につければ、初心者でも短いコードで「安全・確実・再現性高い」メール読み取りができます。

タイトルとURLをコピーしました