Skip to content

IMAP实现讲解 ​


1. 邮件格式内容示例 ​

下面展示了一封存储在数据库中的邮件原始内容示例(包括头部和正文):

text
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:

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,每行中:

java
List<Row> mailRows = Db.find(finalSql);

对 Row 里 flags 列的读取方式:

java
String[] flagsObj   = row.get("flags");
String[] flagsArray = row.getStringArray("flags");
  • 这两种写法都可正确将数据库的 text[] 转为 Java 的 String[]。

3. 带序号(seq_num)和聚合 flags 的查询 ​

为了在 IMAP FETCH 响应中返回 sequence number(临时序号),需要在 SQL 里先给每封未删除邮件打一个按 UID 排序的行号,再和正文表与 flag 表关联。示例如下:

sql
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 主查询 ​

  1. 关联正文表:通过 message_id 联到 mw_mail_message,读取 raw_content 与 size_in_bytes。
  2. 左连接 flags:从 mw_mail_flag 拿到可能的多行 flag,使用 ARRAY_AGG 聚合,并在 NULL 时返回 {}。
  3. 分组:对除 flags 以外的所有非聚合字段进行 GROUP BY。
  4. 排序:最终按 uid 升序输出,保证输出列表顺序稳定。

4. 返回结果的字段含义 ​

列名示例值含义
id527874583000649728数据库主键,内部唯一标识此邮件。
uid1IMAP 持久 UID,在同一 mailbox 中全局唯一且递增。
internal_date2025-06-26 17:45:08.037+08服务器接收邮件的时间戳,带时区信息。
raw_content上述完整 MIME 文本邮件头与正文的原始 MIME 格式内容。
size_in_bytes241原始文本字节长度,用于 RFC822.SIZE 报文字段。
seq_num1临时序号:本次查询中该邮件是第 1 条。
flags{"\\Recent"}聚合后的 flag 数组,如 \Seen、\Flagged、\Deleted 等。
  • 临时序号(seq_num) 用于 IMAP 客户端在 FETCH 响应行首标注。
  • UID 则是该邮件的长期标识,客户端可用它执行幂等操作。

5. IMAP 中的 Message-ID、UID 与 seq_num 区别 ​

  1. Message-ID

    • 定义于邮件头部,是客户端或服务器生成的全局唯一字符串,用以邮件去重与线程关联。
    • 格式形如 <随机串@域名>,不参与 IMAP 序号或 UID 分配。
  2. UID(唯一标识符)

    • 持久存在于同一 mailbox 内,由服务器分配且永不重用。
    • 用于客户端跨会话、多次操作时的稳定定位(如 UID FETCH 123 (FLAGS))。
  3. seq_num(序列号/临时序号)

    • 仅在当前 VIEW(邮件列表)中有效。

    • 随着新邮件插入、邮件删除或 EXPUNGE,seq_num 会重新编号。

    • 在所有 FETCH 响应行首必须显示(协议要求),格式为:

      * <seq_num> FETCH (UID <uid> FLAGS (…) …)

6. 能否用 UID 代替 seq_num? ​

  • 协议规范:

    1. 所有 FETCH 响应行都 必须 以当前的 sequence number 开头。
    2. UID FETCH 命令中,客户端用 UID 指定要操作的邮件集,但响应里仍然要返回 sequence number。
  • 两者语义不同:

    • uid 是持久标识;
    • seq_num 是临时定位。
  • 如果直接用 UID 当作 seq_num:

    • 当列表变动时(如新邮件到达、EXPUNGE),“第 5 封邮件”在客户端看来可能成了 UID=7 的邮件,导致客户端内部命令错位。
    • 会严重破坏 IMAP 客户端与服务器之间的状态同步。

结论:不能简单用 UID 代替 seq_num;两者都要维护并分别返回,才能完全符合 IMAP 协议并保证客户端正常工作。


MIT Licensed