IMAP 原理与报文 — 四状态机、命令集与 FETCH 语法

IMAP 四状态模型、标签命令与三类响应、标志与 UID/UIDVALIDITY 体系、FETCH 数据项、SEARCH 与 IDLE 机制

先建立直觉

IMAP 干的事情是:让你远程操作放在服务器上的那堆信,而不是把信搬回家

想象一个云端的书房:书一直在书房里,你在手机上贴的书签、分的类、扔进废纸篓的动作,全都是在书房里真实发生的。所以换台电脑打开,看到的还是同一个书房、同样的摆放。

它还很省事——你可以只把书的目录页调出来看看,觉得有兴趣再把某一章单独取出来读,不必把整本书都搬过来。

它解决什么问题

没有 IMAP 会怎样?在手机上收了信,回到电脑上就找不着了;电脑上辛苦分好的文件夹,手机上完全不存在;一封信在这台标了已读,在那台还是未读。这正是 POP3"下载即搬走"模型在多设备时代的困境。

IMAP 针对性地解决四件事:

  1. 多端状态一致:已读、星标、回复过等标志存在服务器,任一端修改,全端同步。
  2. 服务器端文件夹:支持创建、重命名、订阅任意层级的邮箱,归档结构对所有客户端可见。
  3. 按需获取(Partial Fetch):可以只取头部渲染列表、只取某个 MIME 段下载单个附件,不必整封拉下来。
  4. 服务器端搜索与推送SEARCH 在服务端完成检索,IDLE 让服务器主动推送新邮件通知。

工作流程(简化版)

一次 IMAP 使用,用大白话说是 5 步:

  1. 登录:连上服务器、报账号密码,进门。
  2. 看有哪些文件夹:让服务器列一下你的收件箱、已发送、归档等目录。
  3. 选中一个文件夹:告诉服务器"我现在要操作收件箱",服务器回报里面有多少封、版本号是多少。
  4. 按需取内容:先只拿标题和发件人渲染列表,点开某封才拿正文,要下附件才单独拿那一段。
  5. 改状态 / 挂机等新信:把"已读"“移到归档"这些动作写回服务器;然后挂起等待,有新信服务器会主动通知你。

IMAP 核心模型:服务器持有邮件与全部元数据,客户端用命令远程操作。与 POP3"拉走就完事"不同,IMAP 的已读、移动、删除都要写回服务器,故天然多设备同步。

四状态模型

1
2
TCP/TLS → 未认证 Not Authenticated →LOGIN→ 已认证 Authenticated
        →SELECT/EXAMINE→ 已选择 Selected →LOGOUT/CLOSE→ 注销 Logout
状态进入可做什么
未认证连接建立(或 PREAUTH能力协商、加密升级、登录
已认证LOGIN/AUTHENTICATE 成功文件夹增删改查、查询未读数、APPEND
已选择SELECT(读写)/EXAMINE(只读)抓邮件、改标志、搜索、复制、移动、清除
注销LOGOUT/服务器 BYE连接关闭

EXAMINESELECT 唯一区别:前者只读打开,不会自动置 \Seen——即"预览不标已读"的实现。

标签(Tag)与流水线

每条命令必须以唯一标签开头,服务器最终响应带同一标签。因标签配对,客户端可不等响应连发多条(流水线),服务器可乱序返回,高延迟链路效率远高于 POP3。

1
2
3
4
C: a001 SELECT INBOX
S: * 172 EXISTS
S: * OK [UIDVALIDITY 1690000000]
S: a001 OK [READ-WRITE] SELECT completed ← 带标签最终响应

报文 / 头部长什么样

三类响应

看这张表前先记住:看到行首那个字符就知道这行是什么——有标签的是"这条命令的最终结果”,* 开头的是服务器主动告诉你的变化,+ 开头的是"你继续说"。

类型前缀含义
带标签响应<tag> OK/NO/BADOK 成功;NO 命令合法但业务失败;BAD 语法/状态错误
未标记响应*推送数据/事件:* 5 EXISTS* 2 EXPUNGE* BYE
续行请求+要求客户端继续(字面量、AUTHENTICATE 质询、IDLE 就绪)

NOBAD 是排错关键:NO=命令写对但业务失败(如 NO Mailbox doesn't exist);BAD=语法错或在错误状态下发命令。

核心命令集(RFC 3501)

看这份清单前先记住:命令是按状态分组的——没登录只能做协商和登录,没 SELECT 文件夹就不能碰邮件,在错误状态下发命令直接得 BAD

任意状态CAPABILITY(能力)、NOOP(触发推送)、LOGOUT未认证STARTTLSLOGIN 用户 口令AUTHENTICATE 机制(PLAIN/LOGIN/XOAUTH2)。 已认证SELECT/EXAMINECREATE/DELETE/RENAMELIST 参考 通配% 匹配一层,* 全部)、SUBSCRIBE/LSUBSTATUS 邮箱 (MESSAGES UNSEEN UIDNEXT UIDVALIDITY)APPEND 邮箱 [标志] {字节}已选择FETCH/UID FETCHSTORE ±FLAGS (.SILENT 抑制回显)SEARCH(或 UID SEARCH)、COPY/MOVE(RFC 6851 原子移动)、EXPUNGECLOSE(隐式 EXPUNGE)、UNSELECTIDLE(RFC 2177 推送)。

命令英文全称对照:CAPABILITY(能力查询)、NOOP = No Operation(空操作/触发推送)、LOGOUT(注销)、STARTTLS(启动加密)、AUTHENTICATE(SASL 认证)、SELECT(选择邮箱-读写)、EXAMINE(选择邮箱-只读)、CREATE/DELETE/RENAME(建/删/改名邮箱)、LIST(列出邮箱)、SUBSCRIBE/LSUB = List Subscribed(订阅/列出已订阅)、STATUS(邮箱状态)、APPEND(追加邮件)、FETCH(抓取内容)、STORE(写标志)、SEARCH(服务端搜索)、COPY(复制)、MOVE(移动)、EXPUNGE(清除已删标记的邮件)、CLOSE(关闭邮箱)、UNSELECT(取消选择)、IDLE(挂起等待推送)。

系统标志与编号体系

看这两张表前先记住:标志决定"这封信长什么状态",编号决定"怎么指认这封信"——后者是 IMAP 编程最容易翻车的地方。

系统标志含义
\Seen / \Answered / \Flagged / \Draft已读 / 已回复 / 星标 / 草稿
\Deleted待删除(等 EXPUNGE
\Recent本会话首现新邮件(会话级,rev2 已废弃)
\*(PERMANENTFLAGS 中)允许自定义关键字如 $Junk
概念说明
序号1..N 连续编号,EXPUNGE 会令后续序号前移
UID文件夹内单调递增永不复用,删其他邮件不影响它
UIDVALIDITY文件夹版本号,SELECT 返回;不变则缓存有效,变了须清空全量重同步
UIDNEXT下一封新邮件 UID,用于判断是否新信

英文全称对照:UID = Unique Identifier(唯一标识符)、UIDVALIDITY(UID 有效性/文件夹版本号)、UIDNEXT(下一个可用 UID)、\Seen(已读)、\Answered(已回复)、\Flagged(已标星)、\Draft(草稿)、\Deleted(已标记删除)、\Recent(最近新到)。

增量同步经典写法:记录上次 UIDVALIDITY 与最大 UID,本次 SELECT 先比对,一致则 UID FETCH <lastUID+1>:* ... 只取新信。

FETCH 数据项

看这张表前先记住:FETCH 就是一份"点菜单"——你点什么它给什么,点得越少流量越省。手机客户端之所以省流量,全靠只点前几项。

数据项返回
FLAGS / UID / INTERNALDATE / RFC822.SIZE标志 / UID / 服务器收信时间 / 字节数
ENVELOPE结构化信封(日期/主题/From/To/Message-ID),不下正文
BODYSTRUCTURE完整 MIME 树,决定下载哪一段
BODY[HEADER.FIELDS (FROM SUBJECT DATE)]仅指定头部(列表渲染最省流量)
BODY[1]/BODY[2]第 n 个 MIME 段(只下某附件)
BODY.PEEK[...]BODY[...]不置 \Seen(预览必用)
RFC822整封原始邮件

数据项中文含义:FLAGS(标志集合)、INTERNALDATE(服务器内部收信时间)、RFC822.SIZE(邮件字节数)、ENVELOPE(结构化信封)、BODYSTRUCTURE(正文 MIME 结构树)、BODY(正文段)、BODY.PEEK(偷看正文段,不改已读标志)。

1
2
3
a005 UID FETCH 1:* (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)
a006 UID FETCH 1024 (BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])
a008 UID FETCH 1024 (BODY.PEEK[2]) ← 只下第 2 段附件

SEARCHUID SEARCH UNSEEN/FROM "x"/SUBJECT "发票"/SINCE 1-Aug-2026/LARGER 5000000,多条件为「与」,用 OR/NOT 前缀表「或/非」,全文须 CHARSET UTF-8 TEXT "合同"

交互时序

一句话看懂这张图:登录 → 看文件夹 → 选中收件箱 → 先取元数据渲染列表、再按需取正文 → 改标志和移动都写回服务器 → 最后挂 IDLE 等新信主动推过来。注意带 a00x 标签的行是"这条命令的结果",* 开头的行是服务器推给你的数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sequenceDiagram
    autonumber
    participant C as IMAP 客户端
    participant S as IMAP 服务器
    Note over C,S: TCP+TLS(端口 993)
    S->>C: * OK [CAPABILITY IMAP4rev1 IDLE UIDPLUS MOVE] Ready
    C->>S: a001 LOGIN alice ********
    S-->>C: a001 OK Logged in
    C->>S: a002 LIST "" "*"
    S-->>C: * LIST (\HasNoChildren) "/" "INBOX"<br/>* LIST (\Sent) "/" "Sent"<br/>a002 OK
    C->>S: a003 STATUS INBOX (MESSAGES UNSEEN UIDNEXT UIDVALIDITY)
    S-->>C: * STATUS INBOX (MESSAGES 172 UNSEEN 3 UIDNEXT 2051 UIDVALIDITY 1690000000)
    C->>S: a004 SELECT INBOX
    S-->>C: * 172 EXISTS<br/>* FLAGS (...)<br/>* OK [UIDVALIDITY 1690000000]<br/>a004 OK [READ-WRITE]
    C->>S: a005 UID SEARCH UNSEEN
    S-->>C: * SEARCH 2048 2049<br/>a005 OK
    C->>S: a006 UID FETCH 2048:2050 (FLAGS ENVELOPE RFC822.SIZE)
    S-->>C: * 170 FETCH (UID 2048 FLAGS () ENVELOPE (...) SIZE 8421)<br/>a006 OK
    Note right of C: 只取元数据渲染列表
    C->>S: a007 UID FETCH 2048 (BODY.PEEK[1])
    S-->>C: * 170 FETCH (BODY[1] {512} 正文...)<br/>a007 OK
    Note right of C: PEEK 不置 Seen
    C->>S: a008 UID STORE 2048 +FLAGS (\Seen)
    S-->>C: * 170 FETCH (UID 2048 FLAGS (\Seen))<br/>a008 OK
    Note right of S: 标志写回服务器,他端立即可见
    C->>S: a009 UID MOVE 2049 "Archive"
    S-->>C: a009 OK Move completed
    C->>S: a010 IDLE
    S-->>C: + idling
    S-->>C: * 173 EXISTS
    Note left of S: 新邮件主动推送
    C->>S: DONE
    S-->>C: a010 OK IDLE terminated
    C->>S: a011 LOGOUT
    S-->>C: * BYE<br/>a011 OK

关键机制 / 变体

  • IDLE 准实时推送(RFC 2177)——这是让你手机能"叮"一声收到新邮件而不用每分钟去问一遍:客户端进等待态,服务器有变化主动发未标记响应。须每≤29分钟重发(NAT/防火墙会回收长连接),且 IDLE 只作用于当前 SELECT 的文件夹,监听多文件夹需多条连接。
  • 删除两阶段与 MOVE——这是为了给"删错了"留后悔的余地,并让移动操作不会半途出错留下两份STORE +FLAGS (\Deleted) 只打标记,EXPUNGE 才真删;多数客户端"删除"实为 MOVE 到废纸篓,MOVE(RFC 6851)原子操作,优于 COPY+STORE+EXPUNGE 三步(中断会重复)。
  • UIDVALIDITY 变更——这是服务器在告诉你**“这个文件夹已经不是原来那个了,你手上的缓存全作废”**:若与本地缓存不同,文件夹已非同一(重建/迁移/修索引),所有缓存 UID 失效,须清空全量重同步——“换服务器后邮件全部重下"的技术原因。
  • 字面量语法——这是为了让含换行的大段数据也能在行式协议里安全传输:含 CRLF 的数据用 {字节数} 声明后跟原文,LITERAL+ 允许 {n+} 不等续行省 RTT。
  • 认证加密——这决定了你的账号口令是不是在裸奔STARTTLS(143 显式,后重 CAPABILITY)、隐式 TLS(993,RFC 8314 推荐);LOGIN/AUTHENTICATE PLAIN 明文须 TLS 内;XOAUTH2 是 Gmail/365 主推。授权码同 SMTP/POP3。

常见误区

  1. “命令可以像 POP3 那样直接敲” —— 不行。IMAP 每条命令必须带唯一标签,忘了加标签服务器只会回 BAD,且看不出是哪里错。
  2. “序号和 UID 差不多,随便用哪个” —— 差很多。序号会因 EXPUNGE 而整体前移,跨命令引用就会指错信;UID 在同一 UIDVALIDITY 下永不变,编程一律用 UID FETCH / UID STORE
  3. “点开预览了但不想标已读,客户端会自动处理” —— 不会。用 BODY[...] 抓取会隐式加上 \Seen,想预览不标已读必须用 BODY.PEEK[...],或用 EXAMINE 只读打开文件夹。
  4. "NOBAD 都是报错,一样看” —— 方向完全不同。NO 是命令写对了但业务不允许(如文件夹不存在),该查业务配置;BAD 是语法错或在错误状态下发命令,该查自己的命令怎么写的。

速记口诀

命令必带标签,星号是推送、加号要续行;OK 通过、NO 业务错、BAD 语法错。

认 UID 别认序号,预览用 PEEK 不留痕;删要两步 EXPUNGE 才算数,UIDVALIDITY 一变全重来。

知识框架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
mindmap
  root((IMAP))
    定位
      应用层邮件访问
      TCP 143 / 993 IMAPS
      RFC 3501 rev1 / 9051 rev2
      邮件留服务器
    四状态
      未认证 CAPABILITY LOGIN
      已认证 LIST STATUS APPEND
      已选择 FETCH STORE SEARCH MOVE IDLE
      注销 LOGOUT BYE
    响应模型
      标签命令流水线
      OK NO BAD
      星号未标记 +续行
      NO业务失败 BAD语法
    编号体系
      序号会重排
      UID 永久稳定
      UIDVALIDITY 缓存版本
      UIDNEXT 判新信
    标志
      Seen Answered Flagged
      Deleted Draft
      自定义关键字
    FETCH
      ENVELOPE 结构化
      BODYSTRUCTURE MIME树
      BODY.PEEK 不置已读
      BODY段号 按需下附件
    机制
      IDLE 推送 29分钟重发
      删除两阶段 MOVE原子
      字面量 花括号
    对比POP3
      同步 vs 搬走
      多文件夹 vs 仅INBOX