任务4:实现 POP3 服务(数据库版本)
本任务旨在基于 Tio 框架和已有的服务层(MwUserService
、MailboxService
等)实现一个支持数据库交互的 POP3 服务器。本文档介绍了各个组件的功能及配置方法,并保留了原有代码示例。
项目结构
com.tio.mail.wing.config
└── Pop3ServerConfig.java // POP3 服务的启动配置
com.tio.mail.wing.handler
├── Pop3ServerAioHandler.java // POP3 协议的解码、编码与主流程处理器
└── Pop3SessionContext.java // 会话状态与用户信息上下文
com.tio.mail.wing.service
└── Pop3Service.java // 授权阶段 & 事务阶段命令处理
// 依赖服务层:
com.tio.mail.wing.service.MwUserService // 用户认证服务
com.tio.mail.wing.service.MailboxService // 邮箱与消息查询服务
配置与启动:Pop3ServerConfig
负责读取配置、创建 Tio 服务实例并启动监听。
package com.tio.mail.wing.config;
import java.io.IOException;
import com.litongjava.annotation.Initialization;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.server.ServerTioConfig;
import com.litongjava.tio.server.TioServer;
import com.litongjava.tio.server.intf.ServerAioHandler;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.tio.mail.wing.handler.Pop3ServerAioHandler;
import com.tio.mail.wing.listener.Pop3ServerAioListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Pop3ServerConfig {
@Initialization
public void startPop3Server() {
ServerAioHandler serverHandler = Aop.get(Pop3ServerAioHandler.class);
Pop3ServerAioListener pop3ServerAioListener = Aop.get(Pop3ServerAioListener.class);
// 配置对象
ServerTioConfig serverTioConfig = new ServerTioConfig("pop3-server");
serverTioConfig.setServerAioHandler(serverHandler);
serverTioConfig.setServerAioListener(pop3ServerAioListener);
serverTioConfig.checkAttacks = false;
serverTioConfig.ignoreDecodeFail = true;
serverTioConfig.setWorkerThreads(4);
// 设置心跳,-1 取消心跳
serverTioConfig.setHeartbeatTimeout(-1);
// TioServer 对象
TioServer tioServer = new TioServer(serverTioConfig);
// 启动服务
try {
int port = EnvUtils.getInt("mail.server.pop3.port", 110);
tioServer.start(null, port);
log.info("Started POP3 server on port: {}", port);
} catch (IOException e) {
log.error("Failed to start POP3 server", e);
}
}
}
- 使用
@Initialization
注解自动初始化。 - 端口从环境变量
mail.server.pop3.port
(默认 110)读取。 - 禁用心跳检测,线程数配置为 4。
会话上下文:Pop3SessionContext
用于跟踪当前连接的状态与用户身份。
package com.tio.mail.wing.handler;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Pop3SessionContext {
// 会话状态枚举
public enum State {
/** 未认证 */
AUTHORIZATION,
/** 已认证,可进行邮件操作 */
TRANSACTION,
/** 准备关闭 */
UPDATE
}
private State state = State.AUTHORIZATION;
private Long userId;
private String username;
}
state
:标记为AUTHORIZATION
、TRANSACTION
或UPDATE
。userId
、username
:仅在授权成功后填充。
编解码与消息处理:Pop3ServerAioHandler
实现 ServerAioHandler
,负责协议行的解码、响应编码及命令分发。
package com.tio.mail.wing.handler;
import java.nio.ByteBuffer;
import com.litongjava.aio.Packet;
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.Pop3Packet;
import com.tio.mail.wing.service.Pop3Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Pop3ServerAioHandler implements ServerAioHandler {
private Pop3Service pop3Service = Aop.get(Pop3Service.class);
/**
* 解码:从 ByteBuffer 中解析出以 \r\n 结尾的一行命令
*/
@Override
public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws TioDecodeException {
String charset = channelContext.getTioConfig().getCharset();
String line = null;
try {
line = ByteBufferUtils.readLine(buffer, charset);
} catch (LengthOverflowException e) {
e.printStackTrace();
}
if (line == null) {
return null;
}
return new Pop3Packet(line);
}
/**
* 编码:将响应字符串转换为 ByteBuffer
*/
@Override
public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) {
String charset = channelContext.getTioConfig().getCharset();
Pop3Packet pop3Packet = (Pop3Packet) packet;
String line = pop3Packet.getLine();
try {
byte[] bytes = line.getBytes(charset);
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
return buffer;
} catch (Exception e) {
log.error("Encoding error", e);
return null;
}
}
/**
* 消息处理:根据收到的命令和当前会话状态进行响应
*/
@Override
public void handler(Packet packet, ChannelContext channelContext) throws Exception {
Pop3Packet pop3Packet = (Pop3Packet) packet;
String commandLine = pop3Packet.getLine().trim();
log.info("POP3 <<< {}", commandLine);
// 获取或创建会话上下文
Pop3SessionContext sessionContext = (Pop3SessionContext) channelContext.get("sessionContext");
String[] parts = commandLine.split("\\s+", 2);
String command = parts[0].toUpperCase();
String reply = null;
switch (sessionContext.getState()) {
case AUTHORIZATION:
reply = pop3Service.handleAuthorizationState(command, parts, sessionContext);
break;
case TRANSACTION:
reply = pop3Service.handleTransactionState(command, parts, sessionContext);
break;
case UPDATE:
if ("QUIT".equals(command)) {
reply = pop3Service.handleQuit(sessionContext);
if (reply != null) {
Tio.send(channelContext, new Pop3Packet(reply));
}
Tio.close(channelContext, "quit");
} else {
reply = "-ERR Command not allowed in UPDATE state.\r\n";
}
break;
}
if (reply != null) {
Tio.send(channelContext, new Pop3Packet(reply));
}
}
}
decode
:基于行解码器按\r\n
拆分输入。encode
:将响应文本转为ByteBuffer
。handler
:根据Pop3SessionContext.State
分发给Pop3Service
。
业务逻辑:Pop3Service
负责处理 POP3 协议中授权阶段(AUTHORIZATION)和事务阶段(TRANSACTION)的命令。
package com.tio.mail.wing.service;
import java.util.List;
import com.litongjava.jfinal.aop.Aop;
import com.tio.mail.wing.handler.Pop3SessionContext;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Pop3Service {
private final MwUserService userService = Aop.get(MwUserService.class);
private final MailboxService mailboxService = Aop.get(MailboxService.class);
/**
* 处理授权阶段命令,返回一次性可发送的 POP3 响应字符串
*/
public String handleAuthorizationState(String command, String[] parts, Pop3SessionContext sessionContext) {
StringBuilder resp = new StringBuilder();
switch (command) {
case "CAPA":
// +OK 响应头
resp.append("+OK Capability list follows\r\n");
// 多行能力列表,并以单独一行 "." 结束
resp.append("TOP\r\n").append("USER\r\n").append("UIDL\r\n").append("PIPELINING\r\n")
// 可选 STLS 注释留在代码里,未来如需支持可解开
// .append("STLS\r\n")
.append(".\r\n");
break;
case "USER":
if (parts.length < 2) {
resp.append("-ERR Username required.\r\n");
} else {
sessionContext.setUsername(parts[1]);
resp.append("+OK Password required for ").append(parts[1]).append("\r\n");
}
break;
case "PASS":
String username = sessionContext.getUsername();
if (username == null) {
resp.append("-ERR USER command first.\r\n");
} else if (parts.length < 2) {
resp.append("-ERR Password required.\r\n");
} else {
Long userId = userService.authenticate(username, parts[1]);
if (userId != null) {
sessionContext.setState(Pop3SessionContext.State.TRANSACTION);
sessionContext.setUserId(userId);
resp.append("+OK Mailbox open.\r\n");
} else {
sessionContext.setUsername(null);
resp.append("-ERR Authentication failed.\r\n");
}
}
break;
case "QUIT":
// 委托给 handleQuit 构造响应
return handleQuit(sessionContext);
default:
resp.append("-ERR Unknown command or command not allowed.\r\n");
}
String result = resp.toString();
log.info("POP3 >>>\n{}", result.trim());
return result;
}
/**
* 处理事务阶段命令,返回一次性可发送的 POP3 响应字符串
*/
public String handleTransactionState(String command, String[] parts, Pop3SessionContext sessionContext) {
StringBuilder resp = new StringBuilder();
String username = sessionContext.getUsername();
Long userId = sessionContext.getUserId();
switch (command) {
case "STAT":
int[] stat = mailboxService.getStat(username);
resp.append("+OK ").append(stat[0]).append(" ").append(stat[1]).append("\r\n");
break;
case "TOP":
if (parts.length < 2) {
resp.append("-ERR Message number and number of lines required.\r\n");
break;
}
String[] topArgs = parts[1].split("\\s+");
if (topArgs.length < 2) {
resp.append("-ERR Message number and number of lines required.\r\n");
break;
}
try {
int msgId = Integer.parseInt(topArgs[0]);
int lines = Integer.parseInt(topArgs[1]);
String content = mailboxService.getMessageContent(username, msgId);
if (content == null) {
resp.append("-ERR No such message.\r\n");
} else {
resp.append("+OK Top of message follows\r\n");
// 头+前几行正文
String[] contentLines = content.split("\r\n");
boolean inBody = false;
int bodyCount = 0;
for (String line : contentLines) {
resp.append(line).append("\r\n");
if (!inBody && line.isEmpty())
inBody = true;
if (inBody && !line.isEmpty())
bodyCount++;
if (inBody && bodyCount >= lines)
break;
}
resp.append(".\r\n");
}
} catch (NumberFormatException e) {
resp.append("-ERR Invalid arguments for TOP command.\r\n");
}
break;
case "LIST":
List<Integer> sizes = mailboxService.listMessages(userId);
resp.append("+OK ").append(sizes.size()).append(" messages\r\n");
for (int i = 0; i < sizes.size(); i++) {
resp.append(i + 1).append(" ").append(sizes.get(i)).append("\r\n");
}
resp.append(".\r\n");
break;
case "UIDL":
List<Long> uids = mailboxService.listUids(userId);
resp.append("+OK Unique-ID listing follows\r\n");
for (int i = 0; i < uids.size(); i++) {
resp.append(i + 1).append(" ").append(uids.get(i)).append("\r\n");
}
resp.append(".\r\n");
break;
case "RETR":
if (parts.length < 2) {
resp.append("-ERR Message ID required.\r\n");
break;
}
try {
int msgId = Integer.parseInt(parts[1]);
String content = mailboxService.getMessageContent(username, msgId);
if (content == null) {
resp.append("-ERR No such message.\r\n");
} else {
resp.append("+OK Message ").append(msgId).append(" follows\r\n");
// 邮件全文
resp.append(content).append("\r\n.\r\n");
}
} catch (NumberFormatException e) {
resp.append("-ERR Invalid message ID.\r\n");
}
break;
case "DELE":
// 标记删除逻辑(此示例暂不真正删除)
resp.append("+OK Message marked for deletion.\r\n");
break;
case "NOOP":
resp.append("+OK\r\n");
break;
case "RSET":
// 取消删除标记逻辑
resp.append("+OK Deletion marks removed.\r\n");
break;
case "QUIT":
return handleQuit(sessionContext);
default:
resp.append("-ERR Unknown command.\r\n");
}
String result = resp.toString();
log.info("POP3 >>>\n{}", result.trim());
return result;
}
/**
* 处理 QUIT,返回一次性可发送的 POP3 响应字符串
*/
public String handleQuit(Pop3SessionContext sessionContext) {
sessionContext.setState(Pop3SessionContext.State.UPDATE);
// 这里执行真正的删除操作(本系统暂不删除邮件)
String result = "+OK tio-mail-wing POP3 server signing off.\r\n";
log.info("POP3 >>>\n{}", result.trim());
return result;
}
}
handleAuthorizationState
:处理CAPA
、USER
、PASS
、QUIT
等命令。handleTransactionState
:处理STAT
、LIST
、UIDL
、RETR
、DELE
、NOOP
、RSET
、QUIT
等命令。- 依赖
MailboxService
完成邮件统计、列表、UID 查询和内容获取,不在此处展开。
注意事项
- 服务端口:确保
mail.server.pop3.port
已在环境或配置文件中正确设置。 - 事务结束:在
QUIT
命令后,服务端会切换到UPDATE
状态并关闭连接。 - 并发处理:可根据需求调整
serverTioConfig.setWorkerThreads(...)
。 - 扩展能力:如需支持
STLS
、APOP
等功能,可在handleAuthorizationState
中增补。
以上即为 POP3 服务数据库版本的完整实现文档。可根据实际业务扩展或细化日志和异常处理逻辑。