Python標準ライブラリでiCloudメールを送受信

Category: Tech
Tag: Python
thumbnail

Pythonの標準ライブラリのみを使ってメールの送受信をしてみました。
他人に送信するのはもちろん、自分に送信すればエラーなどの自動通知システムとして利用することもできます。(自分はこれが目的)
今回使うメーラーはiCloudメールですが、パスワードを用意すればGmailなどでも動作するはずです。

Tip

前提知識:
メールの送信にはSMTPというプロトコルが使われます。
ついさっき知ったのですが、SMTPはメールサーバー同士だけでなく、クライアントからサーバーへの送信にも使われます。
そして、SMTPは送信専用でサーバーからクライアントへの受信にはIMAPという別のプロトコルが使われます。

アプリパスワードの生成

メール送信にはパスワードが必要です。このパスワードはApple Accountのパスワードとは別のもので、事前に作成する必要があります。
https://account.apple.com にアクセスしてサインインすると、下の方にアプリ用パスワードという項目があります。
以降はここで生成したパスワードを使います。

送信

emailに関連するデータを扱うemailパッケージとSMTPを扱うsmtplibを使います。いずれも標準ライブラリなのでインストール不要です。
開いたSMTPセッションは最後にQUITコマンドで閉じる必要がありますが、Pythonのwith構文を使うことでスコープを抜けた際に自動で処理されます。

import smtplib
from email.message import EmailMessage

SMTP_SERVER = "smtp.mail.me.com"
SMTP_PORT   = 587
ADDR_FROM   = "[email protected]"
ADDR_TO     = "[email protected]"
PASSWORD    = "xxxx-xxxx-xxxx-xxxx"

SUBJECT = "Message from Python"
MESSAGE = "hello, world!"

message = EmailMessage()
message.set_content(MESSAGE)
message["Subject"] = SUBJECT
message["From"] = ADDR_FROM
message["To"]   = ADDR_TO

with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp:
    smtp.starttls()
    smtp.login(ADDR_FROM, PASSWORD)
    smtp.send_message(message)

Tip

SMTPサーバーのホストはGmail:smtp.gmail.com、Outlook:smtp-mail.outlook.comとなります。
ちなみにiCloudメールのSMTPサーバーがsmtp.mail.me.comなのは、iCloudの前身であるMobileMeでme.comが使われていた名残だそうです。

html送信

プレーンテキストではなく、htmlを送信する場合はset_contentの引数にsubtype='html'を渡します。

message.set_content("<h1>hello, world!</h1>", subtype='html')

受信

先述の通り、SMTPは送信専用で受信はIMAPなので、こちらはimaplibを使います。
smtplib同様、Python標準ライブラリなのでインストール不要です。

IMAPはコマンドでやり取りするスタイルで、imaplibではそれぞれのコマンドに対応した関数が用意されています。
そのため、使いこなすにはIMAPコマンドの使い方を調べることをおすすめします。

受信サンプルコード

最新メッセージ取得

最新の5件のメッセージを取得して、新しい順に表示します。

import imaplib
import email
from email import policy

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "[email protected]"
PASSWORD    = "xxxx-xxxx-xxxx-xxxx"


imap = imaplib.IMAP4_SSL(IMAP_SERVER)
imap.login(MAIL_ADDR, PASSWORD)

message_total = int(imap.select("INBOX")[1][0])  # メールボックスを選択してメッセージの総数を取得

for i in range(5):
    status, messages = imap.fetch(str(message_total-i), "(BODY.PEEK[])") # 本文を既読フラグを立てずに全て取得
    message = email.message_from_bytes(messages[0][1], policy=policy.default)

    print(f"Date:   {message.get('Date')}")
    print(f"From:   {message.get('From')}")
    print(f"To:     {message.get('To')}")
    print(f"Subject:{message.get('Subject')}")
    print(f"Message:{message.get_body(('plain','html')).get_content()}")
    print("--------------------------------------------------------------------------------\n")

imap.close()
imap.logout()

未読メッセージ取得

未読メッセージを取得します

import imaplib
import email
from email import policy

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "[email protected]"
PASSWORD    = "xxxx-xxxx-xxxx-xxxx"


imap = imaplib.IMAP4_SSL(IMAP_SERVER)
imap.login(MAIL_ADDR, PASSWORD)

imap.select("INBOX")

status, data = imap.search(None, "UNSEEN")

if data[0]==None:
    print("no result")
else:
    unseen_messages = data[0].decode().split()

    for message_id in unseen_messages:
        status, messages = imap.fetch(message_id, "(BODY.PEEK[])")
        message = email.message_from_bytes(messages[0][1], policy=policy.default)
        
        print("---------------------------------")
        print(f"Date:   {message.get('Date')}") 
        print(f"From:   {message.get('From')}")
        print(f"To:     {message.get('To')}")
        print(f"Subject:{message.get('Subject')}")

imap.close()
imap.logout()

SELECTコマンド

メールボックスを選択します。
成功するとメッセージの総数が返されます。

>>> print(imap.select("INBOX"))
('OK', [b'87'])

FETCHコマンド

指定したメッセージのデータを取得します。

メッセージ番号は1,3,7として複数指定したり、5:10として範囲指定することもできます。ただ、返されるメッセージの順番はRFCで規定されてないようで、指定順に関わらずメッセージ番号の昇順で返されることが多いようです。

Note

メッセージ番号とUID
多くのコマンドで帰ってくるメッセージ番号はただの連番で、メールの削除などによって変化します。
一方UIDはメールボックス内で固有で、新しいメッセージに対し昇順で割り当てられます。

オプションによって取得するデータを指定することができ、スペースで区切れば複数のデータを指定することもできます。
オプションの例
BODY[]: 本文を取得([ ]内はmultipartの番号、空なら全て取得)
BODY.PEEK[]: BODY[]と同じですが、既読にしません
FLAGS: フラグを取得
UID: メッセージ固有のIDを取得

imaplibではfetch関数の第一引数にメッセージ番号を、第二引数にオプションを渡します。
オプションは丸括弧で囲った文字列として渡します。

imap.fetch("1,5", "(BODY[] FLAGS)")
# 1と5番のメッセージの本文とフラグを取得

SEARCHコマンド

メッセージを検索するコマンドです。
検索条件には以下のようなものがあります。
ALL: 全てのメッセージ
UNSEEN: 未読メッセージ
TEXT "検索文字列": 件名、本文、送信元など、全体を検索
SUBJECT "検索文字列": 件名で検索
FROM "送信元": 送信元で検索

スペースで区切るとAND検索、OR検索は(OR TEXT "hello" TEXT "world")のように記述します。

imaplibではsearchメソッドの第一引数にcharset,第二引数に検索クエリを渡します。

>>> imap.search(None, '(UNSEEN FROM "[email protected]")')
('OK', [b'77 8 6 50 86 75 88'])

日本語で検索する場合はutf-8でエンコードします。

imap.search("UTF-8", '(SUBJECT "こんにちは")'.encode('utf-8'))

multipart

メールにはmultipartという仕組みがあり、ファイルの添付(multipart/mixed)やhtmlとplain-textの複数形式の送信(multipart/alternative)などに使われます。

multipartはネストが可能なため、本来であれば再帰関数で処理する必要がありますが、email.messageクラスではwalk()メソッドでpartのリスト(正確にはgenerator)を取得することができます。

また、EmailMessage.get_body()関数で必要とするContent-Typeのpartを取得することもできます。該当するものが無い時はNoneが返ります。

multipartを表示するサンプルプログラム emailパッケージにはmultipartの構造を表示する`_structure()`というデバッグ関数があります。
import imaplib
import email
from email import policy
from email.iterators import _structure

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "[email protected]"
PASSWORD    = "xxxx-xxxx-xxxx-xxxx"


imap = imaplib.IMAP4_SSL(IMAP_SERVER)
imap.login(MAIL_ADDR, PASSWORD)

message_total = int(imap.select("INBOX")[1][0])

for i in range(5):
    status, messages = imap.fetch(str(message_total-i), "(BODY.PEEK[])")
    message = email.message_from_bytes(messages[0][1], policy=policy.default)

    print("\n------------------------------\n")
    print(f"Date:   {message.get('Date')}")
    print(f"From:   {message.get('From')}")
    print(f"To:     {message.get('To')}")
    print(f"Subject:{message.get('Subject')}")
    print("")
    iterators._structure(message)

imap.close()
imap.logout()
Date:   Thu, 02 Jul 2026 17:18:23 +0900
From:   moris <[email protected]>
To:     [email protected]
Subject:pdfファイル添付テスト

multipart/mixed
    multipart/alternative
        text/plain
        text/html
    application/pdf

--------------------------

Date:   Mon, 29 Jun 2026 23:06:20 +0000
From:   [email protected]
To:     [email protected]
Subject:iCloud メールテスト

text/html

--------------------------

Date:   Mon, 29 Jun 2026 08:56:46 +0000
From:   Google <[email protected]>
To:     [email protected]
Subject:[email protected] のセキュリティ通知

multipart/alternative
    text/plain
    text/html

--------------------------

Date:   Sun, 28 Jun 2026 13:22:36 +0000
From:   Cloudflare Registrar <[email protected]>
To:     [email protected]
Subject:30 Day Domain Renewal Notice

multipart/alternative
    text/plain
    text/html

メッセージ削除

メッセージをゴミ箱に入れるには\Deletedフラグを追加します。
IMAPでフラグの操作にはSTOREコマンドを使います。

Googleのログイン通知をゴミ箱に入れるサンプル

import imaplib

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "[email protected]"
PASSWORD    = "xxxx-xxxx-xxxx-xxxx"


imap = imaplib.IMAP4_SSL(IMAP_SERVER)
imap.login(MAIL_ADDR, PASSWORD)

imap.select("INBOX")
status, message_ids = imap.search("UTF-8", '(FROM "[email protected]" SUBJECT "セキュリティ通知")'.encode('utf-8'))

if message_ids[0]==None:
    print("no result")
else:
    message_ids = message_ids[0].split()
    for message_id in message_ids:
        imap.store(message_id, "+FLAGS", "\\Deleted")
        print(f"{message_id} +FLAG Deleted")

imap.close()
imap.logout()

メールボックス一覧を表示

import imaplib

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "[email protected]"
PASSWORD    = "xxxx-xxxx-xxxx-xxxx"
 
imap = imaplib.IMAP4_SSL(IMAP_SERVER)
imap.login(MAIL_ADDR, PASSWORD)

status, boxes = imap.list()
for box in boxes:
    print(box.decode())

imap.logout()
() "/" "Archive"
() "/" "Junk"
(\Noinferiors) "/" "INBOX"
(\Trash) "/" "Deleted Messages"
(\Sent) "/" "Sent Messages"
() "/" "Drafts"

参考ページ

Python email.message: Representing an email message
https://docs.python.org/3/library/email.message.html

Python smtplib — SMTP protocol client
https://docs.python.org/3/library/smtplib.html

Python imaplib — IMAP4 protocol client
https://docs.python.org/3/library/imaplib.html

ITmedia IMAP4のSEARCH条件の一覧
https://atmarkit.itmedia.co.jp/fnetwork/rensai/netpro09/imap4-searchoption.html

ITmedia IMAP4のFETCHオプションの一覧
https://atmarkit.itmedia.co.jp/fnetwork/rensai/netpro09/imap4-fetch.html