📚 第八章 应用协议
📄 一句话概括

POP3 的模型是「把信搬到本机」:收完就删,服务器只当临时中转柜。 三个阶段:授权、事务、更新,删掉的标记要到 QUIT 才生效。

📝 本节和前面几节的关系

这一节会起一台假 POP3 服务,用真的 poplib 收一次信,把双方对话全打出来。 然后说清它的两个结构性缺陷。

一、三个阶段,一条命令一条应答

POP3 只有三个阶段:授权(USER 和 PASS)、事务(STAT、LIST、RETR、DELE)、更新(QUIT)。 这三个阶段的划分不是为了整齐,是为了「权限和状态分离」—— 授权之前一条邮件都碰不到,更新之后才算真删。 命令只有十来条,而且严格一问一答。 所以 POP3 的客户端实现极其简单,这就是它当年能铺开的原因之一。 注意 STAT 和 LIST 的区别:一个只报「几封、共多少字节」,一个逐条报大小。 客户端先用 STAT 决定「要不要收」,再用 LIST 决定「先收哪封」。

二、DELE 只是打个标记

这是 POP3 设计里少见的「安全默认值」。 DELE 之后邮件并不会立刻消失,真正删除要等 QUIT 进入更新阶段。 所以中途断线,那些标记全部作废,信一封都不会丢。 这个设计的意义在于:「收信」这个动作可能持续几分钟,中间断线的概率不低。 如果 DELE 立刻生效,一次网络抖动就可能永久丢信。 把「意图」和「生效」分开,是分布式系统里反复出现的模式。 有意思的是:同样的问题在 IMAP 里用「标记删除 + EXPUNGE」解决, 机制更复杂,但目的完全一样。 「两阶段删除」几乎是所有邮件协议的标配。

三、两个结构性缺陷

第一个是「收完就删」:第二台设备看不到第一台收过的信。 这在单机时代无所谓,在「手机加电脑加平板」的时代就是灾难。 很多客户端默认「服务器保留副本」,就是为了绕过它——代价是服务器空间一直被占着。 第二个是「状态在本地」:「已读」「标记」「在哪个文件夹」这些概念在 POP3 里根本不存在。 它们只存在于客户端的本地数据库里,换一台机器就全没了。 所以 POP3 不是「功能少」,是「它的数据模型里没有这些字段」。 这两条加起来,决定了 POP3 的定位:「一台机器收一个邮箱」。 一旦「多设备」成为常态,就必须换协议——这就是 IMAP 存在的全部理由。

四、动手 Demo:POP3 的「下载并删除」,为什么两台电脑会打架

起一台假 POP3 服务器(两封信),用真的 poplib 走一遍全流程。 重点是「删除发生在 QUIT 那一刻」。

import socketserver, threading, poplib

box = [('1', '来自张三', 512), ('2', '来自李四', 768)]
log = []

class FakePOP(socketserver.StreamRequestHandler):
    def w(self, line):
        self.wfile.write((line + '\r\n').encode('utf-8'))
        log.append(('S', line))

    def handle(self):
        self.w('+OK fake pop3 ready')
        while True:
            line = self.rfile.readline()
            if not line:
                return
            s = line.decode().rstrip('\r\n')
            log.append(('C', s))
            up = s.upper()
            if up.startswith('USER'):
                self.w('+OK user accepted')
            elif up.startswith('PASS'):
                self.w('+OK maildrop has %d messages' % len(box))
            elif up == 'STAT':
                self.w('+OK %d %d' % (len(box), sum(b[2] for b in box)))
            elif up == 'LIST':
                self.w('+OK %d messages' % len(box))
                for no, subj, n in box:
                    self.w('%s %d' % (no, n))
                self.w('.')
            elif up.startswith('RETR'):
                no = s.split()[1]
                hit = [b for b in box if b[0] == no][0]
                self.w('+OK %d octets' % hit[2])
                self.w('Subject: ' + hit[1])
                self.w('')
                self.w('this is the body of message ' + no)
                self.w('.')
            elif up.startswith('DELE'):
                self.w('+OK message deleted')
            elif up == 'QUIT':
                self.w('+OK bye')
                return

c = poplib.POP3('127.0.0.1', port, timeout=5)
c.user('zhangsan')
c.pass_('secret')
print(c.stat(), c.list())
print(c.retr(1)[1])
c.dele(1)
c.quit()

实跑输出:

--------------------------------------------------------------------
POP3 的「下载并删除」,为什么两台电脑会打架
--------------------------------------------------------------------
**POP3 是「把我邮箱里的信全搬到本机」**:**收完就删,服务器只当一个中转柜。**
**下面起一台假 POP3 服务,用真的客户端库收一次信。**

**完整对话**:

    S  +OK fake pop3 ready
    C  USER zhangsan
    S  +OK user accepted
    C  PASS secret
    S  +OK maildrop has 2 messages
    C  STAT
    S  +OK 2 1280
    C  LIST
    S  +OK 2 messages
    S  1 512
    S  2 768
    S  .
    C  RETR 1
    S  +OK 512 octets
    S  Subject: 来自张三
    S  
    S  this is the body of message 1
    S  .
    C  DELE 1
    S  +OK message deleted
    C  QUIT
    S  +OK bye

**C 是我发的命令,S 是服务器答的**,**一问一答严格配对,这就是 POP3 的全部节奏。**
**服务器说有 2 封、共 1280 字节。**
**`RETR 1` 收回来 3 行正文。**
**注意 `STAT` 和 `LIST` 的区别:一个报总数,一个逐条报大小。**
**`LIST` 的结尾是一个单独的点,和 SMTP 一样是「带内信令」。**

**POP3 只有三个阶段**:

  阶段         命令                       注意什么
  -------------------------------------------------------------
  **授权**     USER 与 PASS               **这时还不能动邮件**
  **事务**     STAT、LIST、RETR、DELE     **收信和标记删除**
  **更新**     QUIT                       **删掉的这时才真删**

**第三行才是关键**:**DELE 只是「打个删除标记」,真正删要等 QUIT。**
**所以中途断线,那些标记全部作废**,信一封都不会丢。**这是 POP3 设计里少见的「安全默认值」。**

**但它有两个结构性缺陷**:**一是「收完就删」意味着第二台设备看不到第一台收过的信;**
**二是「状态在本地」意味着「已读」「标记」「文件夹」这些概念根本不存在。**
**很多客户端默认「服务器保留副本」,就是为了绕过第一个缺陷**——**代价是服务器空间一直被占着。**

整段对话二十二行,一问一答严格配对。 服务器说有 2 封、共 1280 字节,RETR 1 收回三行正文。 LIST 的结尾是一个单独的点,和 SMTP 里「正文以点结束」是同一个套路。 这是 1980 年代文本协议的共同习惯:需要一个「后面没有了」的信号, 于是拿一个不可能出现在数据里的短符号来当它。 再看 DELE 1 之后:服务器只回了一句 +OK message deleted, 然后 QUIT 回 +OK bye。 真正删除发生在那句 bye 之前的那一瞬间。 如果客户端在 DELE 之后直接拔网线,那封信还在。

30 秒实验

  1. 想一下:STAT 和 LIST 分别适合什么场景?
  2. 想一下:为什么 DELE 要延迟到 QUIT 才生效?
  3. 想一下:POP3 里「已读」这个状态存在哪儿?

常见误区

  1. 以为「POP3 删了就没了」。 DELE 只打标记,QUIT 才真删。
  2. 以为「POP3 只是 IMAP 的老版本」。 它们是两种不同的模型,一个搬走、一个遥控。
  3. 以为「保留服务器副本是协议特性」。 那是客户端的设置,不是协议规定的。
自测三问(点开对答案) 1. POP3 的三个阶段是什么? —— **授权、事务、更新。** 2. DELE 之后邮件立刻消失吗? —— **不会**,要到 QUIT 进入更新阶段才真删。 3. POP3 的两个结构性缺陷是什么? —— **收完就删导致多设备不同步**;**状态在本地导致换机全丢。**

下一节看 IMAP 怎么把状态搬回服务器。

相关笔记