主动推送
服务端收到新邮件且客户端在线时,主动将邮件推送到客户端
报文
服务端再收到新邮件后向客户端发送的报文内容如下
* 9 EXISTS
* 1 RECENT
DONE
77 OK Idle completed (39.120 + 39.117 + 39.119 secs).
78 noop
78 OK NOOP completed (0.001 + 0.000 secs).
79 UID fetch 9:* (FLAGS)
* 9 FETCH (UID 9 FLAGS (\Recent))
79 OK Fetch completed (0.001 + 0.000 secs).
80 UID fetch 9 (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (From To Cc Bcc Subject Date Message-ID Priority X-Priority References Newsgroups In-Reply-To Content-Type Reply-To)])
* 9 FETCH (UID 9 RFC822.SIZE 625 FLAGS (\Recent) BODY[HEADER.FIELDS (FROM TO CC BCC SUBJECT DATE MESSAGE-ID PRIORITY X-PRIORITY REFERENCES NEWSGROUPS IN-REPLY-TO CONTENT-TYPE REPLY-TO)] {232}
Message-ID: <71e34fa0-a5b4-40d9-9e18-b665443751ff@localdomain>
Date: Sun, 29 Jun 2025 15:47:17 -1000
To: bob@localdomain
From: alice <alice@localdomain>
Subject: notify
Content-Type: text/plain; charset=UTF-8; format=flowed
)
80 OK Fetch completed (0.002 + 0.000 + 0.001 secs).
81 UID fetch 9 (UID RFC822.SIZE BODY.PEEK[])
* 9 FETCH (UID 9 RFC822.SIZE 625 BODY[] {625}
Return-Path: <alice@localdomain>
X-Original-To: bob@localdomain
Delivered-To: bob@localdomain
Received: from [192.168.3.8] (unknown [192.168.3.8])
by mail.localdomain (Postfix) with ESMTPA id 00FC46C5445
for <bob@localdomain>; Sun, 29 Jun 2025 15:47:44 -1000 (HST)
Message-ID: <71e34fa0-a5b4-40d9-9e18-b665443751ff@localdomain>
Date: Sun, 29 Jun 2025 15:47:17 -1000
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Content-Language: en-US
To: bob@localdomain
From: alice <alice@localdomain>
Subject: notify
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
notify
)
81 OK Fetch completed (0.001 + 0.000 secs).
82 UID fetch 9 (UID BODY.PEEK[HEADER.FIELDS (Content-Type Content-Transfer-Encoding)] BODY.PEEK[TEXT]<0.2048>)
* 9 FETCH (UID 9 BODY[HEADER.FIELDS (CONTENT-TYPE CONTENT-TRANSFER-ENCODING)] {91}
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
BODY[TEXT]<0> {10}
notify
)
82 OK Fetch completed (0.001 + 0.000 secs).
83 IDLE
+ idling
报文讲解
* 9 EXISTS
* 1 RECENT
是服务端在 IDLE 状态下“主动”发给客户端的无标签(untagged)通知,用来告诉客户端:当前信箱里共有 9 封邮件,其中 1 封是新近到达的。
详细来说,按照 IMAP 协议(RFC 3501)和其 IDLE 扩展(RFC 2177)的约定:
当客户端发出
IDLE
命令并收到服务器的+ idling
响应后,服务器会进入一个“监听”状态。在此状态中,一旦有新的邮件到达,服务器就会立刻发送类似
* <exists> EXISTS * <recent> RECENT
的无标签响应,通知客户端信箱状态的变化。
客户端接收到这些通知之后,可以选择发
DONE
来结束 IDLE,再通过FETCH
、NOOP
等命令去拉取新邮件的详情;也可以继续保持 IDLE,等待更多更新。
添加IDLE扩展
要支持服务器主动在客户端 IDLE 时推送新邮件通知,你的 CAPABILITY 响应里必须声明对 IDLE 扩展的支持。也就是说,在处理客户端 CAPABILITY
请求时,返回的功能列表中要包含一个 IDLE
标记。例如:
* CAPABILITY IMAP4rev1 AUTH=PLAIN IDLE UIDPLUS LITERAL+ SORT THREAD=REFERENCES
A001 OK CAPABILITY completed.
只要把 IDLE
加进去,客户端就知道服务器支持 RFC 2177 的 IDLE 命令,后续就能进入 IDLE 状态并接收类似
* 9 EXISTS
* 1 RECENT
这样的无标签推送了。
代码实现
package com.tio.mail.wing.service;
import java.util.List;
import java.util.Set;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.db.IAtom;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.utils.lock.SetWithLock;
import com.tio.mail.wing.config.ImapServerConfig;
import com.tio.mail.wing.consts.MailBoxName;
import com.tio.mail.wing.model.Email;
import com.tio.mail.wing.model.MailRaw;
import com.tio.mail.wing.packet.ImapPacket;
import com.tio.mail.wing.utils.MailRawUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MailSaveService {
private MailService mailService = Aop.get(MailService.class);
private MwUserService mwUserService = Aop.get(MwUserService.class);
private MailBoxService mailBoxService = Aop.get(MailBoxService.class);
public boolean saveEmail(String toUser, MailRaw mail) {
String rawContent = MailRawUtils.toRawContent(mail);
return this.saveEmail(toUser, rawContent);
}
/**
* [兼容SMTP] 将接收到的邮件保存到指定用户的收件箱(INBOX)中。
*/
public boolean saveEmail(String username, String rawContent) {
return saveEmailInternal(username, MailBoxName.INBOX, rawContent);
}
public boolean saveEmail(String toUser, String mailBoxName, MailRaw mail) {
String rawContent = MailRawUtils.toRawContent(mail);
return this.saveEmailInternal(toUser, mailBoxName, rawContent);
}
/**
* 内部核心的邮件保存方法。
* 优化:使用事务和原子性UID更新。
*/
public boolean saveEmailInternal(String username, String mailboxName, String rawContent) {
// 1. 获取用户和邮箱信息
Row user = mwUserService.getUserByUsername(username);
if (user == null) {
log.error("User '{}' not found. Cannot save email.", username);
return false;
}
Long userId = user.getLong("id");
Row mailbox = mailBoxService.getMailboxByName(userId, mailboxName);
if (mailbox == null) {
log.error("Mailbox '{}' not found for user '{}'.", mailboxName, username);
return false;
}
Long mailboxId = mailbox.getLong("id");
return saveEmailInternal(username, mailboxName, rawContent, userId, mailboxId);
}
private boolean saveEmailInternal(String username, String mailboxName, String rawContent, Long userId, Long mailboxId) {
IAtom atom = new MailSaveAtom(userId, username, mailboxId, mailboxName, rawContent);
try {
boolean result = Db.tx(atom);
if (!result) {
return result;
}
// 通知客户端
SetWithLock<ChannelContext> channelContexts = Tio.getByUserId(ImapServerConfig.serverTioConfig, userId.toString());
if (channelContexts == null) {
return true;
}
Set<ChannelContext> ctxs = channelContexts.getObj();
if (ctxs != null && ctxs.size() > 0) {
List<Email> all = mailService.getActiveMailFlags(mailboxId);
long exists = all.size();
int recent = 0;
for (Email e : all) {
Set<String> flags = e.getFlags();
if (flags.size() > 0) {
if (flags.contains("\\Recent")) {
recent++;
}
}
}
StringBuffer sb = new StringBuffer();
sb.append("* ").append(exists).append(" EXISTS").append("\r\n");
sb.append("* ").append(recent).append(" RECENT").append("\r\n");
ImapPacket imapPacket = new ImapPacket(sb.toString());
for (ChannelContext ctx : ctxs) {
//* 9 EXISTS
//* 1 RECENT
Tio.send(ctx, imapPacket);
}
}
return result;
} catch (Exception e) {
log.error("Error saving email for user '{}' in mailbox '{}'", username, mailboxName, e);
return false;
}
}
}