任务5:实现 IMAP 服务(数据库版本)
任务概述
本任务旨在基于 T-IO 框架和数据库后端,实现一个完整的 IMAP 服务。系统包含以下几部分:
- ImapPacket:用于表示网络数据包的简单封装。
- ImapSessionContext:会话级上下文,用于管理客户端状态、登录信息和当前选中邮箱。
- ImapServerAioListener:连接级监听器,负责发送欢迎消息及断开时日志记录。
- ImapServerAioHandler:核心处理器,负责协议报文的解码、编码与命令分发。
- ImapServerConfig:服务启动配置类,负责读取端口并启动 TioServer。
- ImapService:集中处理各类 IMAP 命令,和数据库(通过 ActiveRecord)交互,完成邮件列表、SELECT/FETCH/STORE/EXPUNGE 等操作。
1. ImapPacket
// src/main/java/com/tio/mail/wing/packet/ImapPacket.java
package com.tio.mail.wing.packet;
import com.litongjava.aio.Packet;
@SuppressWarnings("serial")
public class ImapPacket extends Packet {
private String line;
public ImapPacket(String line) {
this.line = line;
}
public String getLine() {
return line;
}
}
- 功能:继承自
com.litongjava.aio.Packet
,内部只保存一行文本。 - 用途:在 T-IO 的编解码流程中,承载单条 IMAP 协议行数据。
2. ImapSessionContext
package com.tio.mail.wing.handler;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ImapSessionContext {
public enum State {
/** 未认证 */
NON_AUTHENTICATED,
/** 等待 Base64 编码的用户名 */
AUTH_WAIT_USERNAME,
/** 等待 Base64 编码的密码 */
AUTH_WAIT_PASSWORD,
/** 已认证 */
AUTHENTICATED,
/** 已选择邮箱 */
SELECTED
}
private State state = State.NON_AUTHENTICATED;
private Long userId;
private String username;
private String selectedMailbox;
/**
* 用于暂存 AUTHENTICATE 命令的 tag,
* 以便在多步交互后能正确响应
*/
private String currentCommandTag;
}
State 枚举:生命周期从“未认证”到“已选择邮箱”五种状态。
字段:
state
:当前状态。userId
、username
:登录用户信息。selectedMailbox
:当前选中的邮箱名称。currentCommandTag
:AUTHENTICATE 多步交互时暂存客户端 tag。
3. ImapServerAioListener
package com.tio.mail.wing.listener;
import com.litongjava.aio.Packet;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.server.intf.ServerAioListener;
import com.tio.mail.wing.handler.ImapSessionContext;
import com.tio.mail.wing.packet.ImapPacket;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ImapServerAioListener implements ServerAioListener {
@Override
public void onAfterConnected(ChannelContext channelContext,
boolean isConnected,
boolean isReconnect) throws Exception {
if (isConnected) {
log.info("IMAP client connected: {}", channelContext.getClientNode());
channelContext.set("sessionContext", new ImapSessionContext());
// 发送欢迎消息
Tio.send(channelContext,
new ImapPacket("* OK tio-mail-wing IMAP4rev1 server ready \r\n"));
}
}
@Override
public void onBeforeClose(ChannelContext channelContext,
Throwable throwable,
String remark,
boolean isRemove) throws Exception {
log.info("IMAP client disconnected: {}", channelContext.getClientNode());
}
@Override public void onAfterDecoded(ChannelContext channelContext,
Packet packet,
int packetSize) throws Exception { }
@Override public void onAfterReceivedBytes(ChannelContext channelContext,
int receivedBytes) throws Exception { }
@Override public void onAfterSent(ChannelContext channelContext,
Packet packet,
boolean isSentSuccess) throws Exception { }
@Override public void onAfterHandled(ChannelContext channelContext,
Packet packet,
long cost) throws Exception { }
@Override public boolean onHeartbeatTimeout(ChannelContext channelContext,
Long interval,
int heartbeatTimeoutCount) {
return false;
}
}
- onAfterConnected:首次建立连接时,创建
ImapSessionContext
并发送 IMAP 欢迎行。 - onBeforeClose:连接断开时记录日志。
- 其余回调目前留空,可根据需要扩展心跳、流量统计、数据解码前后日志等。
4. ImapServerAioHandler
package com.tio.mail.wing.handler;
import java.nio.ByteBuffer;
import com.litongjava.aio.Packet;
import com.litongjava.db.activerecord.ActiveRecordException;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.core.TioConfig;
import com.litongjava.tio.core.exception.LengthOverflowException;
import com.litongjava.tio.core.exception.TioDecodeException;
import com.litongjava.tio.core.utils.ByteBufferUtils;
import com.litongjava.tio.server.intf.ServerAioHandler;
import com.tio.mail.wing.packet.ImapPacket;
import com.tio.mail.wing.service.ImapFetchService;
import com.tio.mail.wing.service.ImapService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ImapServerAioHandler implements ServerAioHandler {
private ImapService imapService = Aop.get(ImapService.class);
@Override
public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext ctx) throws TioDecodeException {
String charset = ctx.getTioConfig().getCharset();
String line = null;
try {
line = ByteBufferUtils.readLine(buffer, charset);
} catch (LengthOverflowException e) {
log.error("Line length overflow", e);
}
return line == null ? null : new ImapPacket(line);
}
@Override
public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext ctx) {
String charset = ctx.getTioConfig().getCharset();
ImapPacket imapPacket = (ImapPacket) packet;
try {
return ByteBuffer.wrap(imapPacket.getLine().getBytes(charset));
} catch (Exception e) {
log.error("Encoding error", e);
return null;
}
}
@Override
public void handler(Packet packet, ChannelContext ctx) throws Exception {
ImapPacket imapPacket = (ImapPacket) packet;
String line = imapPacket.getLine().trim();
ImapSessionContext session = (ImapSessionContext) ctx.get("sessionContext");
String username = session.getUsername();
if (username != null) {
log.info("user {} <<< {}", username, line);
} else {
log.info("<<< {}", line);
}
if (session.getState() == ImapSessionContext.State.AUTH_WAIT_USERNAME || session.getState() == ImapSessionContext.State.AUTH_WAIT_PASSWORD) {
String reply = imapService.handleAuthData(session, line);
if (reply != null) {
Tio.send(ctx, new ImapPacket(reply));
}
return;
}
String[] parts = line.split("\\s+", 3);
String tag = parts[0];
String command = parts.length > 1 ? parts[1].toUpperCase() : "";
String args = parts.length > 2 ? parts[2] : "";
String reply = null;
try {
switch (command) {
case "CAPABILITY":
reply = imapService.handleCapability(tag);
break;
case "ID":
reply = imapService.handleId(tag);
break;
case "IDLE":
reply = imapService.handleIdle();
break;
case "AUTHENTICATE":
reply = imapService.handleAuthenticate(session, tag, args);
break;
case "LOGIN":
reply = imapService.handleLogin(session, tag, args);
break;
case "LOGOUT":
reply = imapService.handleLogout(session, tag);
if (reply != null) {
Tio.send(ctx, new ImapPacket(reply));
}
Tio.close(ctx, "logout");
return;
case "CLOSE":
reply = imapService.handleClose(session, tag);
case "LIST":
reply = imapService.handleList(session, tag, args);
break;
case "LSUB":
reply = imapService.handleList(session, tag, args);
break;
case "CREATE":
reply = imapService.handleCreate(session, tag, args);
break;
case "SUBSCRIBE":
reply = imapService.handleSubscribe(tag);
break;
case "SELECT":
reply = imapService.handleSelect(session, tag, args);
break;
case "FETCH":
// 传递 isUidCommand = false
ImapFetchService imapFetchService = Aop.get(ImapFetchService.class);
reply = imapFetchService.handleFetch(session, tag, args, false);
break;
case "STORE":
// 传递 isUidCommand = false
reply = imapService.handleStore(session, tag, args, false);
break;
case "UID":
reply = imapService.handleUid(session, tag, args);
break;
case "NOOP":
reply = tag + " OK NOOP";
case "EXPUNGE":
reply = imapService.handleExpunge(session, tag);
break;
default:
reply = tag + " BAD Unknown or unimplemented command.\r\n";
}
} catch (Exception e) {
reply = tag + " BAD Internal server error.\r\n";
if (e instanceof ActiveRecordException) {
ActiveRecordException ae = (ActiveRecordException) e;
log.error("Error handling IMAP command:{},{},{}", line, ae.getSql(), ae.getParas(), e);
} else {
log.error("Error handling IMAP command: " + line, e);
}
}
if (reply != null) {
Tio.send(ctx, new ImapPacket(reply));
}
}
}
- decode/encode:逐行读写,保证 IMAP 协议的 “CRLF+行” 交互。
- handler:根据客户端发送的 tag、command、args 三部分,调用
ImapService
对应方法。 - 异常处理:对 DB 操作异常(
ActiveRecordException
)做日志输出,并统一返回 BAD 应答。
5. ImapServerConfig
package com.tio.mail.wing.config;
import java.io.IOException;
import com.litongjava.tio.server.ServerTioConfig;
import com.litongjava.tio.server.TioServer;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.tio.mail.wing.handler.ImapServerAioHandler;
import com.tio.mail.wing.listener.ImapServerAioListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ImapServerConfig {
public void startImapServer() {
ImapServerAioHandler serverHandler = new ImapServerAioHandler();
ImapServerAioListener serverListener = new ImapServerAioListener();
ServerTioConfig serverTioConfig = new ServerTioConfig("imap-server");
serverTioConfig.setServerAioHandler(serverHandler);
serverTioConfig.setServerAioListener(serverListener);
serverTioConfig.setHeartbeatTimeout(-1);
serverTioConfig.checkAttacks = false;
serverTioConfig.ignoreDecodeFail = true;
serverTioConfig.setWorkerThreads(4);
TioServer tioServer = new TioServer(serverTioConfig);
try {
int port = EnvUtils.getInt("mail.server.imap.port", 143);
tioServer.start(null, port);
log.info("Started IMAP server on port: {}", port);
} catch (IOException e) {
log.error("Failed to start IMAP server", e);
}
}
}
- EnvUtils:从环境变量或配置文件读取
mail.server.imap.port
,默认 143。 - TioServer:利用
ServerTioConfig
启动 TCP 服务,实现 IMAP 协议监听。
6. ImapService
package com.tio.mail.wing.service;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.litongjava.db.activerecord.Row;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.utils.base64.Base64Utils;
import com.tio.mail.wing.consts.MailBoxName;
import com.tio.mail.wing.handler.ImapSessionContext;
import com.tio.mail.wing.model.Email;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ImapService {
private final MwUserService userService = Aop.get(MwUserService.class);
private final MailboxService mailboxService = Aop.get(MailboxService.class);
/**
* EXPUNGE: 逻辑删除并通知客户端
*/
public String handleExpunge(ImapSessionContext session, String tag) {
String username = session.getUsername();
String mailbox = session.getSelectedMailbox();
StringBuilder sb = new StringBuilder();
// 查询所有待 EXPUNGE 的 seq_num
List<Integer> seqs = mailboxService.getExpungeSeqNums(username, mailbox);
// 逻辑删除数据
mailboxService.expunge(username, mailbox);
// 通知客户端
for (int seq : seqs) {
sb.append("* ").append(seq).append(" EXPUNGE").append("\r\n");
}
sb.append(tag).append(" OK EXPUNGE completed.").append("\r\n");
return sb.toString();
}
/**
* CREATE: 在数据库中创建新邮箱目录
*/
public String handleCreate(ImapSessionContext session, String tag, String args) {
String mailboxName = unquote(args);
mailboxService.createMailbox(session.getUsername(), mailboxName);
return tag + " OK CREATE completed." + "\r\n";
}
/**
* LIST: 从数据库中获取所有用户邮箱目录
*/
public String handleList(ImapSessionContext session, String tag, String args) {
String username = session.getUsername();
List<String> mailboxes = mailboxService.listMailboxes(username);
StringBuilder sb = new StringBuilder();
for (String m : mailboxes) {
if (m.equalsIgnoreCase(MailBoxName.TRASH)) {
sb.append("* LIST (\\HasNoChildren) \"/\" ").append("Trash").append("\r\n");
} else {
sb.append("* LIST (\\HasNoChildren) \"/\" ").append(m).append("\r\n");
}
}
sb.append(tag).append(" OK LIST completed.").append("\r\n");
return sb.toString();
}
public String handleSubscribe(String tag) {
return tag + " OK SUBSCRIBE" + "\r\n";
}
public String handleCapability(String tag) {
StringBuilder sb = new StringBuilder();
sb.append("* CAPABILITY IMAP4rev1 AUTH=LOGIN IDLE UIDPLUS ID LITERAL+ MOVE").append("\r\n");
sb.append(tag).append(" OK CAPABILITY").append("\r\n");
return sb.toString();
}
public String handleId(String tag) {
StringBuilder sb = new StringBuilder();
sb.append("* ID (\"name\" \"tio-mail-wing\")").append("\r\n");
sb.append(tag).append(" OK ID completed.").append("\r\n");
return sb.toString();
}
public String handleIdle() {
return "+ idling" + "\r\n";
}
public String handleAuthenticate(ImapSessionContext session, String tag, String mech) {
StringBuilder sb = new StringBuilder();
if (!"LOGIN".equalsIgnoreCase(mech) && !"PLAIN".equalsIgnoreCase(mech)) {
sb.append(tag).append(" BAD Unsupported authentication mechanism").append("\r\n");
return sb.toString();
}
session.setCurrentCommandTag(tag);
if ("LOGIN".equalsIgnoreCase(mech)) {
session.setState(ImapSessionContext.State.AUTH_WAIT_USERNAME);
String chal = Base64Utils.encodeToString("Username:".getBytes(StandardCharsets.UTF_8));
sb.append("+ ").append(chal).append("\r\n");
} else {
session.setState(ImapSessionContext.State.AUTH_WAIT_PASSWORD);
sb.append("+ ").append("\r\n");
}
return sb.toString();
}
public String handleAuthData(ImapSessionContext session, String data) {
String tag = session.getCurrentCommandTag();
StringBuilder sb = new StringBuilder();
try {
String decoded = Base64Utils.decodeToString(data);
if (session.getState() == ImapSessionContext.State.AUTH_WAIT_USERNAME) {
session.setUsername(decoded);
session.setState(ImapSessionContext.State.AUTH_WAIT_PASSWORD);
String chal = Base64Utils.encodeToString("Password:".getBytes(StandardCharsets.UTF_8));
sb.append("+ ").append(chal).append("\r\n");
} else if (session.getState() == ImapSessionContext.State.AUTH_WAIT_PASSWORD) {
String user, pass;
if (decoded.contains("\0")) {
String[] parts = decoded.split("\0");
user = parts.length > 1 ? parts[1] : "";
pass = parts.length > 2 ? parts[2] : "";
} else {
user = session.getUsername();
pass = decoded;
}
Long userId = userService.authenticate(user, pass);
if (userId != null) {
session.setUsername(user);
session.setUserId(userId);
session.setState(ImapSessionContext.State.AUTHENTICATED);
sb.append(tag).append(" OK AUTHENTICATE completed.").append("\r\n");
} else {
session.setState(ImapSessionContext.State.NON_AUTHENTICATED);
sb.append(tag).append(" NO AUTHENTICATE failed: Authentication failed").append("\r\n");
}
session.setCurrentCommandTag(null);
}
} catch (IllegalArgumentException e) {
session.setState(ImapSessionContext.State.NON_AUTHENTICATED);
sb.append(tag).append(" BAD Invalid base64 data").append("\r\n");
session.setCurrentCommandTag(null);
}
return sb.toString();
}
public String handleLogin(ImapSessionContext session, String tag, String args) {
String[] parts = args.split("\\s+", 2);
if (parts.length < 2) {
return tag + " BAD login arguments invalid" + "\r\n";
}
String user = unquote(parts[0]);
String pass = unquote(parts[1]);
Long userId = userService.authenticate(user, pass);
if (userId != null) {
session.setUsername(user);
session.setUserId(userId);
session.setState(ImapSessionContext.State.AUTHENTICATED);
return tag + " OK LOGIN completed." + "\r\n";
} else {
return tag + " NO LOGIN failed: Authentication failed" + "\r\n";
}
}
public String handleLogout(ImapSessionContext session, String tag) {
if (session.getState() == ImapSessionContext.State.SELECTED) {
mailboxService.expunge(session.getUsername(), session.getSelectedMailbox());
session.setSelectedMailbox(null);
session.setSelectedMailboxId(null);
}
StringBuilder sb = new StringBuilder();
sb.append("* BYE tio-mail-wing IMAP4rev1 server signing off").append("\r\n");
sb.append(tag).append(" OK LOGOUT").append("\r\n");
return sb.toString();
}
public String handleSelect(ImapSessionContext session, String tag, String args) {
String mailbox = unquote(args);
StringBuilder sb = new StringBuilder();
Long userId = session.getUserId();
String username = session.getUsername();
boolean userExists = userService.userExists(userId);
if (!userExists) {
return tag + " NO SELECT failed: user not found: " + username + "\r\n";
}
Long mailBoxId = mailboxService.queryMailBoxId(userId, mailbox);
if (mailBoxId == null || mailBoxId < 1) {
return tag + " NO SELECT failed: mailbox not found: " + mailbox + "\r\n";
}
session.setSelectedMailbox(mailbox);
session.setSelectedMailboxId(mailBoxId);
session.setState(ImapSessionContext.State.SELECTED);
Row meta = mailboxService.getMailboxById(userId, mailBoxId);
if (meta == null) {
return tag + " NO SELECT failed: mailbox not found: " + mailbox + "\r\n";
}
List<Email> all = mailboxService.getActiveMessages(mailBoxId);
mailboxService.clearRecentFlags(username, mailbox);
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++;
}
}
}
long uv = meta.getLong("uid_next");
long un = meta.getLong("uid_validity");
log.info("exists:{},recent:{},uv{},un:{}", exists, recent, uv, un);
sb.append("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)").append("\r\n");
sb.append("* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted.").append("\r\n");
sb.append("* ").append(exists).append(" EXISTS").append("\r\n");
sb.append("* ").append(recent).append(" RECENT").append("\r\n");
sb.append("* OK [UIDVALIDITY ").append(un).append("] UIDs valid.").append("\r\n");
sb.append("* OK [UIDNEXT ").append(uv).append("] Predicted next UID.").append("\r\n");
sb.append(tag).append(" OK [READ-WRITE] SELECT completed.").append("\r\n");
return sb.toString();
}
public String handleStore(ImapSessionContext session, String tag, String args, boolean isUid) {
if (session.getState() != ImapSessionContext.State.SELECTED) {
return tag + " NO STORE failed: No mailbox selected" + "\r\n";
}
String[] p = args.split("\\s+", 3);
if (p.length < 3) {
return tag + " BAD Invalid STORE arguments" + "\r\n";
}
String set = p[0];
String op = p[1];
String flagsStr = p[2].replaceAll("[()]", "");
boolean add = op.startsWith("+");
Set<String> flags = new HashSet<>(Arrays.asList(flagsStr.split("\\s+")));
Long selectedMailboxId = session.getSelectedMailboxId();
List<Email> toUpd = null;
if (isUid) {
toUpd = mailboxService.findEmailsByUidSet(selectedMailboxId, set);
} else {
toUpd = mailboxService.findEmailsBySeqSet(selectedMailboxId, set);
}
StringBuilder sb = new StringBuilder();
for (Email e : toUpd) {
mailboxService.storeFlags(e, flags, add);
if (!op.contains(".SILENT")) {
String f = String.join(" ", e.getFlags());
int seq = toUpd.indexOf(e) + 1;
sb.append("* ").append(seq).append(" FETCH (FLAGS (" + f + ") UID " + e.getUid() + ")").append("\r\n");
}
}
sb.append(tag).append(" OK STORE completed.").append("\r\n");
return sb.toString();
}
public String handleUid(ImapSessionContext session, String tag, String args) {
if (session.getState() != ImapSessionContext.State.SELECTED) {
return tag + " NO UID failed: No mailbox selected" + "\r\n";
}
String[] parts = args.split("\\s+", 2);
String cmd = parts[0].toUpperCase();
String sub = parts.length > 1 ? parts[1] : "";
switch (cmd) {
case "FETCH":
ImapFetchService imapFetchService = Aop.get(ImapFetchService.class);
return imapFetchService.handleFetch(session, tag, sub, true);
case "STORE":
return handleStore(session, tag, sub, true);
case "COPY":
return handleCopy(session, tag, sub, true);
case "MOVE":
return handleMove(session, tag, sub, true);
default:
return tag + " BAD Unsupported UID command: " + cmd + "\r\n";
}
}
/**
* UID MOVE <set> "<mailbox>"
*/
public String handleMove(ImapSessionContext session, String tag, String args, boolean isUid) {
if (session.getState() != ImapSessionContext.State.SELECTED) {
return tag + " NO MOVE failed: No mailbox selected\r\n";
}
String[] p = args.split("\\s+", 2);
if (p.length < 2) {
return tag + " BAD MOVE arguments invalid\r\n";
}
String set = p[0];
String destMailbox = unquote(p[1]);
Long userId = session.getUserId();
String srcMailbox = session.getSelectedMailbox();
try {
mailboxService.moveEmailsByUidSet(userId, srcMailbox, set, destMailbox);
return tag + " OK MOVE completed.\r\n";
} catch (Exception e) {
return tag + " NO MOVE failed: " + e.getMessage() + "\r\n";
}
}
private String handleCopy(ImapSessionContext session, String tag, String args, boolean b) {
if (session.getState() != ImapSessionContext.State.SELECTED) {
return tag + " NO COPY failed: No mailbox selected\r\n";
}
// 拆分出消息集和目标 mailbox
String[] p = args.split("\\s+", 2);
if (p.length < 2) {
return tag + " BAD COPY arguments invalid\r\n";
}
String set = p[0];
String destMailbox = unquote(p[1]);
String user = session.getUsername();
String srcMailbox = session.getSelectedMailbox();
try {
// 调用新加的接口
mailboxService.copyEmailsByUidSet(user, srcMailbox, set, destMailbox);
return tag + " OK COPY completed.\r\n";
} catch (Exception e) {
return tag + " NO COPY failed: " + e.getMessage() + "\r\n";
}
}
/**
* CLOSE: 关闭当前 mailbox,并对所有 \Deleted 标记的邮件做 EXPUNGE
*/
public String handleClose(ImapSessionContext session, String tag) {
if (session.getState() != ImapSessionContext.State.SELECTED) {
return tag + " BAD CLOSE failed: No mailbox selected\r\n";
}
String user = session.getUsername();
String box = session.getSelectedMailbox();
// 1) 找出待 expunge 的 seq nums,发出 untagged EXPUNGE
List<Integer> seqs = mailboxService.getExpungeSeqNums(user, box);
StringBuilder sb = new StringBuilder();
for (int seq : seqs) {
sb.append("* ").append(seq).append(" EXPUNGE").append("\r\n");
}
// 2) 真正逻辑删除
mailboxService.expunge(user, box);
// 3) 取消 selected state
session.setSelectedMailbox(null);
session.setSelectedMailboxId(null);
session.setState(ImapSessionContext.State.AUTHENTICATED);
// 4) 返回 OK
sb.append(tag).append(" OK CLOSE completed").append("\r\n");
return sb.toString();
}
public String unquote(String s) {
if (s != null) {
if (s.startsWith("\"") && s.endsWith("\"")) {
return s.substring(1, s.length() - 1).toLowerCase();
} else {
return s.toLowerCase();
}
}
return s;
}
}
核心业务逻辑集中在 ImapService
,负责所有 IMAP 命令的具体实现,并与数据库交互。主要方法包括:
身份验证:
handleCapability、handleId、handleIdle
:返回无认证即可执行的命令列表与服务器标识。handleAuthenticate、handleAuthData
:支持AUTHENTICATE LOGIN/PLAIN
多步 Base64 交互。handleLogin
:一次性LOGIN
命令验证。
会话管理:
handleLogout
:在 SELECT 状态下先执行 EXPUNGE,再关闭会话。
邮箱管理:
handleCreate
:在数据库中新建目录。handleList / handleSubscribe
:列出目录并应答 SUBSCRIBE。
邮箱操作:
handleSelect
:选中邮箱后,返回 FLAGS、EXISTS、RECENT、UIDVALIDITY、UIDNEXT 等元信息。handleClose
:关闭当前选中邮箱并对\Deleted
标记邮件执行 EXPUNGE。
消息访问:
handleFetch
:支持按序号或 UID 集合获取邮件,返回 UID、RFC822.SIZE、FLAGS,可按需返回全文或头部。handleStore
:对邮件设置或清除标记,支持带或不带.SILENT
的通知。
UID 延伸命令:
handleUid
:对 UID FETCH/STORE/COPY/MOVE 进行分发。handleCopy / handleMove
:在数据库层面完成复制或移动操作。
EXPUNGE:
handleExpunge
:列出待 EXPUNGE 的序号,逻辑删除后反馈给客户端。
内部大量调用 mailboxService
、userService
等 AOP 注入的业务组件,与数据库完成 CRUD。
7.ImapFetchService
package com.tio.mail.wing.service;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.litongjava.jfinal.aop.Aop;
import com.tio.mail.wing.handler.ImapSessionContext;
import com.tio.mail.wing.model.Email;
public class ImapFetchService {
private static final String[] EMAIL_HEADER_FIELDS = new String[] { "From", "To", "Cc", "Bcc", "Subject", "Date", "Message-ID", "Priority", "X-Priority", "References", "Newsgroups", "In-Reply-To",
"Content-Type", "Reply-To" };
private static final Pattern BODY_FETCH_PATTERN = Pattern.compile("BODY(?:\\.PEEK)?\\[(.*?)\\]", Pattern.CASE_INSENSITIVE);
private static final Pattern UID_FETCH_PATTERN = Pattern.compile("([\\d\\*:,\\-]+)\\s+\\((.*)\\)", Pattern.CASE_INSENSITIVE);
private final MailboxService mailboxService = Aop.get(MailboxService.class);
public String handleFetch(ImapSessionContext session, String tag, String args, boolean isUid) {
if (session.getState() != ImapSessionContext.State.SELECTED) {
return tag + " NO FETCH failed: No mailbox selected\r\n";
}
Matcher m = UID_FETCH_PATTERN.matcher(args);
if (!m.find()) {
return tag + " BAD Invalid FETCH arguments: " + args + "\r\n";
}
String user = session.getUsername();
String box = session.getSelectedMailbox();
String set = m.group(1);
String items = m.group(2).toUpperCase();
List<Email> toFetch = null;
if (isUid) {
toFetch = mailboxService.findEmailsByUidSet(user, box, set);
} else {
toFetch = mailboxService.findEmailsBySeqSet(user, box, set);
}
StringBuilder sb = new StringBuilder();
if (toFetch == null || toFetch.isEmpty()) {
sb.append(tag).append(" OK FETCH completed.\r\n");
return sb.toString();
}
if (items.equalsIgnoreCase("FLAGS")) {
//UID fetch 1:* (FLAGS)
sb = fetchFlags(user, box, items, isUid, toFetch);
} else if (items.contains("BODY.PEEK[]")) {
//UID fetch 4 (UID RFC822.SIZE BODY[])
sb = fetchBodyPeek(user, box, items, isUid, toFetch);
} else if (items.contains("BODY[]")) {
sb = fetchBody(user, box, items, isUid, toFetch);
} else {
Matcher b = BODY_FETCH_PATTERN.matcher(items);
if (b.find()) {
//UID fetch 1:6 (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)])
String partToken = b.group(0);
partToken = partToken.replace("BODY.PEEK", "BODY");
sb = fetchHeader(user, box, items, isUid, partToken, toFetch);
}
}
sb.append(tag).append(" OK FETCH completed.\r\n");
return sb.toString();
}
private StringBuilder fetchFlags(String user, String box, String items, boolean isUid, List<Email> toFetch) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < toFetch.size(); i++) {
int seq = i + 1;
Email e = toFetch.get(i);
List<String> parts = new ArrayList<>();
parts.add("UID " + e.getUid());
Set<String> flags = e.getFlags();
if (flags != null) {
parts.add("FLAGS (" + String.join(" ", flags) + ")");
} else {
parts.add("FLAGS ()");
}
String prefix = "* " + seq + " FETCH (" + String.join(" ", parts) + ")\r\n";
sb.append(prefix);
}
return sb;
}
private StringBuilder fetchHeader(String user, String box, String items, boolean isUid, String partToken, List<Email> toFetch) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < toFetch.size(); i++) {
int seq = i + 1;
Email e = toFetch.get(i);
// 先把整封 raw byte[] 读出来,用于大小计算
String rawContent = e.getRawContent();
byte[] raw = rawContent.getBytes(StandardCharsets.UTF_8);
int fullSize = raw.length;
String prefix = prefixLine(seq, isUid, items, fullSize, e);
String hdr = parseHeaderFields(rawContent, EMAIL_HEADER_FIELDS);
byte[] hdrBytes = hdr.getBytes(StandardCharsets.UTF_8);
sb.append(prefix).append(" ").append(partToken);
sb.append(" {").append(hdrBytes.length).append("}\r\n");
sb.append(hdr);
sb.append(hdr).append("\r\n)\r\n");
}
return sb;
}
private StringBuilder fetchBody(String user, String box, String items, boolean isUid, List<Email> toFetch) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < toFetch.size(); i++) {
int seq = i + 1;
Email e = toFetch.get(i);
// 先把整封 raw byte[] 读出来,用于大小计算
String rawContent = e.getRawContent();
byte[] raw = rawContent.getBytes(StandardCharsets.UTF_8);
int fullSize = raw.length;
String prefix = prefixLine(seq, isUid, items, fullSize, e);
mailboxService.storeFlags(e, Collections.singleton("\\Seen"), true);
sb.append(prefix);
sb.append(" BODY[] {").append(fullSize).append("}\r\n");
sb.append(rawContent);
sb.append("\r\n)\r\n");
}
return sb;
}
private StringBuilder fetchBodyPeek(String user, String box, String items, boolean isUid, List<Email> toFetch) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < toFetch.size(); i++) {
int seq = i + 1;
Email e = toFetch.get(i);
// 先把整封 raw byte[] 读出来,用于大小计算
String rawContent = e.getRawContent();
byte[] raw = rawContent.getBytes(StandardCharsets.UTF_8);
int fullSize = raw.length;
String prefix = prefixLine(seq, isUid, items, fullSize, e);
sb.append(prefix);
sb.append(" BODY[] {").append(fullSize).append("}\r\n");
sb.append(rawContent);
sb.append("\r\n)\r\n");
}
return sb;
}
//* 1 FETCH (UID 1 RFC822.SIZE 262 FLAGS (\Seen) BODY[HEADER.FIELDS (FROM TO CC BCC SUBJECT DATE MESSAGE-ID PRIORITY X-PRIORITY REFERENCES NEWSGROUPS IN-REPLY-TO CONTENT-TYPE REPLY-TO)] {211}
private String prefixLine(int seq, boolean isUid, String items, int fullSize, Email email) {
// 按 固定顺序 UID → RFC822.SIZE → FLAGS 构造 parts 列表
List<String> parts = new ArrayList<>();
if (isUid || items.contains("UID")) {
parts.add("UID " + email.getUid());
}
if (items.contains("RFC822.SIZE")) {
parts.add("RFC822.SIZE " + fullSize);
}
if (items.contains("FLAGS")) {
Set<String> flags = email.getFlags();
if (flags != null) {
parts.add("FLAGS (" + String.join(" ", flags) + ")");
} else {
parts.add("FLAGS ()");
}
}
String prefix = "* " + seq + " FETCH (" + String.join(" ", parts);
return prefix;
}
public String parseHeaderFields(String content, String[] fields) {
Map<String, String> hdr = new HashMap<>();
for (String line : content.split("\\r?\\n")) {
if (line.isEmpty()) {
break;
}
int i = line.indexOf(":");
if (i > 0) {
hdr.put(line.substring(0, i).toUpperCase(), line.substring(i + 1).trim());
}
}
StringBuilder sb = new StringBuilder();
for (String f : fields) {
String v = hdr.get(f.toUpperCase());
if (v != null) {
sb.append(f).append(": ").append(v).append("\r\n");
}
}
return sb.toString();
}
}
--
小结
本方案通过 T-IO 框架实现 IMAP 协议的基础交互,结合 ActiveRecord 与自定义业务服务,实现了从连接、认证、目录管理、消息读写到标记与删除的全流程 IMAP 支持。各组件职责分离,后续可按需扩展更多 IMAP 扩展命令和性能优化。