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

特性SMTPPOP3IMAP
功能發送郵件下載郵件同步郵件
方向客戶端 → 伺服器伺服器 → 客戶端雙向同步
Port25/587/465110/995143/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取得唯一 IDUIDL郵件 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 的主要差異是什麼?

答案:

核心差異:郵件儲存位置

特性POP3IMAP
郵件位置下載到本機留在伺服器
多裝置同步❌ 不支援✅ 完美支援
資料夾管理❌ 無✅ 完整支援
離線閱讀✅ 完全支援⚠️ 需先同步
伺服器空間節省(下載後刪除)佔用(郵件留在伺服器)
適合場景單一裝置、隱私需求多裝置、雲端管理

生活比喻:

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
0%