09-3. IMAP 協定:郵件同步協定

深入理解 IMAP 的郵件同步與雲端管理機制

📪 IMAP 協定:郵件同步協定

⏱️ 閱讀時間: 10 分鐘 🎯 難度: ⭐⭐⭐ (中等偏難)


IMAP 在網路模型中的位置

┌──────────────────────────────────────────────────────────┐
│            OSI 七層模型          TCP/IP 四層模型          │
├──────────────────────────────────────────────────────────┤
│  7. 應用層 (Application)                                 │
│     ├─ IMAP ───────────────┐    應用層 (Application)     │
│                             │    (IMAP, SMTP, POP3...)    │
├─────────────────────────────┤                             │
│  6. 表現層 (Presentation)   │                             │
├─────────────────────────────┤                             │
│  5. 會話層 (Session)        │                             │
│     ├─ IMAP Session         │                             │
├─────────────────────────────┼─────────────────────────────┤
│  4. 傳輸層 (Transport)      │    傳輸層 (Transport)       │
│     └─ TCP ─────────────────┘    (TCP)                    │
├─────────────────────────────┼─────────────────────────────┤
│  3. 網路層 (Network)        │    網際網路層 (Internet)    │
│     └─ IP                   │    (IP, ICMP, ARP)          │
├─────────────────────────────┼─────────────────────────────┤
│  2. 資料連結層 (Data Link)  │    網路存取層               │
│  1. 實體層 (Physical)       │    (Network Access)         │
└─────────────────────────────┴─────────────────────────────┘

📍 位置:OSI Layer 7(應用層)/ TCP/IP Layer 4(應用層)
🔌 Port:
  - 143(明文)
  - 993(IMAPS,SSL/TLS 加密)
🚛 傳輸協定:TCP

為什麼 IMAP 用 TCP?

原因說明
可靠同步 🔄郵件狀態(已讀、星號)必須準確同步
長連接 🔗保持連線以接收即時推送(IDLE)
順序保證 🔢資料夾操作需要按順序執行
雙向通訊 ↔️客戶端和伺服器需要雙向同步
複雜操作 🎯搜尋、過濾等複雜查詢需要可靠傳輸

💡 重點:IMAP 設計複雜但功能強大,支援完整的雲端郵件管理


🎯 什麼是 IMAP?

💡 比喻:郵局的透明信箱(遠端看信)

你去郵局 → 透過玻璃看信 → 信留在郵局
- 可以在家裡的郵局看
- 可以在公司的郵局看
- 可以在咖啡廳的郵局看
- 所有地方看到的都一樣

IMAP(Internet Message Access Protocol) 是一種用於在郵件伺服器上管理和同步郵件的協定。郵件保留在伺服器,支援多裝置同步。

IMAP 核心特性

優點:

  • ✅ 多裝置同步(電腦、手機、平板)
  • ✅ 雲端備份(不怕裝置損壞)
  • ✅ 資料夾管理(分類整理)
  • ✅ 搜尋功能(搜尋所有郵件)
  • ✅ 狀態同步(已讀、星號、標籤)
  • ✅ 部分下載(只下載需要的部分)

缺點:

  • ❌ 佔用伺服器空間(需付費或有限制)
  • ❌ 需要網路連線(離線功能有限)
  • ❌ 隱私考量(郵件在雲端)
  • ❌ 較複雜(相比 POP3)

🔀 IMAP vs POP3 vs SMTP

特性SMTPPOP3IMAP
功能發送郵件下載郵件同步郵件
方向客戶端 → 伺服器伺服器 → 客戶端雙向同步
Port25/587/465110/995143/993
郵件位置N/A本機伺服器
多裝置N/A❌ 不支援✅ 完美支援
離線閱讀N/A✅ 完全支援⚠️ 需先同步
資料夾管理N/A❌ 無✅ 完整支援
搜尋N/A只能搜尋本機可搜尋伺服器
標記狀態N/A❌ 無法同步✅ 同步

記憶口訣:

SMTP = 寄(發送郵件)
POP3 = 取(帶走)
IMAP = 看(留著,多處看)

🏗️ IMAP 運作流程

完整會話流程

Client                           IMAP Server (Port 143/993)
  │                                     │
  ├──── 連線 ────────────────────────>│
  │<──── * OK IMAP4 ready ─────────────┤
  │                                     │
  ├──── A001 LOGIN alice password ───>│  認證
  │<──── A001 OK LOGIN completed ──────┤
  │                                     │
  ├──── A002 LIST "" "*" ────────────>│  列出資料夾
  │<──── * LIST () "/" INBOX ──────────┤
  │<──── * LIST () "/" Sent ───────────┤
  │<──── * LIST () "/" Trash ──────────┤
  │<──── A002 OK LIST completed ───────┤
  │                                     │
  ├──── A003 SELECT INBOX ───────────>│  選擇資料夾
  │<──── * 150 EXISTS ─────────────────┤  (150 封郵件)
  │<──── * 5 RECENT ───────────────────┤  (5 封新郵件)
  │<──── * FLAGS (\Seen \Answered...) ─┤
  │<──── A003 OK SELECT completed ─────┤
  │                                     │
  ├──── A004 FETCH 1 BODY[TEXT] ─────>│  取得郵件
  │<──── * 1 FETCH (BODY[TEXT]...) ────┤
  │<──── A004 OK FETCH completed ──────┤
  │                                     │
  ├──── A005 STORE 1 +FLAGS (\Seen) ─>│  標記已讀
  │<──── * 1 FETCH (FLAGS (\Seen)) ────┤
  │<──── A005 OK STORE completed ──────┤
  │                                     │
  ├──── A006 LOGOUT ─────────────────>│  登出
  │<──── * BYE IMAP server logging out ┤
  │<──── A006 OK LOGOUT completed ─────┤

📝 IMAP 命令

認證相關命令

命令功能範例
LOGIN登入A001 LOGIN user pass
LOGOUT登出A002 LOGOUT
AUTHENTICATE安全認證A003 AUTHENTICATE PLAIN

資料夾操作命令

命令功能範例
LIST列出資料夾A004 LIST "" "*"
SELECT選擇資料夾A005 SELECT INBOX
EXAMINE唯讀選擇A006 EXAMINE INBOX
CREATE建立資料夾A007 CREATE Work
DELETE刪除資料夾A008 DELETE Trash
RENAME重新命名A009 RENAME Old New

郵件操作命令

命令功能範例
FETCH取得郵件A010 FETCH 1 BODY[]
STORE修改標記A011 STORE 1 +FLAGS (\Seen)
COPY複製郵件A012 COPY 1 Sent
SEARCH搜尋郵件A013 SEARCH UNSEEN
EXPUNGE永久刪除A014 EXPUNGE

狀態命令

命令功能範例
STATUS取得資料夾狀態A015 STATUS INBOX (MESSAGES)
NOOP保持連線A016 NOOP
IDLE等待推送A017 IDLE

💻 IMAP 會話範例

基本會話

S: * OK IMAP4 server ready <1896.697170952@mail.example.com>

C: A001 LOGIN alice password
S: A001 OK LOGIN completed

C: A002 SELECT INBOX
S: * 2 EXISTS
S: * 0 RECENT
S: * OK [UNSEEN 1] Message 1 is first unseen
S: * OK [UIDVALIDITY 3857529045] UIDs valid
S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
S: A002 OK [READ-WRITE] SELECT completed

C: A003 FETCH 1 BODY[TEXT]
S: * 1 FETCH (BODY[TEXT] {12}
S: Hello Alice!)
S: A003 OK FETCH completed

C: A004 STORE 1 +FLAGS (\Seen)
S: * 1 FETCH (FLAGS (\Seen))
S: A004 OK STORE completed

C: A005 LOGOUT
S: * BYE IMAP4 server logging out
S: A005 OK LOGOUT completed

🐍 Python 使用 IMAP

基本連線與登入

import imaplib

# 連線到 IMAP 伺服器(SSL)
imap = imaplib.IMAP4_SSL('imap.gmail.com', 993)

# 或使用明文連線(不建議)
# imap = imaplib.IMAP4('imap.example.com', 143)

# 登入
imap.login('alice@gmail.com', 'password')

# 列出所有資料夾
status, folders = imap.list()
print("資料夾列表:")
for folder in folders:
    print(folder.decode())

# 登出
imap.logout()

選擇資料夾並讀取郵件

import imaplib
import email
from email.header import decode_header

# 連線並登入
imap = imaplib.IMAP4_SSL('imap.gmail.com')
imap.login('alice@gmail.com', 'password')

# 選擇收件匣
status, messages = imap.select('INBOX')
print(f"收件匣有 {messages[0].decode()} 封郵件")

# 搜尋所有郵件
status, msg_ids = imap.search(None, 'ALL')

# 取得郵件 ID 列表
email_ids = msg_ids[0].split()

# 讀取最新的 5 封郵件
for email_id in email_ids[-5:]:
    # 取得郵件
    status, msg_data = imap.fetch(email_id, '(RFC822)')

    # 解析郵件
    for response_part in msg_data:
        if isinstance(response_part, tuple):
            msg = email.message_from_bytes(response_part[1])

            # 解碼主旨
            subject, encoding = decode_header(msg['Subject'])[0]
            if isinstance(subject, bytes):
                subject = subject.decode(encoding or 'utf-8')

            # 取得寄件者
            from_ = msg.get('From')
            date = msg.get('Date')

            print(f"\n郵件 ID: {email_id.decode()}")
            print(f"From: {from_}")
            print(f"Subject: {subject}")
            print(f"Date: {date}")
            print('-' * 60)

imap.logout()

搜尋郵件

import imaplib

imap = imaplib.IMAP4_SSL('imap.gmail.com')
imap.login('alice@gmail.com', 'password')
imap.select('INBOX')

# 1. 搜尋未讀郵件
status, messages = imap.search(None, 'UNSEEN')
print(f"未讀郵件:{len(messages[0].split())} 封")

# 2. 搜尋特定寄件者
status, messages = imap.search(None, 'FROM', 'boss@company.com')

# 3. 搜尋主旨關鍵字
status, messages = imap.search(None, 'SUBJECT', '會議')

# 4. 搜尋日期範圍
status, messages = imap.search(None, 'SINCE', '01-Jan-2025')
status, messages = imap.search(None, 'BEFORE', '31-Jan-2025')

# 5. 組合條件
status, messages = imap.search(
    None,
    '(FROM "boss@company.com" SUBJECT "緊急" UNSEEN)'
)

# 6. 搜尋已標記星號的郵件
status, messages = imap.search(None, 'FLAGGED')

# 7. 搜尋包含附件的郵件(Gmail 擴展)
status, messages = imap.search(None, 'X-GM-RAW', 'has:attachment')

# 8. 全文搜尋(Gmail 擴展)
status, messages = imap.search(None, 'X-GM-RAW', '"重要文件"')

imap.logout()

資料夾操作

import imaplib

imap = imaplib.IMAP4_SSL('imap.gmail.com')
imap.login('alice@gmail.com', 'password')

# 列出所有資料夾
status, folders = imap.list()
print("所有資料夾:")
for folder in folders:
    print(folder.decode())

# 建立新資料夾
imap.create('Work')
imap.create('Work/Projects')  # 子資料夾

# 重新命名資料夾
imap.rename('Work', 'Office')

# 刪除資料夾(必須為空)
imap.delete('Office/Projects')

# 訂閱資料夾(顯示在客戶端)
imap.subscribe('Office')

# 取消訂閱
imap.unsubscribe('Office')

imap.logout()

郵件標記與狀態管理

import imaplib

imap = imaplib.IMAP4_SSL('imap.gmail.com')
imap.login('alice@gmail.com', 'password')
imap.select('INBOX')

# 標記為已讀
imap.store('1', '+FLAGS', '\\Seen')

# 標記為未讀
imap.store('1', '-FLAGS', '\\Seen')

# 標記星號
imap.store('1', '+FLAGS', '\\Flagged')

# 取消星號
imap.store('1', '-FLAGS', '\\Flagged')

# 標記為刪除(不會立即刪除)
imap.store('1', '+FLAGS', '\\Deleted')

# 永久刪除所有標記為刪除的郵件
imap.expunge()

# 設定多個標記
imap.store('1', '+FLAGS', '(\\Seen \\Flagged)')

# 檢查郵件標記
status, data = imap.fetch('1', '(FLAGS)')
print(f"郵件 1 的標記:{data[0]}")

imap.logout()

移動和複製郵件

import imaplib

imap = imaplib.IMAP4_SSL('imap.gmail.com')
imap.login('alice@gmail.com', 'password')
imap.select('INBOX')

# 複製郵件到其他資料夾
imap.copy('1', 'Work')

# 移動郵件(複製 + 刪除)
imap.copy('1', 'Archive')
imap.store('1', '+FLAGS', '\\Deleted')
imap.expunge()

# 批次移動
# 搜尋特定郵件
status, messages = imap.search(None, 'FROM', 'newsletter@example.com')
email_ids = messages[0].split()

# 移動到特定資料夾
for email_id in email_ids:
    imap.copy(email_id, 'Newsletter')
    imap.store(email_id, '+FLAGS', '\\Deleted')

imap.expunge()
imap.logout()

IDLE 模式(即時推送)

import imaplib
import time

class IdleIMAP(imaplib.IMAP4_SSL):
    """支援 IDLE 的 IMAP 客戶端"""

    def idle(self):
        """進入 IDLE 模式"""
        self.send(b'%s IDLE\r\n' % self._new_tag())
        return self.readline()

    def idle_done(self):
        """離開 IDLE 模式"""
        self.send(b'DONE\r\n')
        return self.readline()

# 使用
imap = IdleIMAP('imap.gmail.com')
imap.login('alice@gmail.com', 'password')
imap.select('INBOX')

print("等待新郵件...")

while True:
    # 進入 IDLE 模式
    response = imap.idle()
    print(f"IDLE 開始:{response}")

    # 等待 29 分鐘(IDLE 限制 30 分鐘)
    time.sleep(29 * 60)

    # 離開 IDLE 模式
    response = imap.idle_done()
    print(f"IDLE 結束:{response}")

    # 檢查新郵件
    status, messages = imap.search(None, 'UNSEEN')
    if messages[0]:
        print(f"有新郵件:{len(messages[0].split())} 封")
        # 處理新郵件...

    # 重新進入 IDLE(循環)

部分下載(節省頻寬)

import imaplib

imap = imaplib.IMAP4_SSL('imap.gmail.com')
imap.login('alice@gmail.com', 'password')
imap.select('INBOX')

# 只下載標頭
status, data = imap.fetch('1', '(BODY[HEADER])')
print("只有標頭:")
print(data[0][1].decode())

# 只下載郵件本文(不含附件)
status, data = imap.fetch('1', '(BODY[TEXT])')
print("\n只有本文:")
print(data[0][1].decode())

# 取得郵件結構(不下載內容)
status, data = imap.fetch('1', '(BODYSTRUCTURE)')
print("\n郵件結構:")
print(data[0])

# 只下載特定部分(例如:第一個附件)
status, data = imap.fetch('1', '(BODY[2])')  # 部分 2

# 取得郵件大小
status, data = imap.fetch('1', '(RFC822.SIZE)')
print(f"\n郵件大小:{data[0]}")

imap.logout()

完整郵件管理範例

import imaplib
import email
from email.header import decode_header
from datetime import datetime

class EmailManager:
    def __init__(self, server, username, password):
        self.imap = imaplib.IMAP4_SSL(server)
        self.imap.login(username, password)

    def list_folders(self):
        """列出所有資料夾"""
        status, folders = self.imap.list()
        folder_list = []

        for folder in folders:
            # 解析資料夾名稱
            parts = folder.decode().split('"')
            if len(parts) >= 3:
                folder_name = parts[-2]
                folder_list.append(folder_name)

        return folder_list

    def select_folder(self, folder='INBOX'):
        """選擇資料夾"""
        status, messages = self.imap.select(folder)
        return int(messages[0])

    def search_emails(self, criteria='ALL'):
        """搜尋郵件"""
        status, messages = self.imap.search(None, criteria)
        return messages[0].split()

    def get_email(self, email_id):
        """取得郵件內容"""
        status, data = self.imap.fetch(email_id, '(RFC822)')

        for response_part in data:
            if isinstance(response_part, tuple):
                msg = email.message_from_bytes(response_part[1])

                # 解碼主旨
                subject = self._decode_header(msg['Subject'])

                # 取得本文
                body = self._get_body(msg)

                return {
                    'id': email_id.decode(),
                    'from': msg.get('From'),
                    'to': msg.get('To'),
                    'subject': subject,
                    'date': msg.get('Date'),
                    'body': body
                }

    def _decode_header(self, header):
        """解碼標頭"""
        if header is None:
            return ''

        decoded = decode_header(header)[0]
        text, encoding = decoded

        if isinstance(text, bytes):
            return text.decode(encoding or 'utf-8', errors='ignore')
        return text

    def _get_body(self, msg):
        """取得郵件本文"""
        if msg.is_multipart():
            for part in msg.walk():
                content_type = part.get_content_type()
                if content_type == 'text/plain':
                    payload = part.get_payload(decode=True)
                    if payload:
                        return payload.decode('utf-8', errors='ignore')
        else:
            payload = msg.get_payload(decode=True)
            if payload:
                return payload.decode('utf-8', errors='ignore')

        return ''

    def mark_as_read(self, email_id):
        """標記為已讀"""
        self.imap.store(email_id, '+FLAGS', '\\Seen')

    def mark_as_unread(self, email_id):
        """標記為未讀"""
        self.imap.store(email_id, '-FLAGS', '\\Seen')

    def move_to_folder(self, email_id, folder):
        """移動到資料夾"""
        self.imap.copy(email_id, folder)
        self.imap.store(email_id, '+FLAGS', '\\Deleted')
        self.imap.expunge()

    def delete_email(self, email_id):
        """刪除郵件"""
        self.imap.store(email_id, '+FLAGS', '\\Deleted')
        self.imap.expunge()

    def close(self):
        """關閉連線"""
        self.imap.logout()

# 使用範例
manager = EmailManager('imap.gmail.com', 'alice@gmail.com', 'password')

# 列出資料夾
folders = manager.list_folders()
print(f"資料夾:{folders}")

# 選擇收件匣
count = manager.select_folder('INBOX')
print(f"收件匣有 {count} 封郵件")

# 搜尋未讀郵件
unread = manager.search_emails('UNSEEN')
print(f"未讀郵件:{len(unread)} 封")

# 讀取第一封未讀郵件
if unread:
    email_data = manager.get_email(unread[0])
    print(f"\n主旨:{email_data['subject']}")
    print(f"寄件者:{email_data['from']}")
    print(f"內容:{email_data['body'][:100]}...")

    # 標記為已讀
    manager.mark_as_read(unread[0])

manager.close()

🎓 常見面試題

Q1:IMAP 和 POP3 的核心差異是什麼?

答案:

核心差異:郵件儲存位置與同步機制

特性POP3IMAP
郵件位置下載到本機留在伺服器
同步方式單向下載雙向同步
多裝置❌ 不支援✅ 完美支援
資料夾❌ 無✅ 完整支援
離線閱讀✅ 完全支援⚠️ 需先同步
標記同步❌ 無✅ 已讀、星號等
部分下載❌ 無✅ 可只下載標頭

技術差異:

POP3 工作流程(簡單):
1. 連線
2. 認證
3. 列出郵件
4. 下載郵件
5. 刪除郵件(可選)
6. 斷線

IMAP 工作流程(複雜):
1. 連線
2. 認證
3. 列出資料夾結構
4. 選擇資料夾
5. 同步郵件狀態(已讀、星號、標籤)
6. 按需下載郵件(不是全部)
7. 保持連線(即時更新)
8. 操作同步回伺服器

現實場景:

# POP3:單一裝置
# 在公司電腦下載郵件
pop3.retr(1)  # 郵件下載到電腦
pop3.dele(1)  # 伺服器刪除

# 回家後...
# 手機、家裡電腦看不到這封郵件(已被刪除)

# IMAP:多裝置同步
# 在公司電腦
imap.store('1', '+FLAGS', '\\Seen')  # 標記已讀

# 回家後...
# 手機、家裡電腦也顯示為「已讀」(同步)

Q2:IMAP 如何實作即時推送(IDLE)?

答案:

IDLE 命令(RFC 2177)

原理:

傳統輪詢(Polling):
客戶端每 5 分鐘問一次「有新郵件嗎?」
→ 浪費資源、延遲高

IMAP IDLE:
客戶端:「我在這裡等,有新郵件請告訴我」
伺服器:(保持連線,有新郵件立即推送)
→ 即時、省資源

流程:

Client                    Server
  │                          │
  ├── A001 SELECT INBOX ────>│
  │<── A001 OK ───────────────┤
  │                          │
  ├── A002 IDLE ────────────>│  進入 IDLE 模式
  │<── + idling ──────────────┤
  │                          │
  │   (等待新郵件...)         │
  │                          │
  │<── * 5 EXISTS ────────────┤  新郵件到達!
  │<── * 5 RECENT ────────────┤
  │                          │
  ├── DONE ─────────────────>│  結束 IDLE
  │<── A002 OK ───────────────┤
  │                          │
  ├── A003 FETCH 5 BODY[] ──>│  下載新郵件

Python 實作:

import imaplib
import select

class IdleMailbox:
    def __init__(self, server, user, password):
        self.imap = imaplib.IMAP4_SSL(server)
        self.imap.login(user, password)
        self.imap.select('INBOX')

    def wait_for_new_mail(self):
        """等待新郵件"""
        # 進入 IDLE 模式
        tag = self.imap._new_tag().decode()
        self.imap.send(f'{tag} IDLE\r\n'.encode())

        # 等待伺服器回應
        while True:
            # 使用 select 檢查是否有資料
            ready = select.select([self.imap.socket()], [], [], 29*60)

            if ready[0]:
                # 有資料(可能是新郵件)
                response = self.imap.readline()

                if b'EXISTS' in response:
                    print("有新郵件!")

                    # 結束 IDLE
                    self.imap.send(b'DONE\r\n')
                    self.imap.readline()  # 讀取 OK 回應

                    return True

            # 29 分鐘後重新 IDLE(IDLE 限制 30 分鐘)
            self.imap.send(b'DONE\r\n')
            self.imap.readline()

            # 重新進入 IDLE
            tag = self.imap._new_tag().decode()
            self.imap.send(f'{tag} IDLE\r\n'.encode())

# 使用
mailbox = IdleMailbox('imap.gmail.com', 'alice@gmail.com', 'password')

while True:
    if mailbox.wait_for_new_mail():
        print("處理新郵件...")
        # 處理新郵件的邏輯

注意事項:

1. IDLE 有時間限制(通常 30 分鐘)
2. 需要定期 DONE 再重新 IDLE
3. 網路斷線需要重連
4. 不是所有伺服器都支援 IDLE

Q3:IMAP 如何優化大量郵件的處理?

答案:

多種優化策略:

1. 部分下載(只取需要的部分):

# ❌ 不好:下載完整郵件(包含大附件)
status, data = imap.fetch('1', '(RFC822)')  # 可能數 MB

# ✅ 好:只下載標頭
status, data = imap.fetch('1', '(BODY[HEADER])')  # 幾 KB

# ✅ 好:只下載本文(不含附件)
status, data = imap.fetch('1', '(BODY[TEXT])')

2. 批次操作(減少往返次數):

# ❌ 不好:逐一操作
for i in range(1, 100):
    imap.store(str(i), '+FLAGS', '\\Seen')  # 100 次請求

# ✅ 好:批次操作
imap.store('1:100', '+FLAGS', '\\Seen')  # 1 次請求

3. 使用 UID(避免編號變動):

# ❌ 不好:使用序號(會變動)
imap.fetch('1', '(RFC822)')  # 如果刪除郵件,編號會改變

# ✅ 好:使用 UID(永久不變)
imap.uid('FETCH', '12345', '(RFC822)')

4. 快取策略:

import json
import os

class CachedIMAPClient:
    def __init__(self, server, user, password):
        self.imap = imaplib.IMAP4_SSL(server)
        self.imap.login(user, password)
        self.cache_file = 'email_cache.json'
        self.cache = self.load_cache()

    def load_cache(self):
        if os.path.exists(self.cache_file):
            with open(self.cache_file, 'r') as f:
                return json.load(f)
        return {}

    def save_cache(self):
        with open(self.cache_file, 'w') as f:
            json.dump(self.cache, f)

    def get_email(self, uid):
        # 檢查快取
        if uid in self.cache:
            print(f"從快取讀取 UID {uid}")
            return self.cache[uid]

        # 從伺服器下載
        print(f"從伺服器下載 UID {uid}")
        status, data = self.imap.uid('FETCH', uid, '(RFC822)')

        # 解析並快取
        email_data = self.parse_email(data)
        self.cache[uid] = email_data
        self.save_cache()

        return email_data

5. 只同步近期郵件:

from datetime import datetime, timedelta

# 只搜尋最近 30 天的郵件
since_date = (datetime.now() - timedelta(days=30)).strftime('%d-%b-%Y')
status, messages = imap.search(None, f'SINCE {since_date}')

# 或只搜尋未讀郵件
status, messages = imap.search(None, 'UNSEEN')

6. 並行處理(多執行緒):

from concurrent.futures import ThreadPoolExecutor
import imaplib

def fetch_email(email_id):
    # 每個執行緒建立獨立連線
    imap = imaplib.IMAP4_SSL('imap.gmail.com')
    imap.login('user@gmail.com', 'password')
    imap.select('INBOX')

    status, data = imap.fetch(email_id, '(RFC822)')
    # 處理郵件...

    imap.logout()
    return data

# 並行下載多封郵件
email_ids = ['1', '2', '3', '4', '5']

with ThreadPoolExecutor(max_workers=5) as executor:
    results = executor.map(fetch_email, email_ids)

Q4:如何實作郵件的本地快取與離線支援?

答案:

完整離線支援策略:

架構:

線上模式:IMAP ↔ 本地資料庫 ↔ 應用程式
離線模式:本地資料庫 ↔ 應用程式

實作範例(使用 SQLite):

import sqlite3
import imaplib
import email
import json
from datetime import datetime

class OfflineIMAPClient:
    def __init__(self, server, user, password):
        self.server = server
        self.user = user
        self.password = password
        self.imap = None
        self.db = sqlite3.connect('emails.db')
        self.init_db()

    def init_db(self):
        """初始化資料庫"""
        cursor = self.db.cursor()

        cursor.execute('''
            CREATE TABLE IF NOT EXISTS emails (
                uid TEXT PRIMARY KEY,
                folder TEXT,
                subject TEXT,
                sender TEXT,
                date TEXT,
                body TEXT,
                flags TEXT,
                synced INTEGER DEFAULT 0
            )
        ''')

        cursor.execute('''
            CREATE TABLE IF NOT EXISTS sync_state (
                folder TEXT PRIMARY KEY,
                last_sync TEXT,
                uidvalidity INTEGER
            )
        ''')

        self.db.commit()

    def connect(self):
        """連線到 IMAP 伺服器"""
        try:
            self.imap = imaplib.IMAP4_SSL(self.server)
            self.imap.login(self.user, self.password)
            return True
        except:
            return False

    def sync_folder(self, folder='INBOX'):
        """同步資料夾"""
        if not self.imap:
            print("離線模式:無法同步")
            return False

        self.imap.select(folder)

        # 取得所有郵件 UID
        status, data = self.imap.uid('SEARCH', None, 'ALL')
        uids = data[0].split()

        cursor = self.db.cursor()

        for uid in uids:
            uid_str = uid.decode()

            # 檢查是否已下載
            cursor.execute('SELECT uid FROM emails WHERE uid = ?', (uid_str,))
            if cursor.fetchone():
                continue  # 已存在,跳過

            # 下載郵件
            status, data = self.imap.uid('FETCH', uid, '(RFC822 FLAGS)')

            if status == 'OK':
                msg = email.message_from_bytes(data[0][1])

                # 儲存到資料庫
                cursor.execute('''
                    INSERT INTO emails (uid, folder, subject, sender, date, body, flags, synced)
                    VALUES (?, ?, ?, ?, ?, ?, ?, 1)
                ''', (
                    uid_str,
                    folder,
                    msg.get('Subject', ''),
                    msg.get('From', ''),
                    msg.get('Date', ''),
                    self.get_body(msg),
                    json.dumps(self.parse_flags(data[0][0])),
                ))

        # 更新同步狀態
        cursor.execute('''
            INSERT OR REPLACE INTO sync_state (folder, last_sync)
            VALUES (?, ?)
        ''', (folder, datetime.now().isoformat()))

        self.db.commit()
        print(f"同步完成:{len(uids)} 封郵件")
        return True

    def get_emails_offline(self, folder='INBOX', limit=50):
        """離線讀取郵件"""
        cursor = self.db.cursor()

        cursor.execute('''
            SELECT uid, subject, sender, date, body, flags
            FROM emails
            WHERE folder = ?
            ORDER BY date DESC
            LIMIT ?
        ''', (folder, limit))

        emails = []
        for row in cursor.fetchall():
            emails.append({
                'uid': row[0],
                'subject': row[1],
                'sender': row[2],
                'date': row[3],
                'body': row[4],
                'flags': json.loads(row[5])
            })

        return emails

    def mark_as_read_offline(self, uid):
        """離線標記為已讀"""
        cursor = self.db.cursor()

        # 更新本地資料庫
        cursor.execute('''
            UPDATE emails
            SET flags = json_set(flags, '$.seen', 1), synced = 0
            WHERE uid = ?
        ''', (uid,))

        self.db.commit()

        # 如果在線,同步到伺服器
        if self.imap:
            self.imap.uid('STORE', uid, '+FLAGS', '\\Seen')

    def get_body(self, msg):
        """取得郵件本文"""
        if msg.is_multipart():
            for part in msg.walk():
                if part.get_content_type() == 'text/plain':
                    return part.get_payload(decode=True).decode('utf-8', errors='ignore')
        else:
            return msg.get_payload(decode=True).decode('utf-8', errors='ignore')
        return ''

    def parse_flags(self, flags_data):
        """解析標記"""
        # 簡化版本
        return {'seen': b'\\Seen' in flags_data}

# 使用
client = OfflineIMAPClient('imap.gmail.com', 'alice@gmail.com', 'password')

# 線上模式:同步
if client.connect():
    client.sync_folder('INBOX')

# 離線模式:讀取本地郵件
emails = client.get_emails_offline('INBOX', limit=10)
for email in emails:
    print(f"{email['subject']} - {email['sender']}")

# 離線標記已讀
client.mark_as_read_offline(emails[0]['uid'])

📝 總結

IMAP 協定核心要點:

  • 功能:同步郵件到多裝置 📪
  • 特色:雲端管理、資料夾、即時推送
  • 優勢:多裝置完美同步

IMAP vs POP3 vs SMTP:

SMTP = 寄信 📮
POP3 = 取信(帶走)📬
IMAP = 看信(留著,多處看)📪

適用場景:

✅ 使用 IMAP:
- 多裝置使用(電腦、手機、平板)
- 需要雲端備份
- 需要資料夾管理
- 現代主流(Gmail、Outlook)

⚠️ 使用 POP3:
- 單一裝置
- 離線閱讀
- 伺服器空間限制
- 隱私考量

記憶口訣:「雲(雲端)、多(多裝置)、同(同步)、資(資料夾)」

最佳實踐:

  • 使用 SSL/TLS 加密(Port 993)
  • 使用 UID 而非序號
  • 部分下載節省頻寬
  • 實作本地快取離線支援
  • 使用 IDLE 實現即時推送

🔗 延伸閱讀

  • 上一篇:09-2. POP3 協定
  • 相關文章:09-1. SMTP 協定
  • RFC 3501(IMAP):https://tools.ietf.org/html/rfc3501
  • RFC 2177(IMAP IDLE):https://tools.ietf.org/html/rfc2177
  • Gmail IMAP 擴展:https://developers.google.com/gmail/imap/imap-extensions
0%