简单邮件传输协议(SMTP)是一种通过网络传输电子邮件(email)的技术标准。它是一种邮件传递协议,而非邮件检索协议。邮政服务将邮件传递到邮箱,但收件人仍然必须从邮箱中提取邮件。同样,SMTP 将电子邮件传递到某个电子邮件提供商的邮件服务器,但需要使用其他协议来从邮件服务器检索该电子邮件,以便收件人读取邮件。
你或许也见过互联网消息访问协议 (IMAP) 和邮局协议 (POP) 。这两者用于从邮件服务器检索电子邮件并将其发送到最终目的地。电子邮件客户端必须从传送链中的最后一个邮件服务器检索电子邮件,才能将其显示给用户。为此目的,客户端使用 IMAP 或 POP 协议,而非 SMTP。

要理解 SMTP 和 IMAP/POP 之间的差异,可以想象一下木板和绳子的不同之处。一块木板可将某物向前推,但不能将它拉回来;绳子可以拉动某物,但不能推动它。同样,SMTP 将电子邮件“推”到邮件服务器,而 IMAP 和 POP 将它“拉”到接收者的应用程序中。
我们先利用标准库提供的网络组件来创建服务器的 main 函数,它监听端口 25 并使用 handle_client 处理连接:
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
fn main() -> anyhow::Result<()> {
let listener = TcpListener::bind("127.0.0.1:25")?;
println!("SMTP server running on 127.0.0.1:25");
for stream in listener.incoming() {
match stream {
Ok(stream) => {
println!("Client connected.");
handle_client(stream)?;
}
Err(e) => println!("Connection error: {}", e),
}
}
Ok(())
}
在探讨如何具体编写 handle_client 前,让我们先深入探讨一下之前忽略的内容。
在 RFC 5321 中规定,SMTP 传输的邮件对象包含一个信封和内容。
DATA 协议单元中发送,本质上是文本。其包含两个部分:头和正文。
SMTP 命令是预定义的基于文本的指令,它告诉客户端或服务器要采取什么操作及如何处理伴随的数据。主要有:
HELO/EHLO 命令相当于打招呼,让客户端和服务器之间启动 SMTP 连接。现在的客户端优先使用 EHLO 而不是较旧的 HELO。MAIL FROM 命令告诉服务器谁在发送该电子邮件。如果 Alice 试图给她的朋友 Bob 发电子邮件,客户端可能会发送 MAIL FROM:<alice@example.com>。RCPT TO 命令用于列出电子邮件的收件人。如果有多个收件人,客户端可多次发送该命令。DATA 命令放在邮件对象的内容前,相当于告诉服务器“我要开始发送内容了”。RSET 命令重置连接,删除所有以前传输的信息,但不关闭 SMTP 连接。RSET 在客户端发送了错误信息的情况下使用。QUIT 命令用于结束连接。某些命令不允许携带参数,例如 HELO/EHLO 和 QUIT;而另外一些命令则出现在信封中。
SMTP 行表示连接双方所传输的每一行信息。RFC 强制要求所有的换行都是 <CR><LF>。
SMTP 使用传输控制协议(TCP)作为传输协议,因此第一步从客户端和服务器之间建立 TCP 连接开始。然后服务器向客户端发送响应 220 <domain> 表示服务已就绪。这里的 <domain> 字段是给人看的,标识了服务器的域名或者其他信息,而客户端会自动忽略。
接下来,电子邮件客户端以一个专门的 HELO 或 EHLO 命令请求建立一个新的 SMTP 连接。
然后客户端向服务器发送信封——或者说是一系列命令:邮件头(包括目的地和主题行等参数)、邮件正文以及任何其他组件。
当开始发送邮件内容时,客户端先发送一个 DATA。服务器会对 DATA 发送 354 响应,然后将该命令之后的各行(以 <CR><LF> 序列结尾的字符串)视为来自发件人的邮件内容。
前面提到邮件内容包括头和正文,每条邮件头独占一行,头和正文之间用空行分隔。
邮件内容由仅包含一个句点的一行终止,即字符序列 <CR><LF>.<CR><LF>,其中第一个 <CR><LF> 实际上是前一行末尾的换行符。
最后客户端发送 QUIT 来关闭连接。此时,服务器将不会从客户端接受任何额外电子邮件数据,除非客户端打开新的 SMTP 连接。
对于上述过程,从发起连接到结束大致的流程如下:
发送端 服务器
│ │
├───────────────TCP connection established──────────────┤
│ │
│EHLO ─────────────────────────────────────────────────>│
│ │
│<─────────────────────────────────────────────── 250 OK│
│ │
│MAIL FROM: <sender@example.com> ──────────────────────>│
│ │
│<─────────────────────────────────────────────── 250 OK│
│ │
│RCPT TO: <receiver@example.com> ──────────────────────>│
│ │
│<─────────────────────────────────────────────── 250 OK│
│ │
│DATA ─────────────────────────────────────────────────>│
│ │
│<────────────────── 354 End data with <CR><LF>.<CR><LF>│
│ │
│From: sender@example.com ─────────────────────────────>│
│To: receiver@example.com ─────────────────────────────>│
│Subject: Test Email ──────────────────────────────────>│
│Test email body ──────────────────────────────────────>│
│. ────────────────────────────────────────────────────>│
│ │
│<─────────────────────────────────────────────── 250 OK│
│ │
│QUIT ─────────────────────────────────────────────────>│
│ │
│<────────────────────────────────────────────── 221 Bye│
V V
下面是 handle_client 的具体实现。它通过识别每次客户端传来的命令来判断连接进行到了何处。为了更清晰地呈现整个连接过程,我们选择打印出服务器每次接收到的和发送的具体内容。
fn handle_client(mut stream: TcpStream) -> anyhow::Result<()> {
let mut reader = BufReader::new(stream.try_clone()?);
stream.write_all(b"220 localhost Simple Rust SMTP Server\r\n")?;
println!("[SEND] 220 localhost Simple Rust SMTP Server\\r\\n");
let mut line = String::new();
loop {
line.clear();
reader.read_line(&mut line)?;
if line.is_empty() {
break;
}
println!("[RECV] {:?}", line);
if line.starts_with("EHLO") || line.starts_with("HELO") {
stream.write_all(b"250 Hello\r\n")?;
println!("[SEND] 250 Hello\\r\\n");
} else if line.starts_with("MAIL FROM") {
stream.write_all(b"250 OK\r\n")?;
println!("[SEND] 250 OK\\r\\n");
} else if line.starts_with("RCPT TO") {
stream.write_all(b"250 OK\r\n")?;
println!("[SEND] 250 OK\\r\\n");
} else if line.starts_with("DATA") {
stream.write_all(b"354 End data with <CRLF>.<CRLF>\r\n")?;
println!("[SEND] 354 End data with <CRLF>.<CRLF>\\r\\n");
loop {
line.clear();
reader.read_line(&mut line)?;
if line.trim() == "." {
println!("[RECV] {:?}", line);
break;
}
println!("[RECV] {:?}", line);
}
stream.write_all(b"250 Message accepted\r\n")?;
println!("[SEND] 250 Message accepted\\r\\n");
} else if line.starts_with("QUIT") {
stream.write_all(b"221 Bye\r\n")?;
println!("[SEND] 221 Bye\\r\\n");
break;
} else {
stream.write_all(b"250 OK\r\n")?;
println!("[SEND] 250 OK\\r\\n");
}
}
Ok(())
}
我们可以使用 lettre 库来创建 SMTP 邮件并发送至本地 25 端口。这里采用的是同步而非异步地发送邮件,因为我们仅用它来测试服务器。为此,我们需要在服务器正常运行的情况下执行该程序,否则会报错。
use lettre::{Message, SmtpTransport, Transport, message::Mailbox};
fn main() -> anyhow::Result<()> {
// 创建一封可被格式化的电子邮件
let email = Message::builder()
// 设置 `From` 头
.from("sender@example.com".parse::<Mailbox>()?)
// 设置 `To` 头
.to("receiver@example.com".parse::<Mailbox>()?)
// 设置 `Subject` 头
.subject("Test Email from lettre")
// 设置邮件正文
.body(String::from("Hello from lettre client!"))?;
// 创建一个本地 SMTP 客户端
// 默认连接端口 25
let mailer = SmtpTransport::unencrypted_localhost();
// 向端口 25 发送电子邮件
match mailer.send(&email) {
Ok(_) => println!("成功向端口 25 发送邮件。"),
Err(e) => println!("Error: {:?}", e),
}
Ok(())
}
运行客户端,服务器应该会打印类似下面的信息:
SMTP server running on 127.0.0.1:25
Client connected.
[SEND] 220 localhost Simple Rust SMTP Server\r\n
[RECV] "EHLO 你的设备名称\r\n"
[SEND] 250 Hello\r\n
[RECV] "MAIL FROM:<sender@example.com>\r\n"
[SEND] 250 OK\r\n
[RECV] "RCPT TO:<receiver@example.com>\r\n"
[SEND] 250 OK\r\n
[RECV] "DATA\r\n"
[SEND] 354 End data with <CRLF>.<CRLF>\r\n
[RECV] "From: sender@example.com\r\n"
[RECV] "To: receiver@example.com\r\n"
[RECV] "Subject: Test Email from lettre\r\n"
[RECV] "Content-Transfer-Encoding: 7bit\r\n"
[RECV] "Date: Sun, 22 Mar 2026 13:17:11 +0000\r\n"
[RECV] "\r\n"
[RECV] "Hello from lettre client!\r\n"
[RECV] ".\r\n"
[SEND] 250 Message accepted\r\n
[RECV] "QUIT\r\n"
[SEND] 221 Bye\r\n
当你看到服务器打印出 Hello from lettre client!,表明它已经成功接收客户端发来的邮件了。恭喜!