Skip to content

任务4:实现 POP3 服务(数据库版本) ​

本任务旨在基于 Tio 框架和已有的服务层(MwUserService、MailService 等)实现一个支持数据库交互的 POP3 服务器。本文档介绍了各个组件的功能及配置方法,并保留了原有代码示例。


项目结构 ​

text
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.MailService    // 邮箱与消息查询服务

配置与启动:Pop3ServerConfig ​

负责读取配置、创建 Tio 服务实例并启动监听。

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

import java.io.IOException;

import nexus.io.annotation.Initialization;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.server.ServerTioConfig;
import nexus.io.tio.server.TioServer;
import nexus.io.tio.server.intf.ServerAioHandler;
import nexus.io.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 ​

用于跟踪当前连接的状态与用户身份。

java
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,负责协议行的解码、响应编码及命令分发。

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

import java.nio.ByteBuffer;

import nexus.io.aio.Packet;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
import nexus.io.tio.core.TioConfig;
import nexus.io.tio.core.exception.LengthOverflowException;
import nexus.io.tio.core.exception.TioDecodeException;
import nexus.io.tio.core.utils.ByteBufferUtils;
import nexus.io.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)的命令。

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

import java.util.List;

import nexus.io.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 MailService mailboxService = Aop.get(MailService.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 等命令。
  • 依赖 MailService 完成邮件统计、列表、UID 查询和内容获取,不在此处展开。

注意事项 ​

  1. 服务端口:确保 mail.server.pop3.port 已在环境或配置文件中正确设置。
  2. 事务结束:在 QUIT 命令后,服务端会切换到 UPDATE 状态并关闭连接。
  3. 并发处理:可根据需求调整 serverTioConfig.setWorkerThreads(...)。
  4. 扩展能力:如需支持 STLS、APOP 等功能,可在 handleAuthorizationState 中增补。

以上即为 POP3 服务数据库版本的完整实现文档。可根据实际业务扩展或细化日志和异常处理逻辑。

MIT Licensed