任务1:实现POP3系统
POP3 协议详解 (Post Office Protocol version 3)
POP3 是一种应用层协议,其全称为“邮局协议第3版”。顾名思义,它的工作模式非常像现实生活中的邮局信箱。
核心思想: 你去邮局的信箱取信,把所有信件都拿出来带回家,然后你的信箱就空了。你在家阅读、整理、删除这些信件,这些操作都与邮局无关了。
POP3 就是这样一个“下载并删除”的协议。它允许邮件客户端连接到邮件服务器,将收件箱(INBOX)中的所有邮件下载到本地计算机,然后通常会从服务器上删除这些邮件。
主要特点
简单性 (Simplicity): POP3 协议非常简单,命令数量少,交互逻辑直接。这使得客户端和服务器的实现都相对容易。
离线工作模式 (Offline Model): 它是为离线邮件阅读而设计的。用户连接网络,一次性收取所有新邮件,然后可以断开网络,在本地慢慢阅读、回复和管理邮件。
状态化会话 (Stateful Session): 一个 POP3 会话包含三个明确定义的状态。服务器会根据客户端当前所处的状态来决定哪些命令是合法的。
服务器资源占用少 (Low Server Resource Usage): 由于邮件默认被下载到本地后就从服务器删除,服务器不需要长期存储大量邮件,节省了存储空间。
不适合多设备同步 (Poor for Multi-Device Sync): 这是它最大的缺点。如果你在电脑上用 POP3 收取了邮件,这些邮件就会从服务器上消失。此时你再用手机登录,将看不到任何旧邮件,因为它们已经“被你从邮局带回家了”。(虽然现在很多客户端提供了“在服务器上保留副本”的选项,但这并非 POP3 的原生设计,且管理起来依然不便)。
POP3 会话的三个阶段
一个典型的 POP3 连接过程分为三个阶段:
1. 授权 (AUTHORIZATION) 阶段
这是会话的开始阶段,客户端需要向服务器验明身份。
- 目标:登录邮箱。
- 过程:客户端连接到服务器的 110 端口后,服务器会发送一个
+OK
的欢迎语。然后客户端必须通过USER
和PASS
命令提供用户名和密码。 - 可用命令:
USER
,PASS
,QUIT
。 - 状态转换:一旦用户名和密码验证成功,会话就从 授权 阶段进入 事务 阶段。如果验证失败,会话仍停留在 授权 阶段。如果客户端发送
QUIT
,则直接进入 更新 阶段。
2. 事务 (TRANSACTION) 阶段
这是会话的核心阶段,客户端在此阶段进行邮件的查询、获取和标记删除。
- 目标:管理邮件。
- 过程:客户端可以查看邮箱统计信息、获取邮件列表、下载邮件内容,并标记要删除的邮件。
- 重要提示:在这一阶段,
DELE
命令只是“标记”邮件为待删除,并不会立即从服务器上真正删除。 - 可用命令:
STAT
: 获取邮箱状态(邮件总数和总大小)。LIST
: 列出每封邮件的编号和大小。RETR [msg_id]
: 获取指定编号的邮件全文。DELE [msg_id]
: 标记指定编号的邮件为待删除。NOOP
: 无操作,用于保持连接活动,服务器会回复+OK
。RSET
: 重置,清除所有在本阶段做的DELE
标记。QUIT
: 结束事务,准备退出。
- 状态转换:当客户端发送
QUIT
命令后,会话进入 更新 阶段。
3. 更新 (UPDATE) 阶段
这是会话的结束阶段,服务器执行清理工作。
- 目标:应用更改并关闭连接。
- 过程:服务器会永久删除所有在 事务 阶段被
DELE
命令标记的邮件。完成删除后,服务器向客户端发送一个+OK
的告别语,然后关闭 TCP 连接。 - 可用命令:无(此阶段由服务器自动执行,不接受客户端新命令)。
常用 POP3 命令详解
命令 | 格式 | 作用 | 示例 | 可能的响应 |
---|---|---|---|---|
USER | USER <username> | 发送用户名 | USER user1@tio.com | +OK (用户存在) / -ERR (用户不存在) |
PASS | PASS <password> | 发送密码 | PASS mysecret | +OK (密码正确) / -ERR (密码错误) |
STAT | STAT | 获取邮箱统计信息 | STAT | +OK 2 320 (2封邮件, 共320字节) |
LIST | LIST [msg_id] | 列出邮件(不带参数列出所有) | LIST | +OK 2 messages\r\n1 120\r\n2 200\r\n. |
RETR | RETR <msg_id> | 获取指定邮件的完整内容 | RETR 1 | +OK 120 octets\r\n(邮件内容)\r\n. |
DELE | DELE <msg_id> | 标记邮件为待删除 | DELE 1 | +OK message 1 deleted |
RSET | RSET | 取消所有删除标记 | RSET | +OK maildrop has 2 messages |
NOOP | NOOP | 无操作(心跳) | NOOP | +OK |
QUIT | QUIT | 结束会话 | QUIT | +OK dewey POP3 server signing off |
响应格式说明:
+OK
: 表示命令成功执行。后面通常跟着一些说明文字。-ERR
: 表示命令执行失败。后面跟着失败原因。- 多行响应: 对于
LIST
和RETR
等可能返回多行数据的命令,响应以+OK
开始,然后是数据行,最后以一个单独的句点.
结束。
一个完整的 Telnet 交互示例
下面是一个手动使用 telnet
与 POP3 服务器交互的完整流程。S:
代表服务器响应,C:
代表客户端输入。
# 1. 连接到服务器的 110 端口
$ telnet mail.example.com 110
# 2. 服务器返回欢迎信息,进入【授权】阶段
S: +OK POP3 server ready <1896.697170952@mail.example.com>
# 3. 客户端发送用户名
C: USER alice
# 4. 服务器确认用户存在
S: +OK
# 5. 客户端发送密码
C: PASS mypassword
# 6. 服务器确认密码正确,进入【事务】阶段
S: +OK alice's maildrop has 2 messages (320 octets)
# 7. 客户端查看邮箱状态
C: STAT
S: +OK 2 320
# 8. 客户端列出所有邮件
C: LIST
S: +OK 2 messages (320 octets)
S: 1 120
S: 2 200
S: .
# 9. 客户端获取第一封邮件
C: RETR 1
S: +OK 120 octets
S: From: bob@example.com
S: To: alice@example.com
S: Subject: Hello
S:
S: This is a test message.
S: .
# 10. 客户端决定删除第一封邮件
C: DELE 1
S: +OK message 1 deleted
# 11. 客户端决定不删除第二封邮件,准备退出
C: QUIT
# 12. 服务器进入【更新】阶段,永久删除被标记的邮件,并发送告别语,然后关闭连接
S: +OK dewey POP3 server signing off (maildrop empty)
# 连接已由外部主机关闭。
官方文档和资源链接
RFC 1939 - Post Office Protocol - Version 3
- 链接: https://www.rfc-editor.org/rfc/rfc1939
- 说明: 这是 POP3 协议的最终官方规范,也被称为“协议圣经”。所有关于 POP3 的实现细节、命令格式、状态转换的权威定义都在这里。如果你要从头实现一个 POP3 服务器,这是必读文档。
RFC 2449 - POP3 Extension Mechanism
- 链接: https://www.rfc-editor.org/rfc/rfc2449
- 说明: 定义了 POP3 的扩展机制,例如
CAPA
命令,它允许客户端查询服务器支持哪些扩展功能。
RFC 2595 - Using TLS with IMAP, POP3 and ACAP
- 链接: https://www.rfc-editor.org/rfc/rfc2595
- 说明: 定义了如何通过
STARTTLS
命令将一个不安全的 POP3 连接升级为安全的 TLS 加密连接。这是实现现代邮件服务安全性的关键。
实现POP3服务
第一步:Pop3Packet
和创建会话上下文
POP3 协议是基于字符串命令的,所以我们的 Packet
直接存储字符串会更方便。同时,我们需要一个会话上下文来跟踪每个客户端连接的状态(例如,是否已登录)。
1. Pop3Packet.java
将 byte[]
改为 String
,方便处理命令。
// src/main/java/com/tio/mail/wing/packet/Pop3Packet.java
package com.tio.mail.wing.packet;
import com.litongjava.aio.Packet;
/**
* POP3 消息包,直接存储解码后的命令或响应字符串
*/
@SuppressWarnings("serial")
public class Pop3Packet extends Packet {
private String line;
public Pop3Packet(String line) {
this.line = line;
}
public String getLine() {
return line;
}
public void setLine(String line) {
this.line = line;
}
}
2. 创建 Pop3SessionContext.java
这是至关重要的一步,用于保存每个连接的会话状态。
// src/main/java/com/tio/mail/wing/handler/Pop3SessionContext.java
package com.tio.mail.wing.handler;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.tio.mail.wing.packet.Pop3Packet;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Pop3SessionContext {
// 会话状态枚举
public enum State {
/**
* 未认证
*/
AUTHORIZATION,
/**
* 已认证,可以进行邮件操作
*/
TRANSACTION,
/**
* 准备关闭
*/
UPDATE
}
private State state = State.AUTHORIZATION;
private String username;
// 可以添加更多字段,如待删除邮件列表等
/**
* 发送 OK 响应
* @param context ChannelContext
* @param message 消息内容
*/
public static void sendOk(ChannelContext context, String message) {
String response = "+OK " + message + "\r\n";
Tio.send(context, new Pop3Packet(response));
}
/**
* 发送 ERR 响应
* @param context ChannelContext
* @param message 消息内容
*/
public static void sendErr(ChannelContext context, String message) {
String response = "-ERR " + message + "\r\n";
Tio.send(context, new Pop3Packet(response));
}
/**
* 发送多行数据
* @param context ChannelContext
* @param data 多行数据
*/
public static void sendData(ChannelContext context, String data) {
// 多行数据以 "." 结尾
String response = data + "\r\n.\r\n";
Tio.send(context, new Pop3Packet(response));
}
}
第二步:创建模拟的业务服务
为了让 Handler
能跑起来,我们先创建两个简单的内存版服务。
1. Email.java
// src/main/java/com/tio/mail/wing/model/Email.java
package com.tio.mail.wing.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* 邮件实体类
* 代表一封存储在服务器上的邮件
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Email {
/**
* 唯一标识符 (UIDL)
* 这是邮件的“身份证”,在邮件的整个生命周期中保持不变。
* 使用 UUID 或者 数据库自增ID + 时间戳等方式确保唯一。
*/
private String uid;
/**
* 邮件大小 (单位:字节)
* 用于 LIST 命令的响应。
*/
private int size;
/**
* 邮件的完整原始内容 (MIME 格式)
* 包括邮件头和邮件体,用于 RETR 命令。
*/
private String content;
/**
* 标记为待删除
* 用于 DELE 命令。当会话结束时,标记为 true 的邮件将被真正删除。
*/
private boolean deleted = false;
/**
* 一个便捷的构造函数,用于快速创建邮件对象。
* 它会自动根据内容计算大小并生成 UID。
* @param content 邮件的原始内容
*/
public Email(String content) {
this.content = content;
// 在真实场景中,字符集转换可能更复杂,这里用 getBytes() 做简单估算
this.size = content.getBytes().length;
// 使用 UUID 生成一个唯一的、随机的 ID
this.uid = UUID.randomUUID().toString().replace("-", "");
}
}
2. UserService.java
// src/main/java/com/tio/mail/wing/service/UserService.java
package com.tio.mail.wing.service;
import com.litongjava.annotation.AService;
import java.util.HashMap;
import java.util.Map;
public class UserService {
// 模拟用户数据库
private static final Map<String, String> users = new HashMap<>();
static {
users.put("user1@tio.com", "pass1");
users.put("user2@tio.com", "pass2");
}
/**
* 认证用户
* @param username 用户名
* @param password 密码
* @return 是否成功
*/
public boolean authenticate(String username, String password) {
String storedPassword = users.get(username);
return storedPassword != null && storedPassword.equals(password);
}
}
3. MailboxService.java
// src/main/java/com/tio/mail/wing/service/MailboxService.java
package com.tio.mail.wing.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import com.litongjava.annotation.AService;
import com.tio.mail.wing.model.Email;
@AService
public class MailboxService {
/**
* 模拟邮件存储系统
* Key: 用户名 (e.g., "user1@tio.com")
* Value: 该用户的邮件列表 (List<Email>)
*/
private static final Map<String, List<Email>> mailboxes = new ConcurrentHashMap<>();
// 使用静态代码块初始化一些模拟数据
static {
// 为 user1@tio.com 创建邮箱并添加两封邮件
List<Email> user1Emails = new ArrayList<>();
user1Emails.add(new Email("From: sender@example.com\r\n" + "To: user1@tio.com\r\n" + "Subject: Test Mail 1\r\n" + "\r\n" + "This is the body of the first email."));
user1Emails.add(new Email("From: another@example.com\r\n" + "To: user1@tio.com\r\n" + "Subject: Hello World\r\n" + "\r\n" + "This is the second message."));
mailboxes.put("user1@tio.com", user1Emails);
// 为 user2@tio.com 创建一个空邮箱
mailboxes.put("user2@tio.com", new ArrayList<>());
}
/**
* 获取用户邮箱中所有未被标记为删除的邮件。
* @param username 用户名
* @return 邮件列表
*/
private List<Email> getActiveMessages(String username) {
List<Email> userEmails = mailboxes.getOrDefault(username, new ArrayList<>());
return userEmails.stream().filter(email -> !email.isDeleted()).collect(Collectors.toList());
}
/**
* 获取邮箱状态(邮件数,总大小)
* @param username 用户名
* @return int[]{邮件数, 总大小}
*/
public int[] getStat(String username) {
List<Email> activeMessages = getActiveMessages(username);
int count = activeMessages.size();
int totalSize = activeMessages.stream().mapToInt(Email::getSize).sum();
return new int[] { count, totalSize };
}
/**
* 获取邮件大小列表,用于 LIST 命令。
* @param username 用户名
* @return 邮件大小列表
*/
public List<Integer> listMessages(String username) {
return getActiveMessages(username).stream().map(Email::getSize).collect(Collectors.toList());
}
/**
* 获取指定邮件内容
* @param username 用户名
* @param msgNumber 邮件序号 (从 1 开始)
* @return 邮件原始内容
*/
public String getMessageContent(String username, int msgNumber) {
List<Email> activeMessages = getActiveMessages(username);
// 序号从 1 开始,列表索引从 0 开始,需要转换
if (msgNumber > 0 && msgNumber <= activeMessages.size()) {
return activeMessages.get(msgNumber - 1).getContent();
}
return null;
}
/**
* 获取邮件的唯一ID列表,用于 UIDL 命令。
* @param username 用户名
* @return 邮件的唯一ID列表
*/
public List<String> listUids(String username) {
return getActiveMessages(username).stream().map(Email::getUid).collect(Collectors.toList());
}
}
Pop3ServerAioListener
// src/main/java/com/tio/mail/wing/listener/Pop3ServerAioListener.java
package com.tio.mail.wing.listener;
import com.litongjava.aio.Packet;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.server.intf.ServerAioListener;
import com.tio.mail.wing.handler.Pop3SessionContext;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Pop3ServerAioListener implements ServerAioListener {
@Override
public void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect) throws Exception {
if (isConnected) {
log.info("POP3 client connected: {}", channelContext.getClientNode());
// 1. 创建会话上下文
Pop3SessionContext sessionContext = new Pop3SessionContext();
channelContext.set("sessionContext", sessionContext);
// 2. 立即发送欢迎消息
Pop3SessionContext.sendOk(channelContext, "tio-mail-wing POP3 server ready.");
log.info("POP3 >>> +OK welcome message sent to {}", channelContext.getClientNode());
}
}
@Override
public void onBeforeClose(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) throws Exception {
log.info("POP3 client disconnected: {}", channelContext.getClientNode());
}
@Override
public void onAfterDecoded(ChannelContext channelContext, Packet packet, int packetSize) throws Exception {
// Do nothing
}
@Override
public void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes) throws Exception {
// Do nothing
}
@Override
public void onAfterSent(ChannelContext channelContext, Packet packet, boolean isSentSuccess) throws Exception {
// Do nothing
}
@Override
public void onAfterHandled(ChannelContext channelContext, Packet packet, long cost) throws Exception {
// Do nothing
}
@Override
public boolean onHeartbeatTimeout(ChannelContext channelContext, Long interval, int heartbeatTimeoutCount) {
return false;
}
}
Pop3ServerAioHandler
这是最核心的修改,我们将实现真正的 POP3 协议逻辑。
// src/main/java/com/tio/mail/wing/handler/Pop3ServerAioHandler.java
package com.tio.mail.wing.handler;
import java.nio.ByteBuffer;
import java.util.List;
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.MailboxService;
import com.tio.mail.wing.service.UserService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Pop3ServerAioHandler implements ServerAioHandler {
private UserService userService = Aop.get(UserService.class);
private MailboxService mailboxService = Aop.get(MailboxService.class);
private static final String CHARSET = "UTF-8";
/**
* 解码:从 ByteBuffer 中解析出以 \r\n 结尾的一行命令
*/
@Override
public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws TioDecodeException {
// Tio内置了行解码器,非常方便
String line = null;
try {
line = ByteBufferUtils.readLine(buffer, CHARSET);
} catch (LengthOverflowException e) {
e.printStackTrace();
}
// 如果 line 为 null,表示数据不完整,不是一个完整的行,需要等待更多数据
if (line == null) {
return null;
}
// 返回一个包含该行命令的 Packet
return new Pop3Packet(line);
}
/**
* 编码:将响应字符串转换为 ByteBuffer
*/
@Override
public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) {
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();
// 根据会话状态处理命令
switch (sessionContext.getState()) {
case AUTHORIZATION:
handleAuthorizationState(command, parts, channelContext, sessionContext);
break;
case TRANSACTION:
handleTransactionState(command, parts, channelContext, sessionContext);
break;
case UPDATE:
// 在 UPDATE 状态,通常只响应 QUIT
if ("QUIT".equals(command)) {
handleQuit(channelContext, sessionContext);
} else {
Pop3SessionContext.sendErr(channelContext, "Command not allowed in UPDATE state.");
}
break;
}
}
private void handleAuthorizationState(String command, String[] parts, ChannelContext channelContext, Pop3SessionContext sessionContext) {
switch (command) {
case "CAPA":
// 1. 先发送 +OK 响应头
Pop3SessionContext.sendOk(channelContext, "Capability list follows");
// 2. 构造多行响应体
StringBuilder capaBuilder = new StringBuilder();
capaBuilder.append("TOP\r\n"); // 声明支持 TOP 命令
capaBuilder.append("USER\r\n"); // 声明支持 USER/PASS 认证
capaBuilder.append("UIDL\r\n"); // 声明支持 UIDL 命令
capaBuilder.append("PIPELINING\r\n"); // 声明支持管道,可以提高效率
// capaBuilder.append("STLS\r\n"); // 如果未来支持 STARTTLS,可以取消这行注释
capaBuilder.append("."); // 以点结束多行响应
// 3. 发送响应体
Tio.send(channelContext, new Pop3Packet(capaBuilder.toString() + "\r\n"));
// 注意:因为我们自己拼接了多行响应,所以不需要用 sendData 方法,
// 并且日志也应该在发送时手动打印,以保持一致性。
log.info("POP3 >>>\n{}", capaBuilder.toString().trim());
break;
case "USER":
if (parts.length < 2) {
Pop3SessionContext.sendErr(channelContext, "Username required.");
return;
}
sessionContext.setUsername(parts[1]);
Pop3SessionContext.sendOk(channelContext, "Password required for " + parts[1]);
break;
case "PASS":
if (sessionContext.getUsername() == null) {
Pop3SessionContext.sendErr(channelContext, "USER command first.");
return;
}
if (parts.length < 2) {
Pop3SessionContext.sendErr(channelContext, "Password required.");
return;
}
if (userService.authenticate(sessionContext.getUsername(), parts[1])) {
sessionContext.setState(Pop3SessionContext.State.TRANSACTION);
Pop3SessionContext.sendOk(channelContext, "Mailbox open.");
} else {
Pop3SessionContext.sendErr(channelContext, "Authentication failed.");
sessionContext.setUsername(null); // 认证失败,清空用户名
}
break;
case "QUIT":
handleQuit(channelContext, sessionContext);
break;
default:
Pop3SessionContext.sendErr(channelContext, "Unknown command or command not allowed.");
}
}
private void handleTransactionState(String command, String[] parts, ChannelContext channelContext, Pop3SessionContext sessionContext) {
String username = sessionContext.getUsername(); // 获取当前用户名
switch (command) {
case "STAT":
int[] stat = mailboxService.getStat(sessionContext.getUsername());
Pop3SessionContext.sendOk(channelContext, stat[0] + " " + stat[1]);
break;
case "TOP":
if (parts.length < 2) {
Pop3SessionContext.sendErr(channelContext, "Message number and number of lines required.");
return;
}
String[] topArgs = parts[1].split("\\s+");
if (topArgs.length < 2) {
Pop3SessionContext.sendErr(channelContext, "Message number and number of lines required.");
return;
}
try {
int msgId = Integer.parseInt(topArgs[0]);
int lines = Integer.parseInt(topArgs[1]);
String content = mailboxService.getMessageContent(username, msgId);
if (content != null) {
Pop3SessionContext.sendOk(channelContext, "Top of message follows");
// 简单的模拟实现:返回邮件头和正文的前几行
String[] contentLines = content.split("\r\n");
StringBuilder topResponse = new StringBuilder();
boolean inBody = false;
int bodyLinesCount = 0;
for (String line : contentLines) {
topResponse.append(line).append("\r\n");
if (line.isEmpty()) { // 空行是邮件头和体的分隔符
inBody = true;
}
if (inBody && !line.isEmpty()) {
bodyLinesCount++;
}
if (inBody && bodyLinesCount >= lines) {
break; // 正文行数已达到要求
}
}
topResponse.append(".");
Tio.send(channelContext, new Pop3Packet(topResponse.toString() + "\r\n"));
log.info("POP3 >>>\n{}", topResponse.toString().trim());
} else {
Pop3SessionContext.sendErr(channelContext, "No such message.");
}
} catch (NumberFormatException e) {
Pop3SessionContext.sendErr(channelContext, "Invalid arguments for TOP command.");
}
break;
case "LIST":
List<Integer> sizes = mailboxService.listMessages(username);
Pop3SessionContext.sendOk(channelContext, sizes.size() + " messages");
StringBuilder listResponse = new StringBuilder();
for (int i = 0; i < sizes.size(); i++) {
listResponse.append(i + 1).append(" ").append(sizes.get(i)).append("\r\n");
}
listResponse.append(".");
Tio.send(channelContext, new Pop3Packet(listResponse.toString() + "\r\n"));
log.info("POP3 >>>\n{}", listResponse.toString().trim());
break;
case "UIDL":
List<String> uids = mailboxService.listUids(username);
Pop3SessionContext.sendOk(channelContext, "Unique-ID listing follows");
StringBuilder uidlResponse = new StringBuilder();
for (int i = 0; i < uids.size(); i++) {
// 序号 (i+1) 和 唯一ID
uidlResponse.append(i + 1).append(" ").append(uids.get(i)).append("\r\n");
}
uidlResponse.append(".");
Tio.send(channelContext, new Pop3Packet(uidlResponse.toString() + "\r\n"));
log.info("POP3 >>>\n{}", uidlResponse.toString().trim());
break;
case "RETR":
if (parts.length < 2) {
Pop3SessionContext.sendErr(channelContext, "Message ID required.");
return;
}
try {
int msgId = Integer.parseInt(parts[1]);
String content = mailboxService.getMessageContent(sessionContext.getUsername(), msgId);
if (content != null) {
Pop3SessionContext.sendOk(channelContext, "Message " + msgId + " follows");
Pop3SessionContext.sendData(channelContext, content);
} else {
Pop3SessionContext.sendErr(channelContext, "No such message.");
}
} catch (NumberFormatException e) {
Pop3SessionContext.sendErr(channelContext, "Invalid message ID.");
}
break;
case "DELE":
// TODO: 实现标记删除逻辑
Pop3SessionContext.sendOk(channelContext, "Message marked for deletion.");
break;
case "NOOP":
Pop3SessionContext.sendOk(channelContext, "");
break;
case "RSET":
// TODO: 实现取消删除标记的逻辑
Pop3SessionContext.sendOk(channelContext, "Deletion marks removed.");
break;
case "QUIT":
handleQuit(channelContext, sessionContext);
break;
default:
Pop3SessionContext.sendErr(channelContext, "Unknown command.");
}
}
private void handleQuit(ChannelContext channelContext, Pop3SessionContext sessionContext) {
sessionContext.setState(Pop3SessionContext.State.UPDATE);
// TODO: 在这里执行真正的删除操作
Pop3SessionContext.sendOk(channelContext, "tio-mail-wing POP3 server signing off.");
Tio.close(channelContext, "Client requested QUIT.");
}
}
Pop3ServerConfig
// src/main/java/com/tio/mail/wing/config/Pop3ServerConfig.java
package com.tio.mail.wing.config;
import java.io.IOException;
import com.litongjava.annotation.AConfiguration;
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;
@AConfiguration
@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;
// 设置心跳,-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);
}
}
}
测试
telnet
现在,你的 POP3 服务器已经具备了基础的协议交互能力。你可以使用 telnet
工具来手动测试它。
前提:
- 确保你的
tio-mail-wing
项目正在运行。 - 打开你的命令行工具(Windows 的 CMD/PowerShell,macOS/Linux 的 Terminal)。
测试步骤:
连接服务器 输入以下命令并回车。如果你的端口不是 110,请替换。
telnet localhost 110
如果连接成功,服务器会返回欢迎信息:
+OK tio-mail-wing POP3 server ready.
发送用户名 输入
USER
命令。USER user1@tio.com
服务器响应:
+OK Password required for user1@tio.com
发送密码 输入
PASS
命令。PASS 00000000
服务器响应,表示登录成功:
+OK Mailbox open.
查看邮箱状态 输入
STAT
命令。STAT
服务器返回邮件数量和总大小(来自我们模拟的
MailboxService
):+OK 2 320
列出邮件列表 输入
LIST
命令。LIST
服务器返回多行响应:
+OK 2 messages 1 120 2 200 .
读取第一封邮件 输入
RETR 1
命令。RETR 1
服务器返回邮件的完整内容:
+OK Message 1 follows From: sender@example.com To: user1@tio.com Subject: Test Mail 1 This is the body of the first email. .
退出 输入
QUIT
命令。QUIT
服务器响应并关闭连接:
+OK tio-mail-wing POP3 server signing off.
telnet
会话将在此处终止。
如果你能顺利完成以上所有步骤,并看到预期的响应,那么恭喜你,第一阶段的 POP3 服务核心协议实现已经成功完成!
下一步就是将 UserService
和 MailboxService
对接到真实的数据库或文件系统(如 Maildir 格式)。
curl
C:\Users\Administrator>curl -v --url "pop3://localhost:110" --user "user1@tio.com:00000000"
* Host localhost:110 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:110...
* Connected to localhost (::1) port 110
< +OK tio-mail-wing POP3 server ready.
> CAPA
< +OK Capability list follows
< TOP
< USER
< UIDL
< PIPELINING
< .
> USER user1@tio.com
< +OK Password required for user1@tio.com
> PASS pass1
< +OK Mailbox open.
> LIST
< +OK 2 messages
1 105
2 97
* Connection #0 to host localhost left intact
C:\Users\Administrator>curl -v --url "pop3://localhost:110/1" --user "user1@tio.com:00000000"
* Host localhost:110 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:110...
* Connected to localhost (::1) port 110
< +OK tio-mail-wing POP3 server ready.
> CAPA
< +OK Capability list follows
< TOP
< USER
< UIDL
< PIPELINING
< .
> USER user1@tio.com
< +OK Password required for user1@tio.com
> PASS pass1
< +OK Mailbox open.
> RETR 1
< +OK Message 1 follows
From: sender@example.com
To: user1@tio.com
Subject: Test Mail 1
This is the body of the first email.
* Connection #0 to host localhost left intact