1. 引言
电子邮件是互联网上最基础也最常用的应用之一。虽然今天我们大多使用现成的邮件服务(如 Gmail、Outlook、企业邮箱),但亲手编写一套邮件服务器和客户端,能让你深入理解邮件从发送到接收的完整链路,掌握 SMTP、POP3/IMAP 等核心协议的工作原理。
本文将从零开始,用 Python 编写一个可运行的邮件服务器和客户端。服务器负责接收和存储邮件,客户端负责发送和收取邮件。我们会先介绍邮件系统的基本架构,再逐步实现服务器端和客户端,最后给出完整的测试流程。
2. 邮件系统基础架构
在动手写代码之前,先了解邮件系统的基本组成。一个完整的邮件系统通常包含以下几个角色:
-
邮件用户代理(MUA):即邮件客户端,负责撰写、发送和阅读邮件。
-
邮件传输代理(MTA):即邮件服务器,负责邮件的路由和转发。
-
邮件投递代理(MDA) :可以把它理解为「邮局里的分拣员」。当 MTA 把邮件送到收件人所在的服务器后,MDA 负责把邮件放进收件人自己的「信箱」里------也就是写入收件人在服务器上的邮箱存储目录。在本文的示例中,服务器收到
DATA命令后把邮件保存到mailboxes/bob@example.com/目录,这一步就是 MDA 的职责。邮件发送和接收主要依赖以下协议: -
SMTP(简单邮件传输协议):用于客户端向服务器发送邮件,以及服务器之间转发邮件,默认端口 25。
-
POP3(邮局协议第 3 版):用于客户端从服务器下载邮件,默认端口 110。
-
IMAP(互联网消息访问协议):用于客户端在线管理服务器上的邮件,默认端口 143。
本文的示例将实现一个简化版 SMTP 服务器和一个支持 SMTP 发送、POP3 接收的客户端。
3. 环境准备
在开始之前,请确保你的开发环境满足以下要求:
- Python 3.8 及以上版本
- 无需安装任何第三方库,全部使用 Python 标准库实现
你可以先创建一个项目目录,并在其中新建以下文件:
text
mail_project/
├── server.py # 邮件服务器
├── client.py # 邮件客户端
└── mailboxes/ # 邮件存储目录(服务器自动创建)
4. 实现邮件服务器
邮件服务器的核心职责是:监听 SMTP 端口,接收客户端发来的邮件,解析邮件内容,并将邮件保存到对应收件人的邮箱目录中。
4.1 服务器主框架
服务器启动后监听 2525 端口,等待客户端连接。当客户端连上后,服务器先发送欢迎信息,然后进入命令循环,逐行读取客户端发来的命令并作出响应。
下面用类似 telnet 的方式展示服务器启动后,客户端连接并交互的完整流程(S: 表示服务器响应,C: 表示客户端发送的命令):
text
$ python server.py
邮件服务器已启动,监听端口 2525
# 客户端连接
S: 220 Welcome to My Mail Server
# 客户端问候服务器,开始会话
C: HELO alice
S: 250 Hello
# 指定发件人地址
C: MAIL FROM:<alice@example.com>
S: 250 OK
# 指定收件人地址
C: RCPT TO:<bob@example.com>
S: 250 OK
# 开始传输邮件正文
C: DATA
S: 354 End with <CR><LF>.<CR><LF>
# 邮件正文内容(以单独一行的 "." 结束)
C: From: alice@example.com
C: To: bob@example.com
C: Subject: 你好,这是测试邮件
C:
C: 这是一封来自自建邮件服务器的测试邮件。
C: .
S: 250 Message accepted
# 结束会话
C: QUIT
S: 221 Bye
4.2 处理 SMTP 命令
从上面的交互流程可以看到,SMTP 会话由一系列命令组成。我们至少需要实现以下命令:
HELO/EHLO:客户端问候服务器,开始会话。MAIL FROM:指定发件人地址。RCPT TO:指定收件人地址。DATA:开始传输邮件正文内容。QUIT:结束会话。
服务器收到 DATA 命令后进入数据模式,此时客户端发送的所有行都被当作邮件正文收集起来,直到收到单独一行的 . 才结束。随后服务器把邮件保存到收件人的邮箱目录中,并返回 250 Message accepted。
如果客户端发送了无法识别的命令,服务器会返回 500 Command unrecognized;如果客户端在数据模式下发送了空行或非法内容,服务器也会给出相应的错误响应。
5. 实现邮件客户端
邮件客户端需要完成两件事:通过 SMTP 发送邮件,通过 POP3 接收邮件。下面同样用类似 telnet 的方式展示客户端与服务器之间的交互流程。
5.1 发送邮件
客户端通过 SMTP 协议向服务器发送邮件。整个交互流程如下(S: 表示服务器响应,C: 表示客户端发送的命令):
text
# 客户端连接服务器
S: 220 Welcome to My Mail Server
# 客户端问候服务器,开始会话
C: HELO alice
S: 250 Hello
# 指定发件人地址
C: MAIL FROM:<alice@example.com>
S: 250 OK
# 指定收件人地址
C: RCPT TO:<bob@example.com>
S: 250 OK
# 开始传输邮件正文
C: DATA
S: 354 End with <CR><LF>.<CR><LF>
# 邮件正文内容(以单独一行的 "." 结束)
C: From: alice@example.com
C: To: bob@example.com
C: Subject: 你好,这是测试邮件
C:
C: 这是一封来自自建邮件服务器的测试邮件。
C: .
S: 250 Message accepted
# 结束会话
C: QUIT
S: 221 Bye
5.2 接收邮件
客户端通过 POP3 协议从服务器接收邮件。整个交互流程如下(S: 表示服务器响应,C: 表示客户端发送的命令):
text
# 客户端连接服务器
S: +OK POP3 server ready
# 客户端发送用户名进行认证
C: USER bob@example.com
S: +OK User accepted
# 客户端发送密码完成认证
C: PASS 123456
S: +OK Mailbox locked and ready
# 客户端列出邮箱中的邮件
C: LIST
S: +OK 1 messages
S: 1 320
S: .
# 客户端读取第 1 封邮件
C: RETR 1
S: +OK message follows
S: From: alice@example.com
S: To: bob@example.com
S: Subject: 你好,这是测试邮件
S:
S: 这是一封来自自建邮件服务器的测试邮件。
S: .
# 客户端结束会话
C: QUIT
S: +OK Bye
6. 扩展:增加 POP3 服务器支持
上面的客户端示例中,接收邮件依赖一个 POP3 服务器。为了让整套系统完全自包含,我们可以在服务器端也实现一个简单的 POP3 服务。POP3 服务器的交互流程如下(S: 表示服务器响应,C: 表示客户端发送的命令):
text
# 客户端连接服务器
S: +OK POP3 server ready
# 客户端发送用户名进行认证
C: USER bob@example.com
S: +OK User accepted
# 客户端发送密码完成认证
C: PASS 123456
S: +OK Mailbox locked and ready
# 客户端列出邮箱中的邮件
C: LIST
S: +OK 2 messages
S: 1 320
S: 2 280
S: .
# 客户端读取第 1 封邮件
C: RETR 1
S: +OK message follows
S: From: alice@example.com
S: To: bob@example.com
S: Subject: 你好,这是测试邮件
S:
S: 这是一封来自自建邮件服务器的测试邮件。
S: .
# 客户端读取不存在的邮件
C: RETR 99
S: -ERR no such message
# 客户端结束会话
C: QUIT
S: +OK Bye
POP3 服务器需要实现的核心命令包括:
USER <用户名>:客户端发送用户名进行认证。PASS <密码>:客户端发送密码完成认证。LIST:列出邮箱中的邮件编号和大小。RETR <编号>:读取指定编号的邮件内容。QUIT:结束会话。
7. 测试流程
完成以上设计后,按以下步骤进行测试:
- 启动服务器:
bash
python server.py
- 在另一个终端中,用 telnet 或 nc 连接服务器,模拟客户端发送邮件:
bash
telnet 127.0.0.1 2525
连接后,按照第 4 节展示的交互流程,依次输入 HELO、MAIL FROM、RCPT TO、DATA、邮件正文和 .,最后输入 QUIT 结束会话。
-
查看服务器端日志,确认邮件已保存到
mailboxes/bob@example.com/目录。 -
在另一个终端中,用 telnet 或 nc 连接 POP3 服务器,模拟客户端接收邮件:
bash
telnet 127.0.0.1 1110
连接后,按照第 5 节展示的交互流程,依次输入 USER、PASS、LIST、RETR 1,最后输入 QUIT 结束会话。
如果一切正常,你应该能在控制台看到刚才发送的邮件内容。
8. 总结与进一步扩展
通过本文的实践,你已经亲手实现了一个简化版的邮件服务器和客户端,理解了 SMTP 和 POP3 协议的核心交互流程。这个项目虽然简单,却涵盖了邮件系统最本质的机制。
如果你想继续深入,可以考虑以下扩展方向:
- 增加 IMAP 协议支持,实现邮件的在线管理和文件夹操作。
- 加入 TLS/SSL 加密,保障邮件传输安全。
- 实现邮件附件(MIME 多部分)的解析和发送。
- 增加用户认证和访问控制,防止未授权访问。
- 将邮件存储改为数据库(如 SQLite),便于检索和管理。
邮件系统是一个庞大而精妙的工程,希望本文能成为你探索网络协议和系统编程的起点。
9. 搭建实用的邮件服务器确实不简单
需要安装 sendmail、dovocot等软件,需要申请互联网IP,需要申请DNS域名,需要申请证书
DNS申请原理,参考 https://blog.csdn.net/ggaofengg/article/details/164187662
免费证书也是可以申请的,但前提是需要有dns域名。
自己签发证书,可以没有域名,但其它邮件服务器不认,你的邮件会被当成垃圾邮件。
搭建邮件服务器确实曾以"配置复杂、坑多难调"著称,尤其是传统的 Sendmail 或 Postfix 组合,需要手动处理 DNS 记录、SSL 证书、反垃圾邮件策略等,稍有不慎就会导致邮件被拒收或进入垃圾箱。
不过,随着技术演进,现在已有多种极简方案能大幅降低门槛,甚至实现"5分钟部署"。以下是针对不同场景的简化搭建思路:
🚀 方案一:极致轻量化个人邮箱(推荐个人/小团队)
如果你只是想要一个专属域名的邮箱,且服务器配置较低,PMail 是目前最简化的选择之一。
核心优势:
极简部署:基于 Go 语言开发,二进制文件仅 15MB,内存占用不到 10MB,1核512M内存的 VPS 即可流畅运行。
一键启动:支持 Docker 或二进制直接运行,无需复杂的依赖安装。
自动 SSL:内置 ACME 协议,自动申请和更新 Let's Encrypt 证书,无需手动配置 HTTPS/SMTPS。
Web 向导配置:提供图形化初始化界面,引导完成域名、数据库和管理员账户设置。
部署示例:
docker run -p 25:25 -p 80:80 -p 443:443 -p 587:587 -p 993:993 -p 995:995
-v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest
启动后访问 Web 界面即可完成剩余配置,支持 DKIM/SPF 自动检测,确保高送达率。
我试了一下
sudo k3s ctr run --rm --net-host
--env setup_port=8008
--env http_proxy=http://127.0.0.1:1
--env https_proxy=http://127.0.0.1:1
--env no_proxy=127.0.0.1,localhost
--mount type=bind,src=/opt/pmail-config,dst=/work/config,options=rbind:rw
ghcr.io/jinnrry/pmail:latest pmail-server
用这种方式启动的容器,不能用kubectl命令,也不能用crictl命令查询和删除,crictl命令和docker命令比较接近,但它只能看到kubectl启动的pod中的容器
只能用最原始的ctr命令删除 sudo k3s ctr tasks kill pmail-server ; sleep 3; sudo k3s ctr c rm pmail-server
要点:
--net-host:host 网络直通端口
--env setup_port=8008:bootstrap 界面用 8008 端口
http_proxy=127.0.0.1:1:让 ip-api.com 那个无超时请求立刻连接失败,跳过卡死
挂载 /work/config(配置和 sqlite 数据库 pmail.db 都在这里,持久化)
Web 管理界面:浏览器打开 http://10.137.239.107:8008/
注意:这台机器上 8008 是我们自己起的容器。如果是正式部署 ,建议用 Kubernetes Deployment + Service(NodePort) 的方式,避免用 --net-host 直接占用宿主机端口。
二、初始化(第一次运行)
我通过 API 完整走完了 setup 向导,等价于你在浏览器里点一遍:
步骤 配置
数据库 sqlite → ./config/pmail.db
管理员 admin / Admin12345
域名 test.local
SSL 自签名证书(/opt/pmail-config/ssl/)+ 生成的 DKIM 密钥
配置文件在 /opt/pmail-config/config.json,其中 httpPort:8008(避开 traefik)、httpsEnabled:2(只用 HTTP)。
三、测试结果(全部通过)
当前运行的服务端口:8008(Web)、25(SMTP)、465(SMTP+SSL)、587(SMTP+STARTTLS)、110(POP3)、995(POP3+SSL)、993(IMAP+SSL)
测试项 结果
Web 界面 ✅ 200,正常返回前端
API ping ✅ pong
登录(admin/test2) ✅ 返回账户+域名信息
SMTP 发信 → IMAP 收信 ✅ 外部域名发件投递到 INBOX,主题/收发件人正确
SMTP SSL 465 认证登录 ✅
SMTP STARTTLS 587 ✅
POP3 110 ✅ 能列出邮件
Web 收件箱 API ✅ 显示邮件列表
四、一个重要的使用说明(测试时踩到的)
PMail 有反伪造检测:如果发件域名是本地配置的域名(如 test.local)且 SPF 校验失败,邮件会被标记为"伪造"投到"已删除"文件夹。我最初用 admin@test.local → test2@test.local 测试,邮件就进了 Deleted 而不是收件箱------这是正常的邮件安全行为,不是 bug。
本地测试:用非本地域名发件(如 someone@example.com → test2@test.local),验证通过 ✅
真实使用:配置好域名的 SPF/DKIM DNS 记录后,本地用户互发不会被误判
五、邮件客户端配置(真实使用)
IMAP:imap.test.local,端口 993 (SSL)
SMTP:smtp.test.local,端口 465 (SSL) 或 587 (STARTTLS)
POP3:pop.test.local,端口 110/995
当前容器还在运行,你可以直接打开 http://10.137.239.107:8008/ 用 admin/Admin12345 登录体验。需要我帮你把它整理成一个正式的 K8s Deployment YAML,或者把 test.local 换成你真实的域名重新配置吗?
echo "=== create test2 ==="
curl --noproxy '*' -s -b /tmp/pmail.cookies -X POST http://127.0.0.1:8008/api/user/create -H 'Content-Type: application/json' -d '{"username":"test2","password":"Test2123","account":"test2"}'
echo; echo "=== user list ===";
curl --noproxy '*' -s -b /tmp/pmail.cookies -X POST http://127.0.0.1:8008/api/user/list -H 'Content-Type: application/json' -d '{"current_page":1,"page_size":10}'|jq
python3 << 'EOF'
import sqlite3
con = sqlite3.connect("/opt/pmail-config/pmail.db")
cur = con.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
print("tables", r\[0 for r in cur.fetchall()])
for t in "email","email_queue","mail_queue","send_queue","email_send","user_mail":
try:
cur.execute(f"SELECT count(*) FROM {t}")
print(f"{t} count:", cur.fetchone()0)
except Exception as e:
pass
con.close()
EOF
可以直接查询数据库,不需要密码。这就是sqllite的安全模型,依赖操作系统对这个db文件的权限
sqlite3 /opt/pmail-config/pmail.db "SELECT id,account,name,is_admin FROM user;"