09-2. POP3 協定:郵件下載協定
深入理解 POP3 的郵件下載機制與應用
目錄
📬 POP3 協定:郵件下載協定
⏱️ 閱讀時間: 8 分鐘 🎯 難度: ⭐⭐ (中等)
POP3 在網路模型中的位置
┌──────────────────────────────────────────────────────────┐
│ OSI 七層模型 TCP/IP 四層模型 │
├──────────────────────────────────────────────────────────┤
│ 7. 應用層 (Application) │
│ ├─ POP3 ───────────────┐ 應用層 (Application) │
│ │ (POP3, SMTP, IMAP...) │
├─────────────────────────────┤ │
│ 6. 表現層 (Presentation) │ │
├─────────────────────────────┤ │
│ 5. 會話層 (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:
- 110(明文)
- 995(POP3S,SSL/TLS 加密)
🚛 傳輸協定:TCP為什麼 POP3 用 TCP?
| 原因 | 說明 |
|---|---|
| 可靠傳輸 ✅ | 郵件不能遺失,需要確保完整下載 |
| 順序保證 🔢 | 郵件內容必須按順序接收 |
| 連線狀態 🔗 | 需要維護登入狀態和下載進度 |
| 錯誤處理 🛡️ | TCP 提供錯誤檢測和重傳 |
💡 重點:POP3 設計簡單,專注於「下載郵件」這個單一功能
🎯 什麼是 POP3?
💡 比喻:去郵局取信(帶回家)
你去信箱 → 把所有信拿走 → 帶回家
- 信箱清空
- 只能在家裡看信
- 不能在辦公室再看一次POP3(Post Office Protocol version 3) 是一種用於從郵件伺服器下載郵件到本機的協定。
POP3 核心特性
優點:
- ✅ 簡單高效
- ✅ 離線閱讀(郵件在本機)
- ✅ 節省伺服器空間(下載後刪除)
- ✅ 隱私保護(郵件不留在雲端)
缺點:
- ❌ 郵件下載後伺服器通常會刪除
- ❌ 無法多裝置同步
- ❌ 無法管理資料夾
- ❌ 標記狀態無法同步
🔀 POP3 vs IMAP vs SMTP
| 特性 | SMTP | POP3 | IMAP |
|---|---|---|---|
| 功能 | 發送郵件 | 下載郵件 | 同步郵件 |
| 方向 | 客戶端 → 伺服器 | 伺服器 → 客戶端 | 雙向同步 |
| Port | 25/587/465 | 110/995 | 143/993 |
| 郵件位置 | N/A | 本機(下載後) | 伺服器 |
| 多裝置 | N/A | ❌ 不支援 | ✅ 完美支援 |
| 離線閱讀 | N/A | ✅ 完全支援 | ⚠️ 需先同步 |
| 伺服器空間 | N/A | ✅ 節省 | ❌ 佔用空間 |
| 資料夾管理 | N/A | ❌ 不支援 | ✅ 完整支援 |
記憶口訣:
SMTP = 寄(發送郵件)
POP3 = 取(帶走)
IMAP = 看(留著)🏗️ POP3 運作流程
完整會話流程
Client POP3 Server (Port 110/995)
│ │
├──── 連線 ────────────────────────>│
│ │
│<──── +OK POP3 ready ───────────────┤
│ │
├──── USER alice ──────────────────>│ 認證階段
│<──── +OK ──────────────────────────┤
│ │
├──── PASS secret123 ──────────────>│
│<──── +OK Logged in ────────────────┤
│ │
├──── STAT ────────────────────────>│ 交易階段
│<──── +OK 2 320 ────────────────────┤ (2 封郵件, 320 bytes)
│ │
├──── LIST ────────────────────────>│
│<──── +OK 2 messages ───────────────┤
│<──── 1 120 ────────────────────────┤
│<──── 2 200 ────────────────────────┤
│ │
├──── RETR 1 ──────────────────────>│
│<──── +OK 120 octets ───────────────┤
│<──── (郵件內容) ────────────────────┤
│ │
├──── DELE 1 ──────────────────────>│
│<──── +OK message 1 deleted ────────┤
│ │
├──── QUIT ────────────────────────>│ 更新階段
│<──── +OK Goodbye ──────────────────┤ (真正刪除郵件)📝 POP3 命令
認證階段命令
| 命令 | 功能 | 範例 |
|---|---|---|
| USER | 指定使用者名稱 | USER alice |
| PASS | 提供密碼 | PASS secret123 |
| APOP | 安全認證(MD5) | APOP alice <digest> |
交易階段命令
| 命令 | 功能 | 範例 | 回應 |
|---|---|---|---|
| STAT | 郵件統計 | STAT | +OK 2 320 (2封, 320 bytes) |
| LIST | 列出所有郵件 | LIST | 郵件編號和大小清單 |
| LIST n | 列出特定郵件 | LIST 1 | +OK 1 120 |
| RETR n | 下載郵件 | RETR 1 | 郵件完整內容 |
| DELE n | 標記刪除 | DELE 1 | +OK |
| NOOP | 無操作(保持連線) | NOOP | +OK |
| RSET | 重置(取消刪除) | RSET | +OK |
| TOP n m | 取得郵件前 m 行 | TOP 1 10 | 郵件前 10 行 |
| UIDL | 取得唯一 ID | UIDL | 郵件 ID 清單 |
更新階段命令
| 命令 | 功能 | 說明 |
|---|---|---|
| QUIT | 結束連線 | 真正刪除被標記的郵件 |
💻 POP3 會話範例
基本會話
S: +OK POP3 server ready <1896.697170952@mail.example.com>
C: USER alice
S: +OK
C: PASS secret123
S: +OK Logged in
C: STAT
S: +OK 2 320
(2 封郵件,總共 320 bytes)
C: LIST
S: +OK 2 messages
S: 1 120
S: 2 200
S: .
C: RETR 1
S: +OK 120 octets
S: From: bob@example.com
S: To: alice@example.com
S: Subject: Hello
S:
S: Hi Alice!
S: .
C: DELE 1
S: +OK message 1 deleted
C: QUIT
S: +OK Goodbye🐍 Python 使用 POP3
基本連線與登入
import poplib
# 連線到 POP3 伺服器(SSL)
server = poplib.POP3_SSL('pop.gmail.com', 995)
# 或使用明文連線(不建議)
# server = poplib.POP3('pop.example.com', 110)
# 登入
server.user('alice@gmail.com')
server.pass_('password')
# 取得郵件統計
num_messages = len(server.list()[1])
print(f'共有 {num_messages} 封郵件')
# 登出
server.quit()下載所有郵件
import poplib
from email import parser
# 連線並登入
server = poplib.POP3_SSL('pop.gmail.com', 995)
server.user('alice@gmail.com')
server.pass_('password')
# 取得郵件數量
num_messages = len(server.list()[1])
print(f'共有 {num_messages} 封郵件')
# 下載所有郵件
for i in range(num_messages):
# 下載郵件(1-indexed)
raw_email = b"\n".join(server.retr(i+1)[1])
# 解析郵件
email_message = parser.Parser().parsestr(raw_email.decode('utf-8'))
# 顯示基本資訊
print(f"\n郵件 #{i+1}")
print(f"From: {email_message['From']}")
print(f"To: {email_message['To']}")
print(f"Subject: {email_message['Subject']}")
print(f"Date: {email_message['Date']}")
# 取得內容
if email_message.is_multipart():
for part in email_message.walk():
content_type = part.get_content_type()
if content_type == 'text/plain':
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
print(f"Content:\n{body[:200]}...") # 只顯示前 200 字元
else:
body = email_message.get_payload(decode=True)
if body:
print(f"Content:\n{body.decode('utf-8', errors='ignore')[:200]}...")
print('-' * 60)
# 關閉連線
server.quit()下載特定郵件
import poplib
from email import parser
server = poplib.POP3_SSL('pop.gmail.com', 995)
server.user('alice@gmail.com')
server.pass_('password')
# 先列出所有郵件
resp, mails, octets = server.list()
print(f"郵件清單:")
for mail in mails:
print(mail.decode()) # 格式:編號 大小
# 下載第 1 封郵件
resp, lines, octets = server.retr(1)
# 合併郵件內容
raw_email = b"\n".join(lines)
email_message = parser.Parser().parsestr(raw_email.decode('utf-8'))
print(f"Subject: {email_message['Subject']}")
print(f"From: {email_message['From']}")
server.quit()只取得郵件標頭(不下載全部)
import poplib
server = poplib.POP3_SSL('pop.gmail.com', 995)
server.user('alice@gmail.com')
server.pass_('password')
# 使用 TOP 命令:取得郵件前 0 行(只有標頭)
resp, lines, octets = server.top(1, 0)
# 解析標頭
raw_header = b"\n".join(lines).decode('utf-8')
print(raw_header)
server.quit()處理附件
import poplib
from email import parser
import os
server = poplib.POP3_SSL('pop.gmail.com', 995)
server.user('alice@gmail.com')
server.pass_('password')
# 下載第 1 封郵件
raw_email = b"\n".join(server.retr(1)[1])
email_message = parser.Parser().parsestr(raw_email.decode('utf-8'))
# 處理附件
if email_message.is_multipart():
for part in email_message.walk():
# 取得 Content-Disposition
content_disposition = str(part.get('Content-Disposition'))
# 檢查是否為附件
if 'attachment' in content_disposition:
filename = part.get_filename()
if filename:
# 解碼檔名
if filename:
print(f"發現附件:{filename}")
# 儲存附件
filepath = os.path.join('attachments', filename)
os.makedirs('attachments', exist_ok=True)
with open(filepath, 'wb') as f:
f.write(part.get_payload(decode=True))
print(f"已儲存到:{filepath}")
server.quit()使用 UIDL(保留已下載的郵件)
import poplib
import json
import os
# 儲存已下載郵件的 UID
UIDL_FILE = 'downloaded_uids.json'
def load_downloaded_uids():
"""載入已下載的 UID"""
if os.path.exists(UIDL_FILE):
with open(UIDL_FILE, 'r') as f:
return set(json.load(f))
return set()
def save_downloaded_uids(uids):
"""儲存已下載的 UID"""
with open(UIDL_FILE, 'w') as f:
json.dump(list(uids), f)
# 連線
server = poplib.POP3_SSL('pop.gmail.com', 995)
server.user('alice@gmail.com')
server.pass_('password')
# 載入已下載的 UID
downloaded_uids = load_downloaded_uids()
# 取得伺服器上所有郵件的 UIDL
resp, uidl_list, octets = server.uidl()
new_count = 0
for uidl_line in uidl_list:
# 格式:編號 UID
msg_num, uid = uidl_line.decode().split()
msg_num = int(msg_num)
# 檢查是否已下載
if uid not in downloaded_uids:
print(f"下載新郵件 #{msg_num} (UID: {uid})")
# 下載郵件
raw_email = b"\n".join(server.retr(msg_num)[1])
# ... 處理郵件 ...
# 標記為已下載
downloaded_uids.add(uid)
new_count += 1
# 不刪除郵件(POP3 預設會刪除,這裡不呼叫 DELE)
print(f"下載了 {new_count} 封新郵件")
# 儲存 UID 清單
save_downloaded_uids(downloaded_uids)
server.quit()🎓 常見面試題
Q1:POP3 和 IMAP 的主要差異是什麼?
答案:
核心差異:郵件儲存位置
| 特性 | POP3 | IMAP |
|---|---|---|
| 郵件位置 | 下載到本機 | 留在伺服器 |
| 多裝置同步 | ❌ 不支援 | ✅ 完美支援 |
| 資料夾管理 | ❌ 無 | ✅ 完整支援 |
| 離線閱讀 | ✅ 完全支援 | ⚠️ 需先同步 |
| 伺服器空間 | 節省(下載後刪除) | 佔用(郵件留在伺服器) |
| 適合場景 | 單一裝置、隱私需求 | 多裝置、雲端管理 |
生活比喻:
POP3 = 從郵局把信拿回家
- 信帶走了,郵局就沒有了
- 只能在家看信
- 換地方就看不到
IMAP = 透過玻璃看郵局的信箱
- 信還在郵局
- 可以在任何地方看
- 所有地方看到的都一樣什麼時候用 POP3?
✅ 只用一台電腦收信
✅ 郵件伺服器空間小
✅ 需要離線閱讀
✅ 隱私考量(不想郵件留雲端)什麼時候用 IMAP?
✅ 多裝置收信(電腦、手機、平板)
✅ 需要雲端備份
✅ 需要資料夾管理
✅ 現代主流(Gmail、Outlook 預設)Q2:POP3 如何避免重複下載郵件?
答案:
使用 UIDL(Unique ID Listing)命令
原理:
每封郵件有唯一的 UID(不會改變)
客戶端記錄已下載的 UID
下次只下載新的 UID實作步驟:
# 1. 第一次執行
server.uidl()
# 回應:
# 1 AAA111
# 2 BBB222
# 3 CCC333
# 下載並記錄:['AAA111', 'BBB222', 'CCC333']
# 2. 第二次執行(收到新郵件)
server.uidl()
# 回應:
# 1 AAA111 ← 已下載
# 2 BBB222 ← 已下載
# 3 CCC333 ← 已下載
# 4 DDD444 ← 新郵件!
# 只下載 UID = DDD444 的郵件完整範例:
import poplib
import json
# 載入已下載的 UID
def load_uids():
try:
with open('uids.json', 'r') as f:
return set(json.load(f))
except:
return set()
# 儲存 UID
def save_uids(uids):
with open('uids.json', 'w') as f:
json.dump(list(uids), f)
server = poplib.POP3_SSL('pop.gmail.com', 995)
server.user('user@gmail.com')
server.pass_('password')
downloaded = load_uids()
# 取得所有郵件的 UID
resp, uidl_list, octets = server.uidl()
for line in uidl_list:
num, uid = line.decode().split()
if uid not in downloaded:
# 下載新郵件
raw_email = server.retr(int(num))[1]
# ... 處理郵件 ...
# 記錄 UID
downloaded.add(uid)
save_uids(downloaded)
server.quit()Q3:POP3 如何保留郵件在伺服器?
答案:
方法:不呼叫 DELE 命令
POP3 預設行為:
1. RETR(下載郵件)
2. DELE(標記刪除)
3. QUIT(真正刪除)
結果:郵件被刪除保留郵件的做法:
# ❌ 會刪除郵件
server.retr(1) # 下載
server.dele(1) # 刪除
server.quit() # 確認刪除
# ✅ 保留郵件
server.retr(1) # 只下載
# 不呼叫 dele()
server.quit() # 郵件保留在伺服器完整範例:
import poplib
server = poplib.POP3_SSL('pop.gmail.com', 995)
server.user('alice@gmail.com')
server.pass_('password')
# 下載所有郵件
num_messages = len(server.list()[1])
for i in range(1, num_messages + 1):
# 只下載,不刪除
raw_email = server.retr(i)[1]
# ... 處理郵件 ...
# 注意:不呼叫 server.dele(i)
# 郵件保留在伺服器
server.quit()配合 UIDL 避免重複下載:
# 使用 UIDL 記錄已下載的郵件
# 下次只下載新郵件
# 郵件保留在伺服器
# 不會重複處理Q4:POP3 如何處理大型郵件?
答案:
策略 1:使用 TOP 命令先檢查大小
import poplib
server = poplib.POP3_SSL('pop.gmail.com', 995)
server.user('alice@gmail.com')
server.pass_('password')
# 取得郵件列表(包含大小)
resp, mails, octets = server.list()
MAX_SIZE = 5 * 1024 * 1024 # 5 MB
for mail in mails:
num, size = mail.decode().split()
num = int(num)
size = int(size)
if size > MAX_SIZE:
print(f"郵件 #{num} 太大 ({size} bytes),跳過")
continue
# 下載郵件
raw_email = server.retr(num)[1]
# ... 處理 ...
server.quit()策略 2:先下載標頭,再決定是否下載全文
# 使用 TOP 只下載標頭
resp, lines, octets = server.top(1, 0) # 只取標頭(0 行內容)
# 檢查標頭
from email import parser
header = parser.Parser().parsestr(b"\n".join(lines).decode())
# 檢查主旨
if '重要' in header['Subject']:
# 下載完整郵件
raw_email = server.retr(1)[1]
else:
print("不重要的郵件,跳過")策略 3:分塊下載(POP3 不直接支援,需配合其他工具)
# POP3 不支援分塊下載
# 如果需要下載大型郵件,建議:
# 1. 使用 IMAP(支援部分下載)
# 2. 增加超時時間
# 3. 使用專業郵件客戶端
import socket
socket.setdefaulttimeout(300) # 5 分鐘超時
server = poplib.POP3_SSL('pop.gmail.com', 995)
# ... 下載大型郵件 ...📝 總結
POP3 協定核心要點:
- 功能:下載郵件到本機 📬
- 特色:簡單、高效、離線可用
- 限制:不支援多裝置同步
POP3 vs IMAP 選擇:
選擇 POP3:
✅ 單一裝置使用
✅ 離線閱讀需求
✅ 伺服器空間有限
✅ 隱私考量(不留雲端)
選擇 IMAP:
✅ 多裝置同步(現代主流)
✅ 雲端管理
✅ 資料夾分類
✅ Gmail/Outlook 等服務記憶口訣:「取(POP3)vs 看(IMAP)」
最佳實踐:
- 使用 SSL/TLS 加密(Port 995)
- 使用 UIDL 避免重複下載
- 不刪除郵件(不呼叫 DELE)
- 定期備份本地郵件
- 考慮改用 IMAP(現代趨勢)
🔗 延伸閱讀
- 上一篇:09-1. SMTP 協定
- 下一篇:09-3. IMAP 協定
- RFC 1939(POP3):https://tools.ietf.org/html/rfc1939
- RFC 2595(POP3 over TLS):https://tools.ietf.org/html/rfc2595