简介:这份资源是面向计算机、网络工程等专业学生的计算机网络课程设计参考方案,围绕SMTP与POP3协议模拟邮件系统的收发流程,适合正在做课设或想理解邮件协议实战应用的学习者。压缩包共58个文件,约119KB,以12个Java源码与12个class文件为核心,配合10个XML配置、6个JSP页面、3个CSS样式及properties等配置项,完整呈现一个可运行的Maven工程结构。系统采用Servlet+JSP+MyBatis+JavaMail的MVC架构,实现用户注册登录、资料编辑、发信、查看来信、回复及收件箱管理,并与公网邮箱绑定后借助SMTP完成投递。目前已有3493人学习下载,读者可据此快速搭建数据库并运行项目,理解邮件协议在J2EE环境下的落地方式,同时借鉴其分层设计与配置组织思路,为课程设计答辩或后续网络编程实践提供可复用的参考。
1. 从零手搓模拟邮件系统:SMTP 与 POP3 的底层逻辑到底怎么跑通
很多人做计算机网络课程设计时,第一反应是去网上找一份现成的 Java 或 Python 邮件系统源码,改个界面就交差。但答辩时老师只要问一句「你的邮件正文是怎么从客户端走到服务端的」,大多数人就卡住了。模拟邮件系统(SMTP+POP3协议)这个题目的核心价值,恰恰在于它逼着你把「邮件发送」和「邮件接收」这两条链路彻底拆开看一遍。SMTP 负责把邮件从客户端推给服务端,或者从一台服务端推到另一台服务端;POP3 负责把邮件从服务端拉到客户端。两者一个管「投递」,一个管「取件」,方向完全相反。这个课程设计适合已经学过 TCP/IP 基本概念、想通过动手真正理解应用层协议交互过程的人。做完之后,你对「命令-响应」模式、端口号、状态码、Base64 编码这些知识点会有完全不同的体感。
2. 动手之前先把协议交互流程理清楚:SMTP 和 POP3 到底在传什么
2.1 SMTP 的推模式:从 HELO 到 DATA 的完整命令链
SMTP 的本质是一个基于 TCP 的文本协议,默认端口 25,加密版本 465/587。它的工作模式是「推」——客户端主动把邮件推给服务端。一次典型的 SMTP 会话大致经过这几个阶段:建立 TCP 连接后,服务端先回220表示就绪;客户端发HELO或EHLO表明身份;服务端回250;然后客户端用MAIL FROM声明发件人,用RCPT TO声明收件人,每步服务端都会回250表示接受;接着客户端发DATA,服务端回354表示可以开始输入邮件内容;客户端输入完正文后,单独发一行.表示结束;服务端回250表示邮件已接收;最后客户端发QUIT,服务端回221并关闭连接。
这套流程里最容易翻车的地方是DATA阶段。邮件头(From、To、Subject、Date)和邮件正文之间必须空一行,正文结束后的.必须单独占一行,而且如果正文里本身有一行以.开头,需要转义成..。这些细节在 RFC 5321 里有明确定义,但自己写代码时极容易忽略。我一般会建议在课程设计里把 SMTP 会话做成一个状态机,每个状态只接受特定的命令,这样调试时能快速定位是哪一步的响应码不对。
2.2 POP3 的拉模式:USER、PASS、LIST、RETR 四步取件
POP3 默认端口 110,加密版本 995。它的交互比 SMTP 更简单,因为它的目标只有一个:把邮件从服务端取走。典型会话流程是:TCP 连接建立后,服务端回+OK;客户端发USER 用户名,服务端回+OK;客户端发PASS 密码,服务端回+OK表示登录成功;然后客户端可以用STAT查邮件数量和总大小,用LIST列出每封邮件的编号和大小,用RETR 编号取回某封邮件的完整内容,用DELE 编号标记删除,最后用QUIT结束会话并真正执行删除。
POP3 的响应只有两种前缀:+OK表示成功,-ERR表示失败。这种极简设计意味着错误信息全靠后面的文本描述,所以课程设计里如果要做健壮性,必须对-ERR做分类处理。另一个关键点是:POP3 默认是「下载并删除」模式,但很多客户端会改成「下载并保留」,这个行为差异在课程设计里可以通过DELE命令的调用时机来控制。
2.3 为什么课程设计里建议用 Python 的 socket 而不是现成库
很多同学会想直接用 Python 的smtplib和poplib,两行代码就发完邮件了。但课程设计的目的是理解协议,不是调库。用socket裸写的好处是:你能亲眼看到每一个命令字符串是怎么拼出来的,每一个响应码是怎么解析的。我一般会建议先用socket实现一个最小可用的 SMTP 客户端,能往本地搭建的 SMTP 服务端发一封纯文本邮件;然后再用socket实现一个 POP3 客户端,能从本地 POP3 服务端把邮件取回来。这两步跑通之后,再考虑用smtplib做对比验证,这样你就能清楚地区分「协议本身」和「库的封装」之间的边界。
3. 用 Python socket 手写 SMTP 客户端:从连接建立到邮件投递
3.1 最小 SMTP 客户端的完整代码与逐行说明
下面这段代码实现了一个最简 SMTP 客户端,能连接本地 25 端口的 SMTP 服务端并发送一封纯文本邮件。代码里没有用任何第三方库,全部基于socket和base64。
import socket import base64 from datetime import datetime def smtp_send(server, port, mail_from, rcpt_to, subject, body): # 建立 TCP 连接 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((server, port)) # 读取服务端欢迎信息,应为 220 resp = client.recv(1024).decode() print("S:", resp.strip()) if not resp.startswith("220"): raise Exception("服务端未就绪") # 发送 HELO 表明身份 client.sendall(b"HELO testclient\r\n") resp = client.recv(1024).decode() print("S:", resp.strip()) # 声明发件人 client.sendall(f"MAIL FROM:<{mail_from}>\r\n".encode()) resp = client.recv(1024).decode() print("S:", resp.strip()) # 声明收件人 client.sendall(f"RCPT TO:<{rcpt_to}>\r\n".encode()) resp = client.recv(1024).decode() print("S:", resp.strip()) # 请求进入 DATA 阶段 client.sendall(b"DATA\r\n") resp = client.recv(1024).decode() print("S:", resp.strip()) if not resp.startswith("354"): raise Exception("服务端未接受 DATA 命令") # 构造邮件头与正文,注意头与正文之间空一行 date_str = datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0800") mail_content = ( f"From: {mail_from}\r\n" f"To: {rcpt_to}\r\n" f"Subject: {subject}\r\n" f"Date: {date_str}\r\n" f"MIME-Version: 1.0\r\n" f"Content-Type: text/plain; charset=utf-8\r\n" f"\r\n" f"{body}\r\n" f".\r\n" ) client.sendall(mail_content.encode()) resp = client.recv(1024).decode() print("S:", resp.strip()) # 结束会话 client.sendall(b"QUIT\r\n") resp = client.recv(1024).decode() print("S:", resp.strip()) client.close() if __name__ == "__main__": smtp_send( server="127.0.0.1", port=25, mail_from="sender@test.local", rcpt_to="receiver@test.local", subject="课程设计测试邮件", body="这是一封通过手写 SMTP 客户端发送的测试邮件。" )这段代码的关键点有三个。第一,每次sendall之后必须recv读取服务端响应,否则服务端的响应会堆积在缓冲区里,后续命令的响应码就对不上了。第二,邮件头里的Date格式必须符合 RFC 5322,否则某些严格的服务端会拒收。第三,正文结束后的.必须单独一行,且前面不能有空格。如果正文里本身有一行以.开头,需要替换成..,这个转义逻辑在课程设计里可以单独写一个函数处理。
3.2 参数怎么改:端口、编码、超时与调试开关
端口号是最容易改错的参数。本地测试如果用的是 Postfix 或 Sendmail,默认监听 25;如果用 Python 自带的aiosmtpd或smtpd模块起测试服务端,可以指定任意端口,比如 8025。改端口只需要改port参数,但要注意如果端口小于 1024,Linux 下需要 root 权限。
编码方面,邮件头里的非 ASCII 字符(比如中文主题)需要做 Base64 编码,格式是=?utf-8?b?<base64字符串>?=。正文如果包含中文,Content-Type里必须声明charset=utf-8,并且发送时用.encode('utf-8')。我一般会在代码里加一个debug开关,打开时打印每一条发送和接收的原始字符串,这样出问题时能直接看到是哪个命令的响应码不对。
超时设置也值得加。socket.settimeout(10)可以避免服务端不响应时客户端一直卡死。在课程设计答辩时,这个细节能体现你对网络编程的边界意识。
3.3 怎么验证发出去的邮件真的到了服务端
最直接的验证方式是去服务端的邮件存储目录里找。如果用的是 Postfix,邮件通常落在/var/mail/<用户名>或者/var/spool/mail/<用户名>。如果用的是自己写的模拟服务端,那就在服务端代码里把收到的邮件内容打印出来或者写进文件。
另一种验证方式是用telnet手动走一遍 SMTP 流程。在命令行里执行telnet 127.0.0.1 25,然后逐条输入HELO、MAIL FROM、RCPT TO、DATA,最后输入.结束。这个过程能帮你确认服务端本身是否正常工作,从而把「客户端问题」和「服务端问题」隔离开。我一般会先用telnet确认服务端没问题,再跑自己写的客户端代码,这样排查范围能缩小一半。
4. 用 Python socket 手写 POP3 客户端:把邮件从服务端取回来
4.1 POP3 会话的代码实现与状态码解析
POP3 客户端的代码结构和 SMTP 类似,但命令更少,响应格式更统一。下面这段代码实现了登录、列出邮件、取回第一封邮件、退出四个步骤。
import socket def pop3_fetch(server, port, user, password): client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((server, port)) # 读取欢迎信息,应为 +OK resp = client.recv(1024).decode() print("S:", resp.strip()) if not resp.startswith("+OK"): raise Exception("POP3 服务端未就绪") # 发送用户名 client.sendall(f"USER {user}\r\n".encode()) resp = client.recv(1024).decode() print("S:", resp.strip()) # 发送密码 client.sendall(f"PASS {password}\r\n".encode()) resp = client.recv(1024).decode() print("S:", resp.strip()) if not resp.startswith("+OK"): raise Exception("登录失败") # 列出邮件 client.sendall(b"LIST\r\n") resp = client.recv(4096).decode() print("S:", resp.strip()) # 取回第一封邮件 client.sendall(b"RETR 1\r\n") # 邮件内容可能较大,需要循环接收直到遇到单独的 "." mail_data = b"" while True: chunk = client.recv(4096) mail_data += chunk if b"\r\n.\r\n" in mail_data: break print("邮件内容:") print(mail_data.decode(errors="replace")) # 退出 client.sendall(b"QUIT\r\n") resp = client.recv(1024).decode() print("S:", resp.strip()) client.close() if __name__ == "__main__": pop3_fetch( server="127.0.0.1", port=110, user="receiver", password="test123" )这段代码里最需要注意的是RETR命令的响应接收。POP3 的邮件内容可能超过一次recv的缓冲区大小,所以必须循环接收,直到检测到\r\n.\r\n这个结束标记。这个标记和 SMTP 的 DATA 结束标记是同一个逻辑,但 POP3 里它是作为多行响应的终止符出现的。
另一个细节是LIST命令的响应。它返回的是多行数据,第一行是+OK,后面每行是「邮件编号 大小」,最后以单独的.结束。如果只recv一次,可能只拿到部分数据,所以实际课程设计里应该写一个recv_until_dot函数来统一处理多行响应。
4.2 邮件正文的 MIME 解析:为什么取回来是一堆乱码
用 POP3 取回来的邮件内容,如果直接打印,经常会看到=?utf-8?b?5rWL6K+V?=这样的字符串,或者正文是一堆 Base64 编码的乱码。这是因为邮件在传输过程中会对非 ASCII 内容做 MIME 编码。要还原成可读文本,需要用 Python 的email模块解析。
import email from email.header import decode_header def parse_mail(raw_bytes): msg = email.message_from_bytes(raw_bytes) # 解析主题 subject_parts = decode_header(msg["Subject"]) subject = "" for part, charset in subject_parts: if isinstance(part, bytes): subject += part.decode(charset or "utf-8") else: subject += part print("主题:", subject) # 解析正文 if msg.is_multipart(): for part in msg.walk(): if part.get_content_type() == "text/plain": body = part.get_payload(decode=True).decode( part.get_content_charset() or "utf-8" ) print("正文:", body) else: body = msg.get_payload(decode=True).decode( msg.get_content_charset() or "utf-8" ) print("正文:", body)这个解析函数能处理大多数纯文本邮件。如果课程设计里要支持附件,还需要处理multipart/mixed类型,把附件部分用get_payload(decode=True)取出来写进文件。我一般会建议课程设计至少支持纯文本和带一个附件两种情况,这样能覆盖 MIME 的主要分支。
4.3 本地搭建测试服务端:不依赖外部邮箱的闭环方案
课程设计最怕的就是依赖外部邮箱服务,因为网络环境、认证策略、频率限制都可能让实验失败。最稳妥的做法是在本地搭一个测试服务端。Python 标准库里的aiosmtpd可以起一个 SMTP 服务端,poplib没有对应的服务端,但可以用asyncio自己写一个极简 POP3 服务端,或者用现成的pypop3之类的轻量实现。
如果不想引入额外依赖,也可以直接用 Python 的smtpd模块(注意 Python 3.12 之后已移除,需要用aiosmtpd替代)起一个调试用 SMTP 服务端,它会把收到的邮件打印到控制台。POP3 服务端可以自己用socket写一个最简单的:监听 110 端口,收到USER和PASS就回+OK,收到LIST就返回一封固定邮件的信息,收到RETR就把预存的邮件内容发回去。这个服务端不需要真正持久化,只要能配合客户端跑通协议流程就行。
5. 避坑与排查:课程设计里最容易翻车的 5 个地方
5.1 现象:客户端发完 DATA 后服务端不回 250,直接卡死
原因通常是邮件内容没有以\r\n.\r\n结尾。SMTP 协议规定,DATA 阶段的结束标志是单独一行的.,而且这一行必须用 CRLF 包裹。如果只发了\n.\n,某些服务端会认为邮件还没结束,继续等待。解决方法是确保邮件内容的最后是\r\n.\r\n,并且正文里如果有以.开头的行,要替换成..。
5.2 现象:POP3 登录一直返回 -ERR,但用户名密码明明是对的
最常见的原因是用户名或密码里包含了特殊字符,或者服务端要求先发USER再发PASS,但代码里顺序反了。另一个可能是服务端开启了 APOP 认证,要求用APOP命令而不是USER/PASS。排查时先用telnet手动走一遍,确认服务端到底接受哪种认证方式。如果telnet能登录而代码不能,那就是代码里的字符串拼接有问题,检查有没有多余的空格或缺少\r\n。
5.3 现象:中文主题在邮件客户端里显示为乱码
这是因为邮件头没有做 RFC 2047 编码。SMTP 协议本身只支持 ASCII,中文必须编码成=?utf-8?b?<base64>?=的形式。解决方法是写一个encode_header函数,对Subject、From显示名等字段做 Base64 编码。注意 Base64 编码后的字符串不能有换行,否则会被邮件客户端截断。
5.4 现象:RETR 取回的邮件内容不完整,只有前几行
原因是recv的缓冲区大小设小了,或者没有循环接收直到遇到结束标记。POP3 的RETR响应是多行模式,必须以单独的.行结束。解决方法是写一个recv_multiline函数,循环recv并拼接,每次检查缓冲区末尾是否出现了\r\n.\r\n。注意这个标记可能跨越两次recv,所以检查时要在拼接后的完整缓冲区里找。
5.5 现象:本地测试正常,换到另一台机器就连不上
先检查防火墙是否放行了 25 和 110 端口。Linux 下可以用ss -tlnp看服务端有没有真正监听。如果服务端监听的是127.0.0.1,那只能本机连接;要允许其他机器连接,需要监听0.0.0.0。另一个常见原因是云服务器厂商默认封禁 25 端口,这种情况下只能换端口或者用内部网络测试。课程设计里如果要在局域网内演示,建议把服务端端口改成 2525 和 1100 这类高位端口,避开限制。
6. 进阶技巧:用 Wireshark 抓包验证协议交互与状态机调试
课程设计做到能发能收之后,如果想拿高分,下一步就是证明「你真的理解协议」。最直观的方式是用 Wireshark 抓包,把 SMTP 和 POP3 的交互过程完整记录下来。具体做法是:在本地回环接口(loopback)上抓包,过滤条件写tcp.port == 25 || tcp.port == 110,然后跑一遍自己的客户端代码。Wireshark 会把每一条命令和响应都解析出来,你能清楚地看到MAIL FROM后面跟的地址、RCPT TO的响应码、DATA阶段的内容长度。
我一般会让学生把抓包结果和代码里的状态机对照着看。比如代码里发完DATA后等待354,抓包里就能看到服务端确实回了354,然后客户端才开始发邮件内容。如果哪一步的响应码和预期不符,抓包里一目了然。这个习惯在以后调试任何基于文本的协议时都用得上,算是课程设计里最值得带走的一项技能。
另一个进阶方向是把 SMTP 和 POP3 的状态机画成表格,每个状态下列出允许的命令和预期的响应码。下面是一个简化的 SMTP 状态表:
| 当前状态 | 允许命令 | 预期响应 | 下一状态 |
|---|---|---|---|
| 连接建立 | 等待服务端 | 220 | 就绪 |
| 就绪 | HELO/EHLO | 250 | 已标识 |
| 已标识 | MAIL FROM | 250 | 已声明发件人 |
| 已声明发件人 | RCPT TO | 250 | 已声明收件人 |
| 已声明收件人 | DATA | 354 | 数据传输中 |
| 数据传输中 | 邮件内容 +. | 250 | 已标识 |
| 任意 | QUIT | 221 | 关闭 |
这张表可以直接写进课程设计报告里,比大段文字描述更有说服力。调试时如果某个命令返回了非预期响应码,就对照这张表看当前状态是否允许该命令。比如在「就绪」状态下直接发MAIL FROM,服务端通常会回503 Bad sequence of commands,这就是状态机没对齐的典型表现。
最后说一个我自己的习惯:每次写完一个协议客户端,我都会先用telnet手动走一遍完整流程,把每一条命令和响应记在纸上,然后再写代码。这样代码里的逻辑顺序就不会错,出问题时也能快速判断是协议理解错了还是代码写错了。希望帮到你。
本文还有配套的精品资源,点击获取