IMAP实现讲解
1. 邮件格式内容示例
下面展示了一封存储在数据库中的邮件原始内容示例(包括头部和正文):
Message-ID: <ad5355e1-7be5-4fdb-aacc-1b992a67180b@tio.com>
Date: Wed, 25 Jun 2025 23:45:07 -1000
MIME-Version: 1.0
User-Agent: default/1.0
Content-Type: text/plain; charset=UTF-8
From: user1@tio.com
To: user2@tio.com
Subject: hi
hi
- Message-ID:全局唯一标识符,由发件端或服务器生成。
- Date:邮件发送或接收时间,含时区偏移。
- MIME-Version、Content-Type:表明邮件的多媒体或字符集格式。
- From/To/Subject:标准头部字段,分别指发件人、收件人和主题。
- 空行 之后即为邮件正文(本例中仅为简单的
hi
)。
2. 获取一个邮箱中所有未删除的邮件(含聚合标志)
要列出某个 mailbox 下所有“未删除”邮件,并将每封邮件的 flag 聚合为一个数组,可使用如下 SQL:
--# mailbox.getActiveMessages
SELECT
m.id,
m.uid,
m.internal_date,
msg.raw_content,
msg.size_in_bytes,
COALESCE(
(SELECT ARRAY_AGG(f.flag) FROM mw_mail_flag f WHERE f.mail_id = m.id),
'{}'
) as flags
FROM
mw_mail m
JOIN
mw_mail_message msg ON m.message_id = msg.id
WHERE
m.mailbox_id = 100101
AND m.deleted = 0
AND NOT EXISTS (
SELECT 1
FROM mw_mail_flag f
WHERE f.mail_id = m.id
AND f.flag = '\Deleted'
)
ORDER BY
m.uid ASC;
过滤条件
m.deleted = 0
:排除标记为已删除的记录。NOT EXISTS … '\Deleted'
:排除被客户通过 STORE +FLAGS (\Deleted) 标记的邮件。
聚合 flags
- 子查询
(SELECT ARRAY_AGG(f.flag) …)
:将同一邮件的所有 flag 聚合为数组。 COALESCE(..., '{}')
:如果没有任何 flag,则返回空数组{}
。
- 子查询
排序
- 按
uid
升序排列,保证客户端读到的列表顺序一致。
- 按
Java 中读取 flags
执行上述 SQL 后,可通过 ActiveRecord 的 Db.find
拿到若干行 Row
,每行中:
List<Row> mailRows = Db.find(finalSql);
对 Row
里 flags
列的读取方式:
String[] flagsObj = row.get("flags");
String[] flagsArray = row.getStringArray("flags");
- 这两种写法都可正确将数据库的
text[]
转为 Java 的String[]
。
3. 带序号(seq_num)和聚合 flags 的查询
为了在 IMAP FETCH 响应中返回 sequence number(临时序号),需要在 SQL 里先给每封未删除邮件打一个按 UID 排序的行号,再和正文表与 flag 表关联。示例如下:
SELECT
ranked.id,
ranked.uid,
ranked.internal_date,
msg.raw_content,
msg.size_in_bytes,
ranked.seq_num,
COALESCE(
ARRAY_AGG(mf.flag) FILTER (WHERE mf.flag IS NOT NULL),
'{}'
) AS flags
FROM (
-- 子查询:为未删除邮件按 uid 升序计算临时序号 seq_num
SELECT
m.id,
m.uid,
m.internal_date,
m.message_id,
ROW_NUMBER() OVER (ORDER BY m.uid ASC) AS seq_num
FROM mw_mail m
WHERE m.mailbox_id = 100201
AND NOT EXISTS (
SELECT 1
FROM mw_mail_flag del_mf
WHERE del_mf.mail_id = m.id
AND del_mf.flag = '\Deleted'
)
) AS ranked
JOIN mw_mail_message msg
ON ranked.message_id = msg.id
LEFT JOIN mw_mail_flag mf
ON ranked.id = mf.mail_id
GROUP BY
ranked.id,
ranked.uid,
ranked.internal_date,
msg.raw_content,
msg.size_in_bytes,
ranked.seq_num
ORDER BY
ranked.uid ASC;
3.1 子查询 ranked
- 过滤指定
mailbox_id
下的所有未删除邮件。 - 用
ROW_NUMBER() OVER (ORDER BY m.uid ASC)
为每条记录生成一个 临时序号seq_num
,从 1 开始自增。
3.2 主查询
- 关联正文表:通过
message_id
联到mw_mail_message
,读取raw_content
与size_in_bytes
。 - 左连接 flags:从
mw_mail_flag
拿到可能的多行 flag,使用ARRAY_AGG
聚合,并在NULL
时返回{}
。 - 分组:对除
flags
以外的所有非聚合字段进行GROUP BY
。 - 排序:最终按
uid
升序输出,保证输出列表顺序稳定。
4. 返回结果的字段含义
列名 | 示例值 | 含义 |
---|---|---|
id | 527874583000649728 | 数据库主键,内部唯一标识此邮件。 |
uid | 1 | IMAP 持久 UID,在同一 mailbox 中全局唯一且递增。 |
internal_date | 2025-06-26 17:45:08.037+08 | 服务器接收邮件的时间戳,带时区信息。 |
raw_content | 上述完整 MIME 文本 | 邮件头与正文的原始 MIME 格式内容。 |
size_in_bytes | 241 | 原始文本字节长度,用于 RFC822.SIZE 报文字段。 |
seq_num | 1 | 临时序号:本次查询中该邮件是第 1 条。 |
flags | {"\\Recent"} | 聚合后的 flag 数组,如 \Seen 、\Flagged 、\Deleted 等。 |
- 临时序号(seq_num) 用于 IMAP 客户端在 FETCH 响应行首标注。
- UID 则是该邮件的长期标识,客户端可用它执行幂等操作。
5. IMAP 中的 Message-ID、UID 与 seq_num 区别
Message-ID
- 定义于邮件头部,是客户端或服务器生成的全局唯一字符串,用以邮件去重与线程关联。
- 格式形如
<随机串@域名>
,不参与 IMAP 序号或 UID 分配。
UID(唯一标识符)
- 持久存在于同一 mailbox 内,由服务器分配且永不重用。
- 用于客户端跨会话、多次操作时的稳定定位(如
UID FETCH 123 (FLAGS)
)。
seq_num(序列号/临时序号)
仅在当前 VIEW(邮件列表)中有效。
随着新邮件插入、邮件删除或 EXPUNGE,
seq_num
会重新编号。在所有 FETCH 响应行首必须显示(协议要求),格式为:
* <seq_num> FETCH (UID <uid> FLAGS (…) …)
6. 能否用 UID 代替 seq_num?
协议规范:
- 所有 FETCH 响应行都 必须 以当前的 sequence number 开头。
UID FETCH
命令中,客户端用 UID 指定要操作的邮件集,但响应里仍然要返回 sequence number。
两者语义不同:
uid
是持久标识;seq_num
是临时定位。
如果直接用 UID 当作 seq_num:
- 当列表变动时(如新邮件到达、EXPUNGE),“第 5 封邮件”在客户端看来可能成了 UID=7 的邮件,导致客户端内部命令错位。
- 会严重破坏 IMAP 客户端与服务器之间的状态同步。
结论:不能简单用 UID 代替 seq_num;两者都要维护并分别返回,才能完全符合 IMAP 协议并保证客户端正常工作。