---
title: Python標準ライブラリでiCloudメールを送受信
description: 送信は簡単、受信は地獄です
thumbnail: 
emoji: ✉️
date: 2026-07-07
category: Tech
tags: [Python]
index: true
published: true
---



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

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


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


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

```py
import smtplib
from email.message import EmailMessage

SMTP_SERVER = "smtp.mail.me.com"
SMTP_PORT   = 587
ADDR_FROM   = "example@icloud.com"
ADDR_TO     = "example@gmail.com"
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'`を渡します。
```py
message.set_content("<h1>hello, world!</h1>", subtype='html')
```



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

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


### 受信サンプルコード
#### 最新メッセージ取得
最新の5件のメッセージを取得して、新しい順に表示します。
```py
import imaplib
import email
from email import policy

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "example@icloud.com"
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()
```

#### 未読メッセージ取得
未読メッセージを取得します
```py
import imaplib
import email
from email import policy

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "example@icloud.com"
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関数の第一引数にメッセージ番号を、第二引数にオプションを渡します。
オプションは丸括弧で囲った文字列として渡します。
```py
imap.fetch("1,5", "(BODY[] FLAGS)")
# 1と5番のメッセージの本文とフラグを取得
```


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

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

imaplibではsearchメソッドの第一引数にcharset,第二引数に検索クエリを渡します。
```py
>>> imap.search(None, '(UNSEEN FROM "example@gmail.com")')
('OK', [b'77 8 6 50 86 75 88'])
```

日本語で検索する場合はutf-8でエンコードします。
```py
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が返ります。

<details>
<summary> multipartを表示するサンプルプログラム </summary>
emailパッケージにはmultipartの構造を表示する`_structure()`というデバッグ関数があります。

```py
import imaplib
import email
from email import policy
from email.iterators import _structure

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "example@icloud.com"
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 <xxxx@gmail.com>
To:     xxxx@icloud.com
Subject:pdfファイル添付テスト

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

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

Date:   Mon, 29 Jun 2026 23:06:20 +0000
From:   xxxx@icloud.com
To:     xxxx@icloud.com
Subject:iCloud メールテスト

text/html

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

Date:   Mon, 29 Jun 2026 08:56:46 +0000
From:   Google <no-reply@accounts.google.com>
To:     xxxx@icloud.com
Subject:xxxx@gmail.com のセキュリティ通知

multipart/alternative
    text/plain
    text/html

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

Date:   Sun, 28 Jun 2026 13:22:36 +0000
From:   Cloudflare Registrar <noreply@notify.cloudflare.com>
To:     xxxx@icloud.com
Subject:30 Day Domain Renewal Notice

multipart/alternative
    text/plain
    text/html
```
</details>


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

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

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "example@icloud.com"
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 "no-reply@accounts.google.com" 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()
```


### メールボックス一覧を表示
```py
import imaplib

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "example@icloud.com"
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"
```

<!--
ゴミ箱を空にするには、expunge関数を実行します。(メールボックスの選択ができん...)
```py
import imaplib

IMAP_SERVER = "imap.mail.me.com"
MAIL_ADDR   = "example@icloud.com"
PASSWORD    = "xxxx-xxxx-xxxx-xxxx"

imap = imaplib.IMAP4_SSL(IMAP_SERVER)
imap.login(MAIL_ADDR, PASSWORD)
imap.expunge()
imap.logout()
```
-->


<!--
### フラグ
IMAPにはメールを管理するためのフラグという機能があり、標準では以下の6つが定義されています。

`\Seen`: 既読（メッセージが開封されたことを示す）
`\Answered`: 返信済み（メッセージに返信したことを示す）
`\Flagged`: 重要/フラグ付き（ユーザーが目印をつけたことを示す）
`\Deleted`: 削除予定（ゴミ箱への移動や削除マークがついたことを示す）
`\Draft`: 下書き（送信前の未完成メッセージであることを示す）
`\Recent`: 新着（前回の接続以降に新しく届いたメッセージであることを示す）

これらのフラグは`STORE`コマンドで管理でき、imaplibにもstoreメソッドが用意されています。
-->


<!--
メールの仕様はRFCなどで規定されているものの、歴史的な経緯によりサーバーやクライアントによって解釈が違う部分があります。
こういった問題に対処するため、Pythonのemailモジュールにはpolicyという仕組みがあり、用途に応じて動作を制御することができます。
policyにはPython3.2以前と互換性のあるemail.policy.compat32(非推奨)や、現在の標準であるemail.policy.defaultなどがあります。
パーサーのpolicyは互換性のために.email.policy.compat32がデフォルト値となっていますが、現在はpolicyを明示することが推奨されており、将来のバージョンで変更される予定だそうです。
-->


## 参考ページ
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
