如果在云端,一名邮差

用 Rust 构建电子邮件系统

技术
技术 Rust SMTP

2026-03-21

简单邮件传输协议SMTP)是一种通过网络传输电子邮件(email)的技术标准。它是一种邮件传递协议,而非邮件检索协议。邮政服务将邮件传递到邮箱,但收件人仍然必须从邮箱中提取邮件。同样,SMTP 将电子邮件传递到某个电子邮件提供商的邮件服务器,但需要使用其他协议来从邮件服务器检索该电子邮件,以便收件人读取邮件。

你或许也见过互联网消息访问协议 (IMAP) 和邮局协议 (POP) 。这两者用于从邮件服务器检索电子邮件并将其发送到最终目的地。电子邮件客户端必须从传送链中的最后一个邮件服务器检索电子邮件,才能将其显示给用户。为此目的,客户端使用 IMAP 或 POP 协议,而非 SMTP。

SMTP、IMAP 和 POP
SMTP、IMAP 和 POP

要理解 SMTP 和 IMAP/POP 之间的差异,可以想象一下木板和绳子的不同之处。一块木板可将某物向前推,但不能将它拉回来;绳子可以拉动某物,但不能推动它。同样,SMTP 将电子邮件“推”到邮件服务器,而 IMAP 和 POP 将它“拉”到接收者的应用程序中。

构建简易 SMTP 服务器

我们先利用标准库提供的网络组件来创建服务器的 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 前,让我们先深入探讨一下之前忽略的内容。

SMTP 的设计细节

在 RFC 5321 中规定,SMTP 传输的邮件对象包含一个信封和内容。

SMTP 命令是预定义的基于文本的指令,它告诉客户端或服务器要采取什么操作及如何处理伴随的数据。主要有:

某些命令不允许携带参数,例如 HELO/EHLOQUIT;而另外一些命令则出现在信封中。

SMTP 行表示连接双方所传输的每一行信息。RFC 强制要求所有的换行都是 <CR><LF>

SMTP 的连接流程

SMTP 使用传输控制协议(TCP)作为传输协议,因此第一步从客户端和服务器之间建立 TCP 连接开始。然后服务器向客户端发送响应 220 <domain> 表示服务已就绪。这里的 <domain> 字段是给人看的,标识了服务器的域名或者其他信息,而客户端会自动忽略。

接下来,电子邮件客户端以一个专门的 HELOEHLO 命令请求建立一个新的 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!,表明它已经成功接收客户端发来的邮件了。恭喜!

参考资料