任务2:实现 SMTP 服务
SMTP 协议详解 (Simple Mail Transfer Protocol)
SMTP,全称为“简单邮件传输协议”,是互联网上传输电子邮件的核心协议。如果说 POP3 是“收信”的协议,那么 SMTP 就是“寄信和送信”的协议。
核心思想: 你写好一封信,把它投进邮筒。邮递员(你的 SMTP 服务器)会收走这封信,然后根据地址,把它送到收件人所在地的邮局(收件人的邮件服务器)。这个投递和中转的过程,就是 SMTP 在做的事情。
SMTP 是一个“推”协议,即客户端主动将邮件“推送”到服务器。它主要用于两个场景:
- 邮件客户端到邮件服务器:例如,你用 Foxmail 写完邮件点击“发送”,Foxmail 就是通过 SMTP 协议将邮件发送到你的发件箱服务器(如
smtp.qq.com
)。 - 邮件服务器到邮件服务器:你的发件箱服务器(
smtp.qq.com
)收到邮件后,会通过 SMTP 协议将邮件传输到收件人的邮件服务器(如mx.google.com
)。
主要特点
文本基础 (Text-Based): 和 POP3 一样,SMTP 也是一个基于文本的、一问一答式的协议,命令和响应都是 ASCII 字符串。
推送模型 (Push Protocol): 客户端主动发起连接并将数据推送到服务器,用于发送邮件。
状态化会话 (Stateful Session): SMTP 会话有明确的阶段。服务器会根据客户端已执行的命令来决定下一步允许执行哪些命令。例如,必须在
MAIL FROM
之后才能执行RCPT TO
。中继功能 (Relay Function): SMTP 服务器的核心功能之一是邮件中继。如果收件人不在本服务器上,它会负责将邮件转发到正确的下一个服务器。
认证机制 (Authentication): 为了防止垃圾邮件泛滥,现代 SMTP 服务(尤其是客户端到服务器的连接)都要求发件人进行身份认证(
AUTH
命令),确认是合法用户后才允许其发送邮件。
SMTP 会话的典型流程
一个 SMTP 会话通常遵循以下步骤:
建立连接 (Connection Establishment)
- 客户端连接到服务器的 25 端口(或加密的 465/587 端口)。
- 服务器发送一个
220
状态码的欢迎信息,表示服务已就绪。
客户端问候 (Client Greeting)
- 客户端发送
HELO
或更现代的EHLO
(Extended HELO) 命令,并带上自己的域名。EHLO
会让服务器返回它支持的所有扩展功能列表。 - 服务器响应
250
状态码,表示问候被接受。
- 客户端发送
身份认证 (Authentication) (可选但关键)
- 如果服务器要求认证,客户端发送
AUTH LOGIN
命令。 - 服务器会通过
334
状态码,分两步要求客户端提供 Base64 编码的用户名和密码。 - 认证成功后,服务器返回
235 Authentication successful
。
- 如果服务器要求认证,客户端发送
邮件事务 (Mail Transaction) 这是一个原子操作序列,用于传输一封邮件。
MAIL FROM:<sender@example.com>
: 客户端指定发件人地址。服务器成功后响应250 OK
。这标志着一个新邮件事务的开始。RCPT TO:<recipient@domain.com>
: 客户端指定一个或多个收件人地址。每指定一个,服务器就检查该地址是否有效/可接收,并响应250 OK
。DATA
: 客户端告知服务器,接下来要发送邮件的具体内容(邮件头和邮件体)。服务器响应354 Start mail input; end with <CRLF>.<CRLF>
,表示已准备好接收。
数据传输 (Data Transfer)
- 客户端开始逐行发送邮件的完整内容(遵循 RFC 5322 格式)。
- 当所有内容发送完毕后,客户端发送一个只包含一个句点
.
的行来表示结束。
事务结束 (Transaction Completion)
- 服务器成功接收并处理完邮件数据后(例如,存入收件人邮箱或准备中继),会响应
250 OK: queued as ...
。
- 服务器成功接收并处理完邮件数据后(例如,存入收件人邮箱或准备中继),会响应
关闭连接 (Connection Termination)
- 客户端发送
QUIT
命令。 - 服务器响应
221 Bye
,然后关闭 TCP 连接。
- 客户端发送
常用 SMTP 命令详解
命令 | 格式 | 作用 | 示例 | 可能的响应 |
---|---|---|---|---|
HELO/EHLO | EHLO client.domain | 客户端向服务器问候,并获取扩展功能 | EHLO mycomputer.local | 250-mail.tio.com 250-AUTH LOGIN 250 OK |
AUTH LOGIN | AUTH LOGIN | 开始登录认证流程 | AUTH LOGIN | 334 VXNlcm5hbWU6 (Base64 of "Username:") |
(用户名) | <base64_user> | 发送 Base64 编码的用户名 | dXNlcjJAdGlvLmNvbQ== | 334 UGFzc3dvcmQ6 (Base64 of "Password:") |
(密码) | <base64_pass> | 发送 Base64 编码的密码 | MDAwMDAwMDA= | 235 Authentication successful / 535 Auth failed |
MAIL FROM | MAIL FROM:<addr> | 指定发件人 | MAIL FROM:<user2@tio.com> | 250 OK |
RCPT TO | RCPT TO:<addr> | 指定收件人 | RCPT TO:<user1@tio.com> | 250 OK / 550 No such user |
DATA | DATA | 告知服务器将开始发送邮件内容 | DATA | 354 Start mail input... |
(结束符) | . | 在单独一行,表示邮件内容结束 | . | 250 OK: queued |
RSET | RSET | 重置当前邮件事务,中止邮件发送 | RSET | 250 OK |
QUIT | QUIT | 结束会话 | QUIT | 221 Bye |
一个完整的 Telnet 交互示例
下面是一个手动使用 telnet
通过 SMTP 发送邮件的完整流程。
# 1. 连接到服务器的 25 端口
telnet localhost 25
# 2. 服务器返回欢迎信息
S: 220 tio-mail-wing ESMTP Service ready
# 3. 客户端问候
C: EHLO client.example.com
S: 250-tio-mail-wing says hello
S: 250 AUTH LOGIN
# 4. 客户端开始认证
C: AUTH LOGIN
S: 334 VXNlcm5hbWU6
# 5. 客户端发送 Base64 编码的用户名 (user2@tio.com)
C: dXNlcjJAdGlvLmNvbQ==
S: 334 UGFzc3dvcmQ6
# 6. 客户端发送 Base64 编码的密码 (00000000)
C: MDAwMDAwMDA=
S: 235 Authentication successful
# 7. 客户端开始邮件事务,指定发件人
C: MAIL FROM:<user2@tio.com>
S: 250 OK
# 8. 客户端指定收件人
C: RCPT TO:<user1@tio.com>
S: 250 OK
# 9. 客户端准备发送邮件内容
C: DATA
S: 354 Start mail input; end with <CRLF>.<CRLF>
# 10. 客户端发送邮件内容(头和体)
Message-ID: <28e9cecf-2cc9-40b6-91bb-8ae4e98155e8@tio.com>
Date: Fri, 27 Jun 2025 13:21:28 -1000
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Content-Language: en-US
To: user1@tio.com
From: user2 imap <user2@tio.com>
Subject: test2
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
test3
.
# 11. 服务器确认邮件已接收
S: 250 OK: queued as 1c175f27-44e8-4972-95d0-614245160740
# 12. 客户端退出
C: QUIT
S: 221 Bye
# 连接已由外部主机关闭。
官方文档和资源链接
RFC 5321 - Simple Mail Transfer Protocol
- 链接: https://www.rfc-editor.org/rfc/rfc5321
- 说明: 这是当前 SMTP 的主要标准,定义了所有核心命令、响应码和操作流程。
RFC 4954 - SMTP Service Extension for Authentication
- 链接: https://www.rfc-editor.org/rfc/rfc4954
- 说明: 定义了
AUTH
命令,包括AUTH LOGIN
、AUTH PLAIN
等认证机制。
实现SMTP服务
第一步:创建 SmtpPacket
和 SmtpSessionContext
1. SmtpPacket.java
与 Pop3Packet
类似,用于封装命令和响应。
// src/main/java/com/tio/mail/wing/packet/SmtpPacket.java
package com.tio.mail.wing.packet;
import com.litongjava.aio.Packet;
@SuppressWarnings("serial")
public class SmtpPacket extends Packet {
private String line;
public SmtpPacket(String line) {
this.line = line;
}
public String getLine() {
return line;
}
}
2. SmtpSessionContext.java
SMTP 的会话状态比 POP3 复杂,这个类是管理状态机的关键。
// src/main/java/com/tio/mail/wing/handler/SmtpSessionContext.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.SmtpPacket;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class SmtpSessionContext {
// SMTP 会话状态机
public enum State {
/** 初始连接,等待 HELO/EHLO */
CONNECTED,
/** 已问候,等待认证或邮件事务 */
GREETED,
/** 已发送 AUTH LOGIN,等待 Base64 用户名 */
AUTH_WAIT_USERNAME,
/** 已收到用户名,等待 Base64 密码 */
AUTH_WAIT_PASSWORD,
/** 已收到 MAIL FROM,等待 RCPT TO 或 DATA */
MAIL_FROM_RECEIVED,
/** 已收到 RCPT TO,等待更多 RCPT TO 或 DATA */
RCPT_TO_RECEIVED,
/** 正在接收邮件内容 */
DATA_RECEIVING,
/** 准备关闭 */
QUIT
}
private State state = State.CONNECTED;
private boolean authenticated = false;
private String username; // 认证后的用户名
// 用于一封邮件的临时数据
private String fromAddress;
private List<String> toAddresses = new ArrayList<>();
private StringBuilder mailContent = new StringBuilder();
/**
* 重置邮件事务状态,以便在同一连接中发送下一封邮件
*/
public void resetTransaction() {
this.fromAddress = null;
this.toAddresses.clear();
this.mailContent.setLength(0);
// 认证状态保留,但事务状态回到 GREETED
this.state = State.GREETED;
}
/**
* 发送响应
* @param context ChannelContext
* @param code 响应码 (e.g., 220, 250, 334)
* @param message 消息
*/
public static void sendResponse(ChannelContext context, int code, String message) {
String response = code + " " + message + "\r\n";
Tio.send(context, new SmtpPacket(response));
}
}
第二步:扩展业务服务
我们需要在 MailboxService
中添加一个保存邮件的方法。
MailboxService.java
(添加新方法)
// src/main/java/com/tio/mail/wing/service/MailboxService.java
// ... (保留原有代码)
/**
* 将接收到的邮件保存到指定用户的邮箱中
* @param username 收件人用户名 (e.g., "user1@tio.com")
* @param emailContent 邮件的完整原始内容 (MIME 格式)
* @return 是否保存成功
*/
public boolean saveEmail(String username, String emailContent) {
List<Email> userEmails = mailboxes.computeIfAbsent(username, k -> new ArrayList<>());
Email newEmail = new Email(emailContent);
userEmails.add(newEmail);
log.info("Saved new email for {} with UID {}", username, newEmail.getUid());
return true;
}
/**
* 检查用户是否存在(用于 RCPT TO 验证)
* @param username 用户名
* @return 是否存在
*/
public boolean userExists(String username) {
// 在真实系统中,这应该查询用户表
// 这里我们用 UserService 的模拟数据来判断
return mailboxes.containsKey(username);
}
第三步:创建 SMTP 的 Listener 和 Handler
1. SmtpServerAioListener.java
负责在客户端连接后发送欢迎消息。
// src/main/java/com/tio/mail/wing/listener/SmtpServerAioListener.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.SmtpSessionContext;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SmtpServerAioListener implements ServerAioListener {
@Override
public void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect) throws Exception {
if (isConnected) {
log.info("SMTP client connected: {}", channelContext.getClientNode());
// 1. 创建会话上下文
SmtpSessionContext sessionContext = new SmtpSessionContext();
channelContext.set("sessionContext", sessionContext);
// 2. 发送欢迎消息 (220)
SmtpSessionContext.sendResponse(channelContext, 220, "tio-mail-wing ESMTP Service ready");
log.info("SMTP >>> 220 Welcome message sent to {}", channelContext.getClientNode());
}
}
@Override
public void onBeforeClose(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) throws Exception {
log.info("SMTP client disconnected: {}", channelContext.getClientNode());
}
@Override
public void onAfterDecoded(ChannelContext channelContext, Packet packet, int packetSize) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void onAfterSent(ChannelContext channelContext, Packet packet, boolean isSentSuccess) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void onAfterHandled(ChannelContext channelContext, Packet packet, long cost) throws Exception {
// TODO Auto-generated method stub
}
@Override
public boolean onHeartbeatTimeout(ChannelContext channelContext, Long interval, int heartbeatTimeoutCount) {
// TODO Auto-generated method stub
return false;
}
}
2. SmtpServerAioHandler.java
这是 SMTP 的核心逻辑处理器,负责解析命令并根据状态机进行响应。
// src/main/java/com/tio/mail/wing/handler/SmtpServerAioHandler.java
package com.tio.mail.wing.handler;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.UUID;
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.SmtpPacket;
import com.tio.mail.wing.service.MailboxService;
import com.tio.mail.wing.service.UserService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SmtpServerAioHandler implements ServerAioHandler {
private UserService userService = Aop.get(UserService.class);
private MailboxService mailboxService = Aop.get(MailboxService.class);
private static final String CHARSET = "UTF-8";
@Override
public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws TioDecodeException {
String line = null;
try {
line = ByteBufferUtils.readLine(buffer, CHARSET);
} catch (LengthOverflowException e) {
e.printStackTrace();
}
if (line == null) {
return null;
}
return new SmtpPacket(line);
}
@Override
public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) {
SmtpPacket smtpPacket = (SmtpPacket) packet;
String line = smtpPacket.getLine();
try {
return ByteBuffer.wrap(line.getBytes(CHARSET));
} catch (Exception e) {
log.error("Encoding error", e);
return null;
}
}
@Override
public void handler(Packet packet, ChannelContext channelContext) throws Exception {
SmtpPacket smtpPacket = (SmtpPacket) packet;
String line = smtpPacket.getLine().trim();
log.info("SMTP <<< {}", line);
SmtpSessionContext session = (SmtpSessionContext) channelContext.get("sessionContext");
// 特殊处理:DATA 状态
if (session.getState() == SmtpSessionContext.State.DATA_RECEIVING) {
handleDataReceiving(line, channelContext, session);
return;
}
String[] parts = line.split("\\s+", 2);
String command = parts[0].toUpperCase();
switch (command) {
case "HELO":
case "EHLO":
handleEhlo(parts, channelContext, session);
break;
case "AUTH":
handleAuth(parts, channelContext, session);
break;
case "MAIL":
handleMail(line, channelContext, session);
break;
case "RCPT":
handleRcpt(line, channelContext, session);
break;
case "DATA":
handleData(channelContext, session);
break;
case "QUIT":
handleQuit(channelContext, session);
break;
case "RSET":
handleRset(channelContext, session);
break;
default:
// 处理认证过程中的 Base64 数据
if (session.getState() == SmtpSessionContext.State.AUTH_WAIT_USERNAME || session.getState() == SmtpSessionContext.State.AUTH_WAIT_PASSWORD) {
handleAuthData(line, channelContext, session);
} else {
SmtpSessionContext.sendResponse(channelContext, 500, "Command not recognized");
}
}
}
private void handleEhlo(String[] parts, ChannelContext ctx, SmtpSessionContext session) {
if (session.getState() != SmtpSessionContext.State.CONNECTED) {
SmtpSessionContext.sendResponse(ctx, 503, "Bad sequence of commands");
return;
}
String domain = parts.length > 1 ? parts[1] : "unknown";
// EHLO 的响应是多行的
Tio.send(ctx, new SmtpPacket("250-tio-mail-wing says hello to " + domain + "\r\n"));
Tio.send(ctx, new SmtpPacket("250 AUTH LOGIN\r\n")); // 声明支持 AUTH LOGIN
session.setState(SmtpSessionContext.State.GREETED);
}
private void handleAuth(String[] parts, ChannelContext ctx, SmtpSessionContext session) {
if (session.getState() != SmtpSessionContext.State.GREETED) {
SmtpSessionContext.sendResponse(ctx, 503, "Bad sequence of commands");
return;
}
if (parts.length > 1 && "LOGIN".equalsIgnoreCase(parts[1])) {
session.setState(SmtpSessionContext.State.AUTH_WAIT_USERNAME);
SmtpSessionContext.sendResponse(ctx, 334, Base64.getEncoder().encodeToString("Username:".getBytes()));
} else {
SmtpSessionContext.sendResponse(ctx, 504, "Authentication mechanism not supported");
}
}
private void handleAuthData(String line, ChannelContext ctx, SmtpSessionContext session) {
try {
String decoded = new String(Base64.getDecoder().decode(line), CHARSET);
if (session.getState() == SmtpSessionContext.State.AUTH_WAIT_USERNAME) {
session.setUsername(decoded);
session.setState(SmtpSessionContext.State.AUTH_WAIT_PASSWORD);
SmtpSessionContext.sendResponse(ctx, 334, Base64.getEncoder().encodeToString("Password:".getBytes()));
} else if (session.getState() == SmtpSessionContext.State.AUTH_WAIT_PASSWORD) {
if (userService.authenticate(session.getUsername(), decoded)) {
session.setAuthenticated(true);
session.setState(SmtpSessionContext.State.GREETED);
SmtpSessionContext.sendResponse(ctx, 235, "Authentication successful");
} else {
session.resetTransaction();
session.setState(SmtpSessionContext.State.GREETED);
SmtpSessionContext.sendResponse(ctx, 535, "Authentication failed");
}
}
} catch (Exception e) {
session.resetTransaction();
session.setState(SmtpSessionContext.State.GREETED);
SmtpSessionContext.sendResponse(ctx, 501, "Invalid base64 data");
}
}
private void handleMail(String line, ChannelContext ctx, SmtpSessionContext session) {
if (session.getState() != SmtpSessionContext.State.GREETED || !session.isAuthenticated()) {
SmtpSessionContext.sendResponse(ctx, 503, "Bad sequence of commands or not authenticated");
return;
}
// 简单解析 MAIL FROM:<address>
String from = line.substring(line.indexOf('<') + 1, line.lastIndexOf('>'));
if (from.isEmpty()) {
SmtpSessionContext.sendResponse(ctx, 501, "Invalid address");
return;
}
session.setFromAddress(from);
session.setState(SmtpSessionContext.State.MAIL_FROM_RECEIVED);
SmtpSessionContext.sendResponse(ctx, 250, "OK");
}
private void handleRcpt(String line, ChannelContext ctx, SmtpSessionContext session) {
if (session.getState() != SmtpSessionContext.State.MAIL_FROM_RECEIVED && session.getState() != SmtpSessionContext.State.RCPT_TO_RECEIVED) {
SmtpSessionContext.sendResponse(ctx, 503, "Bad sequence of commands");
return;
}
String to = line.substring(line.indexOf('<') + 1, line.lastIndexOf('>'));
// 检查收件人是否是本域用户
if (mailboxService.userExists(to)) {
session.getToAddresses().add(to);
session.setState(SmtpSessionContext.State.RCPT_TO_RECEIVED);
SmtpSessionContext.sendResponse(ctx, 250, "OK");
} else {
SmtpSessionContext.sendResponse(ctx, 550, "No such user here");
}
}
private void handleData(ChannelContext ctx, SmtpSessionContext session) {
if (session.getState() != SmtpSessionContext.State.RCPT_TO_RECEIVED) {
SmtpSessionContext.sendResponse(ctx, 503, "Bad sequence of commands");
return;
}
session.setState(SmtpSessionContext.State.DATA_RECEIVING);
SmtpSessionContext.sendResponse(ctx, 354, "Start mail input; end with <CRLF>.<CRLF>");
}
private void handleDataReceiving(String line, ChannelContext ctx, SmtpSessionContext session) {
if (line.equals(".")) {
// 邮件内容接收完毕
String mailData = session.getMailContent().toString();
for (String recipient : session.getToAddresses()) {
mailboxService.saveEmail(recipient, mailData);
}
SmtpSessionContext.sendResponse(ctx, 250, "OK: queued as " + UUID.randomUUID().toString());
// 重置事务,准备接收下一封邮件
session.resetTransaction();
} else {
// 累加邮件内容
session.getMailContent().append(line).append("\r\n");
}
}
private void handleRset(ChannelContext ctx, SmtpSessionContext session) {
session.resetTransaction();
SmtpSessionContext.sendResponse(ctx, 250, "OK");
}
private void handleQuit(ChannelContext ctx, SmtpSessionContext session) {
session.setState(SmtpSessionContext.State.QUIT);
SmtpSessionContext.sendResponse(ctx, 221, "Bye");
Tio.close(ctx, "Client requested QUIT");
}
}
第四步:创建 SMTP 服务器配置
SmtpServerConfig.java
// src/main/java/com/tio/mail/wing/config/SmtpServerConfig.java
package com.tio.mail.wing.config;
import java.io.IOException;
import com.litongjava.annotation.AConfiguration;
import com.litongjava.annotation.Initialization;
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.SmtpServerAioHandler;
import com.tio.mail.wing.listener.SmtpServerAioListener;
import lombok.extern.slf4j.Slf4j;
@AConfiguration
@Slf4j
public class SmtpServerConfig {
@Initialization
public void startSmtpServer() {
SmtpServerAioHandler serverHandler = new SmtpServerAioHandler();
SmtpServerAioListener serverListener = new SmtpServerAioListener();
ServerTioConfig serverTioConfig = new ServerTioConfig("smtp-server");
serverTioConfig.setServerAioHandler(serverHandler);
serverTioConfig.setServerAioListener(serverListener);
serverTioConfig.setHeartbeatTimeout(-1); // SMTP 不需要应用层心跳
TioServer tioServer = new TioServer(serverTioConfig);
try {
int port = EnvUtils.getInt("mail.server.smtp.port", 25);
tioServer.start(null, port);
log.info("Started SMTP server on port: {}", port);
} catch (IOException e) {
log.error("Failed to start SMTP server", e);
}
}
}
测试
现在,你的 SMTP 服务器已经可以运行了。我们可以用多种方式来测试它。
1. 使用 telnet
手动测试
这是最直接的测试方法,可以验证协议的每一步。
前提:
- 确保你的
tio-mail-wing
项目正在运行。 UserService
中有user2@tio.com
/pass2
用于发信认证。MailboxService
中有user1@tio.com
作为收件人。
测试步骤:
连接服务器
telnet localhost 25
服务器响应:
220 tio-mail-wing ESMTP Service ready
问候
EHLO mypc
服务器响应:
250-tio-mail-wing says hello to mypc 250 AUTH LOGIN
认证
AUTH LOGIN
服务器响应:
334 VXNlcm5hbWU6
输入user2@tio.com
的 Base64 编码:dXNlcjJAdGlvLmNvbQ==
服务器响应:
334 UGFzc3dvcmQ6
输入pass2
的 Base64 编码:MDAwMDAwMDA=
服务器响应:
235 Authentication successful
发送邮件
MAIL FROM:<user2@tio.com>
服务器响应:
250 OK
RCPT TO:<user1@tio.com>
服务器响应:
250 OK
DATA
服务器响应:
354 Start mail input; end with <CRLF>.<CRLF>
现在输入邮件内容:Subject: Hello from Telnet! This is a test mail. .
服务器响应:
250 OK: queued as ...
退出
QUIT
服务器响应:
221 Bye
验证结果: 此时,你可以用之前实现的 POP3 服务来验证邮件是否已成功投递。 使用 telnet localhost 110
登录 user1@tio.com
,执行 STAT
或 LIST
,你应该能看到多了一封新邮件。执行 RETR
可以看到刚才发送的内容。
2. 使用邮件客户端 (Foxmail/Outlook/Thunderbird)
这是更真实的测试场景。
- 在你的邮件客户端中,添加一个新账户。
- 选择手动配置,服务器类型选择 POP3/SMTP。
- 接收邮件服务器 (POP3):
- 服务器地址:
localhost
- 端口:
110
- 用户名:
user1@tio.com
- 密码:
pass1
- 加密: 无
- 服务器地址:
- 发送邮件服务器 (SMTP):
- 服务器地址:
localhost
- 端口:
25
- 需要身份验证: 勾选此项
- 用户名:
user2@tio.com
(或者任何你在UserService
中定义的用户) - 密码:
pass2
- 加密: 无
- 服务器地址:
- 配置完成后,尝试给自己(
user1@tio.com
)发送一封邮件。 - 点击“发送”,然后点击“收取”。如果一切正常,你将会在收件箱中看到你刚刚发送的邮件。
如果你能成功完成以上测试,那么恭喜你,你的邮件服务器核心的接收和发送(客户端到服务器)功能已经全部打通!下一步就是实现更复杂的 IMAP 协议和更健壮的数据持久化方案。