Skip to content

主动推送 ​

服务端收到新邮件且客户端在线时,主动将邮件推送到客户端

报文 ​

服务端再收到新邮件后向客户端发送的报文内容如下

* 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

这样的无标签推送了。

代码实现 ​

java
package com.tio.mail.wing.service;

import java.util.List;
import java.util.Set;

import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.db.IAtom;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
import nexus.io.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;
    }
  }
}

MIT Licensed