从零开始用Go实现SMTP服务器 – 有限状态机、原始TCP和基于缓冲区的I/O
我使用 AWS SES 已有一段时间,意识到自己将 SMTP 当作一个黑箱。为了更好地理解这个协议,我从零开始用 Go 语言构建了一个邮件传输代理(MTA)。它使用原始 TCP 套接字处理完整的 SMTP 生命周期(RFC 5321),而不是使用高级框架。
工程挑战:
有限状态机(FSM):SMTP 是严格有状态的。我实现了一个 FSM 来强制命令顺序(例如,防止在 MAIL FROM 之前发送 DATA)。这确保了协议违规在套接字级别被捕获,并返回适当的 503 Bad Sequence 代码。
基于缓冲区的处理:使用 bufio.Scanner 来处理字节流。最大的难点是 DATA 阶段的逻辑——在有效管理内存的同时,使用 strings.Builder 正确检测 \r\n.\r\n 序列。
并发性:利用 Go 的 Accept() 循环为每个会话生成独立的 goroutine,确保到 Gmail 的中继延迟(通过 STARTTLS)不会阻塞监听器。
ISP 解决方案:默认配置为在 2525 端口运行,以绕过常见 ISP 对 25 端口的封锁。
实现的状态代码:
我实现了 RFC 5321 代码的一个子集,包括 220(服务就绪)、354(开始输入),以及对 501(语法)和 451(本地错误)的错误处理。
我为什么要构建这个:
大多数现代教程停留在“如何使用库发送电子邮件”。我想看看“点填充”机制是如何工作的,以及服务器如何在原始连接上实际协商多步骤握手。
我很想听听我可能遗漏的边缘案例——特别是在处理格式错误的头部或在负载下管理长时间 TCP 连接方面。
源代码:https://github.com/Jyotishmoy12/SMTP_Server
查看原文
I’ve been using AWS SES for a while and realized I treated SMTP as a black box. To understand the protocol better, I built an MTA (Mail Transfer Agent) from scratch in Go. It handles the full SMTP lifecycle (RFC 5321) using raw TCP sockets instead of high-level frameworks.<p>The Engineering Challenges:<p>Finite State Machine (FSM): SMTP is strictly stateful. I implemented an FSM to enforce command sequencing (e.g., preventing DATA before MAIL FROM). It ensures that protocol violations are caught at the socket level with proper 503 Bad Sequence codes.<p>Buffer-Oriented Processing: Used bufio.Scanner to handle the byte stream. The biggest hurdle was the DATA phase logic—properly detecting the \r\n.\r\n sequence while managing memory efficiently using strings.Builder.<p>Concurrency: Leveraged Go's Accept() loop to spawn independent goroutines for each session, ensuring that the relay latency to Gmail (via STARTTLS) doesn't block the listener.<p>ISP Workarounds: Configured to run on port 2525 by default to bypass the common ISP block on port 25.<p>Status Codes Implemented:
I implemented a subset of RFC 5321 codes, including 220 (Service Ready), 354 (Start Input), and error handling for 501 (Syntax) and 451 (Local Error).<p>Why I built this:
Most modern tutorials stop at "How to send an email with a library." I wanted to see how the "dot-stuffing" mechanism worked and how a server actually negotiates a multi-step handshake over a raw connection.<p>I’d love to hear about edge cases I might have missed—specifically around handling malformed headers or managing long-lived TCP connections under load.<p>Source Code: https://github.com/Jyotishmoy12/SMTP_Server