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
| 特性 | SMTP | POP3 | IMAP |
|---|---|---|---|
| 功能 | 發送郵件 | 下載郵件 | 同步郵件 |
| 方向 | 客戶端 → 伺服器 | 伺服器 → 客戶端 | 雙向同步 |
| Port | 25/587/465 | 110/995 | 143/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 的核心差異是什麼?
答案:
核心差異:郵件儲存位置與同步機制
| 特性 | POP3 | IMAP |
|---|---|---|
| 郵件位置 | 下載到本機 | 留在伺服器 |
| 同步方式 | 單向下載 | 雙向同步 |
| 多裝置 | ❌ 不支援 | ✅ 完美支援 |
| 資料夾 | ❌ 無 | ✅ 完整支援 |
| 離線閱讀 | ✅ 完全支援 | ⚠️ 需先同步 |
| 標記同步 | ❌ 無 | ✅ 已讀、星號等 |
| 部分下載 | ❌ 無 | ✅ 可只下載標頭 |
技術差異:
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. 不是所有伺服器都支援 IDLEQ3: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_data5. 只同步近期郵件:
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