ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

模拟邮件系统课程设计:用Socket实现SMTP与POP3协议完整闭环

模拟邮件系统课程设计:用Socket实现SMTP与POP3协议完整闭环 简介这是一份面向高校计算机网络课程设计场景的模拟邮件系统完整项目源码适合正在完成课程作业或希望理解邮件收发底层协议的学生与开发者。项目基于SMTP与POP3协议实现用户注册登录、资料编辑、发信、收件箱查看及来信回复等核心功能并支持与公网邮箱绑定后完成真实邮件投递。技术栈采用Maven构建的ServletJSPMyBatisJavaMail组合遵循MVC分层结构配置好数据库即可直接运行。压缩包共58个文件约119KB包含12个Java源文件、12个class编译文件、10个XML配置、6个JSP页面以及CSS、properties等资源覆盖后端逻辑、前端展示与工程配置各环节。目前已有3493人学习下载可作为课程设计参考模板帮助读者快速理解邮件协议交互流程、掌握JavaMail调用方式并复用其分层架构与数据库配置思路。1. 模拟邮件系统SMTPPOP3协议课程设计从协议握手到能收发的完整闭环很多人做计算机网络课程设计时第一反应是抓个现成邮件服务器装上截几张图交差。但真到答辩被问「SMTP 的 DATA 阶段怎么判断邮件结束」「POP3 的 USER/PASS 和 APOP 差在哪」就答不上来了。这个题目的价值恰恰在于它逼你把应用层两个最经典的文本协议从「听说过」变成「亲手实现过」。模拟邮件系统SMTPPOP3协议计算机网络课程设计核心不是搭一个能用的邮箱而是用 Socket 自己写一个 SMTP 服务端负责收信、一个 POP3 服务端负责发信给客户端中间用文件或数据库存邮件。做完之后你对「邮件是怎么从 A 到 B」的理解会从黑匣子变成透明的。适合正在做课设的本科生也适合想补应用层协议实操的考研党。下面按「协议先立住 → 动手能复现 → 坑在哪」的顺序讲透。2. SMTP 与 POP3 的协议边界先搞清楚谁负责什么2.1 为什么邮件系统要拆成两个协议刚接触时最容易混的一点SMTP 和 POP3 到底谁管发、谁管收。答案不是「SMTP 发、POP3 收」这么简单。准确说SMTP 负责「邮件从发送方传到接收方的邮件服务器」以及「从客户端提交到自己的发送服务器」POP3 负责「客户端从自己的接收服务器把邮件下载下来」。也就是说一封邮件从你点「发送」到对方点「收信」中间至少经过两段 SMTP客户端→发送服务器、发送服务器→接收服务器和一段 POP3接收服务器→客户端。这个拆分决定了你课设的架构需要两个服务端进程一个监听 25 端口SMTP收信一个监听 110 端口POP3供客户端拉信。两者共享同一个邮件存储目录。很多同学图省事只写一个 SMTP结果「收信」环节没法演示答辩时被追问就露馅。从协议特征看两者都是「请求-响应」式的文本协议一行一条命令服务端回三位数字状态码。区别在于 SMTP 是「推」push客户端主动把邮件推给服务器POP3 是「拉」pull客户端主动从服务器取。理解这个推拉方向后面写代码时状态机的设计思路就顺了。2.2 两个协议的命令集与状态码对照动手前先把命令集背熟这是写状态机的地基。SMTP 的核心命令不多但每个都有明确的参数格式POP3 更简单但状态切换授权态→事务态必须严格。协议命令参数作用典型响应SMTPHELO/EHLO域名打招呼EHLO 支持扩展250SMTPMAIL FROM发件人地址声明发件人250SMTPRCPT TO收件人地址声明收件人可多次250SMTPDATA无进入邮件内容传输354SMTP.单独一行结束邮件内容250SMTPQUIT无结束会话221POP3USER用户名提交用户名OKPOP3PASS密码提交密码OKPOP3STAT无查询邮件数量和总大小OK n sizePOP3LIST可选邮件号列出邮件OKPOP3RETR邮件号取回某封邮件OKPOP3DELE邮件号标记删除OKPOP3QUIT无提交并退出OK注意 SMTP 用三位数字码250、354、550POP3 用OK/-ERR字符串。这个差异在写响应函数时最容易翻车——我见过有人 POP3 也回 250客户端直接卡死。2.3 邮件存储格式为什么用 eml 文件而不是数据库课设里存储方案有两种文件系统或数据库。我一般推荐用.eml文件一个用户一个目录每封邮件一个文件。原因有三一是 eml 本身就是 RFC 822 定义的邮件格式SMTP 收到的 DATA 内容几乎可以直接落盘不用做字段映射二是 POP3 的 RETR 命令本质就是「把文件内容原样吐回去」文件方案零转换三是调试时直接打开文件就能看比查数据库直观得多。目录结构建议这样设计maildata/ ├── alice/ │ ├── 1.eml │ └── 2.eml └── bob/ └── 1.eml每封邮件文件名用递增序号POP3 的邮件号就是文件名去掉.eml。删除时不要真删改名为1.eml.deleted等 QUIT 时再统一清理——这是 POP3 的「标记删除、退出生效」语义很多同学直接os.remove结果客户端中途断开邮件就丢了血泪经验。提示邮件内容里的.单独一行是 SMTP 的结束标志所以正文里如果出现以.开头的行发送方要做「点填充」前面加一个点接收方要去填充。这个细节不做正文里带句号开头的行会被截断。3. 用 Python Socket 实现 SMTP 服务端收信全流程3.1 最小可跑的 SMTP 服务端骨架先搭一个能接收邮件并落盘的骨架用 Python 的socketthreading每个连接一个线程。核心是状态机从连接建立到 QUIT依次经过「打招呼 → MAIL → RCPT → DATA → 落盘」。import socket import threading import os import time MAIL_ROOT maildata os.makedirs(MAIL_ROOT, exist_okTrue) def handle_smtp(conn, addr): # 每个连接独立的状态 state {from: None, rcpt: [], data_mode: False} conn.sendall(b220 simulated-smtp ready\r\n) buffer b mail_lines [] while True: chunk conn.recv(4096) if not chunk: break buffer chunk # 按行处理注意 DATA 阶段要保留原始行 while b\r\n in buffer: line, buffer buffer.split(b\r\n, 1) text line.decode(utf-8, errorsreplace) if state[data_mode]: if text .: # 邮件结束落盘 save_mail(state, mail_lines) mail_lines [] state[data_mode] False state[from] None state[rcpt] [] conn.sendall(b250 OK queued\r\n) else: # 去点填充 if text.startswith(..): text text[1:] mail_lines.append(text) continue cmd text.upper() if cmd.startswith(HELO) or cmd.startswith(EHLO): conn.sendall(b250 simulated-smtp\r\n) elif cmd.startswith(MAIL FROM:): state[from] text[10:].strip().strip() conn.sendall(b250 OK\r\n) elif cmd.startswith(RCPT TO:): state[rcpt].append(text[8:].strip().strip()) conn.sendall(b250 OK\r\n) elif cmd DATA: if not state[from] or not state[rcpt]: conn.sendall(b503 bad sequence\r\n) else: state[data_mode] True conn.sendall(b354 End data with CRLF.CRLF\r\n) elif cmd QUIT: conn.sendall(b221 Bye\r\n) conn.close() return else: conn.sendall(b500 unknown command\r\n) def save_mail(state, lines): for rcpt in state[rcpt]: user_dir os.path.join(MAIL_ROOT, rcpt.split()[0]) os.makedirs(user_dir, exist_okTrue) # 找下一个序号 existing [f for f in os.listdir(user_dir) if f.endswith(.eml)] idx len(existing) 1 path os.path.join(user_dir, f{idx}.eml) with open(path, w, encodingutf-8) as f: f.write(fFrom: {state[from]}\r\n) f.write(fTo: {, .join(state[rcpt])}\r\n) f.write(fReceived: {time.ctime()}\r\n) f.write(\r\n) f.write(\r\n.join(lines)) def start_smtp(host0.0.0.0, port25): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((host, port)) s.listen(10) print(fSMTP listening on {port}) while True: conn, addr s.accept() threading.Thread(targethandle_smtp, args(conn, addr), daemonTrue).start() if __name__ __main__: start_smtp()逻辑说明handle_smtp里用data_mode标志区分「命令阶段」和「数据阶段」这是 SMTP 状态机的关键。命令阶段逐行解析命令数据阶段只判断是否为单独的.。save_mail按收件人分目录落盘文件名递增。参数说明port25是 SMTP 标准端口但 Linux 下 1024 以下端口需要 root 权限课设演示时建议改成2525避免权限问题。SO_REUSEADDR让服务重启时不被 TIME_WAIT 卡住调试时必加。recv(4096)的缓冲区大小对普通邮件够用如果测试大附件要调到 65536。3.2 用 swaks 或 telnet 验证 SMTP 收信服务端跑起来后别急着写客户端先用命令行工具验证。Linux 下telnet localhost 2525就能手动走一遍协议telnet localhost 2525 # 依次输入 HELO test.com MAIL FROM:alicetest.com RCPT TO:bobtest.com DATA Subject: hello This is a test mail. . QUIT每一步都要看到对应的状态码。如果 DATA 后没收到 354检查是不是漏了 MAIL 或 RCPT。如果.后没收到 250看save_mail有没有抛异常——常见的是目录权限问题。更省事的方式是用swaksSwiss Army Knife for SMTP一条命令发一封测试邮件swaks --to bobtest.com --from alicetest.com \ --server localhost:2525 --body test body --header Subject: swaks test--server指定地址端口--body是正文--header加自定义头。swaks 会自动走完 HELO/MAIL/RCPT/DATA 全流程比 telnet 手敲快得多。发完后去maildata/bob/下看有没有1.eml打开确认 From、To、Subject 都在。注意Windows 下 telnet 默认没装用pkgmgr /iu:TelnetClient开启或者直接用 Python 的smtplib写个三行脚本测试比折腾 telnet 快。4. 用 Python Socket 实现 POP3 服务端让客户端把信拉走4.1 POP3 状态机与授权态实现POP3 比 SMTP 简单但状态切换必须严格连接建立后处于「授权态」只有 USER/PASS 成功后才进入「事务态」事务态才能用 STAT/LIST/RETR/DELE。QUIT 在授权态直接断开在事务态要先执行删除再断开。import socket import threading import os MAIL_ROOT maildata USERS {alice: 123456, bob: 123456} # 课设用明文生产绝不可这样 def handle_pop3(conn, addr): authorized False user None deleted set() conn.sendall(bOK simulated-pop3 ready\r\n) buffer b while True: chunk conn.recv(4096) if not chunk: break buffer chunk while b\r\n in buffer: line, buffer buffer.split(b\r\n, 1) text line.decode(utf-8, errorsreplace).strip() parts text.split() cmd parts[0].upper() if parts else if not authorized: if cmd USER and len(parts) 2: user parts[1] conn.sendall(bOK user accepted\r\n) elif cmd PASS and len(parts) 2: if USERS.get(user) parts[1]: authorized True conn.sendall(bOK logged in\r\n) else: conn.sendall(b-ERR bad password\r\n) elif cmd QUIT: conn.sendall(bOK bye\r\n) conn.close() return else: conn.sendall(b-ERR not authorized\r\n) continue # 事务态 user_dir os.path.join(MAIL_ROOT, user) mails sorted([f for f in os.listdir(user_dir) if f.endswith(.eml)]) if os.path.isdir(user_dir) else [] if cmd STAT: total sum(os.path.getsize(os.path.join(user_dir, m)) for m in mails) conn.sendall(fOK {len(mails)} {total}\r\n.encode()) elif cmd LIST: resp fOK {len(mails)} messages\r\n for i, m in enumerate(mails, 1): size os.path.getsize(os.path.join(user_dir, m)) resp f{i} {size}\r\n resp .\r\n conn.sendall(resp.encode()) elif cmd RETR and len(parts) 2: idx int(parts[1]) if 1 idx len(mails): path os.path.join(user_dir, mails[idx-1]) with open(path, r, encodingutf-8) as f: content f.read() # 点填充 content content.replace(\r\n., \r\n..) conn.sendall(bOK message follows\r\n) conn.sendall(content.encode() b\r\n.\r\n) else: conn.sendall(b-ERR no such message\r\n) elif cmd DELE and len(parts) 2: idx int(parts[1]) if 1 idx len(mails): deleted.add(mails[idx-1]) conn.sendall(bOK marked\r\n) else: conn.sendall(b-ERR no such message\r\n) elif cmd QUIT: # 事务态 QUIT 才真正删除 for m in deleted: os.remove(os.path.join(user_dir, m)) conn.sendall(bOK bye\r\n) conn.close() return else: conn.sendall(b-ERR unknown command\r\n) def start_pop3(host0.0.0.0, port110): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((host, port)) s.listen(10) print(fPOP3 listening on {port}) while True: conn, addr s.accept() threading.Thread(targethandle_pop3, args(conn, addr), daemonTrue).start() if __name__ __main__: start_pop3()逻辑说明authorized标志控制状态切换未授权时只接受 USER/PASS/QUIT。事务态里mails每次命令都重新扫描目录保证看到最新邮件。deleted集合记录待删邮件只有事务态 QUIT 才真正os.remove这是 POP3 的「标记删除」语义。参数说明port110是 POP3 标准端口同样建议改1110避免权限问题。USERS字典课设用明文密码真实场景必须哈希存储。RETR里的点填充用replace处理注意顺序是先替换\r\n.为\r\n..避免误伤正文中间的点。4.2 用 Python poplib 验证收信服务端跑起来后用标准库poplib验证这是最接近真实客户端的测试方式import poplib client poplib.POP3(localhost, 1110) print(client.getwelcome()) # OK simulated-pop3 ready client.user(bob) client.pass_(123456) print(client.stat()) # (邮件数, 总大小) resp, lines, octets client.retr(1) print(b\r\n.join(lines).decode()) client.dele(1) client.quit()getwelcome拿欢迎语user/pass_走授权stat看邮件数retr(1)取第一封dele(1)标记删除quit提交。跑完去maildata/bob/看1.eml应该没了。如果retr报错检查邮件号是否从 1 开始——POP3 邮件号是 1-based不是 0-based这个坑我踩过。提示poplib的pass_带下划线是因为pass是 Python 关键字。如果客户端连不上先用telnet localhost 1110手敲 USER/PASS 确认服务端正常再排查客户端代码。5. 联调避坑SMTP 与 POP3 对接时的 5 个高频翻车点5.1 现象邮件发出去了POP3 却收不到原因SMTP 落盘目录和 POP3 读取目录不一致。常见于两个服务端用了不同的MAIL_ROOT或者 SMTP 按alicetest.com存成了alicetest.com目录POP3 却按alice找。解决统一用「 前面的部分」作为用户名目录两个服务端共用同一个MAIL_ROOT常量。联调前先ls maildata/确认目录名。5.2 现象RETR 取回的邮件正文被截断原因正文里有以.开头的行POP3 客户端把它当成了结束标志。SMTP 落盘时没做点填充或者 POP3 发送时没做点填充。解决SMTP 接收时去填充..变.POP3 发送时加填充.变..。两处都要做缺一不可。测试时故意在正文里加一行.hidden验证。5.3 现象客户端连上就断服务端报 BrokenPipe原因conn.sendall发送时客户端已经关闭连接或者响应里少了\r\n客户端解析卡住后超时断开。解决所有响应必须以\r\n结尾包括多行响应的最后一行。用try/except包住sendall捕获BrokenPipeError后直接return不要让线程崩掉。5.4 现象并发测试时邮件序号重复原因两个 SMTP 连接同时给同一用户发信都读到「现有 3 封」都写成4.eml后写的覆盖先写的。解决落盘时加文件锁或者用time.time()纳秒做文件名。课设并发量小简单做法是用threading.Lock()包住「扫描目录 写文件」这段临界区。5.5 现象POP3 的 DELE 后邮件还在原因DELE 只标记不删除必须 QUIT 才生效。很多同学测试时 DELE 完直接关连接没走 QUIT。解决这是协议设计不是 bug。测试脚本里 DELE 后必须调quit()。如果想让 DELE 立即生效那是 IMAP 的语义POP3 不支持。注意课设演示时如果客户端是 Foxmail、Outlook 这类真实邮件客户端它们默认走 SSL995/465 端口你的明文服务端连不上。要么在客户端里关掉 SSL 并改端口要么课设就用自己的 poplib 脚本演示别在客户端配置上耗时间。6. 进阶把模拟系统做成能演示「完整邮件旅程」的加分项基础版做完答辩能过但想拿高分得让演示有说服力。我的习惯是加一个「邮件旅程可视化」在 SMTP 和 POP3 的关键节点打日志用时间线串起来。比如 SMTP 收到 MAIL FROM 时记[SMTP] alice - bob, stageMAIL落盘时记[STORE] saved to bob/1.emlPOP3 RETR 时记[POP3] bob retrieved 1.eml。演示时开两个终端分别跑服务端第三个终端跑客户端脚本日志实时滚动评委一眼就能看到邮件从推送到拉取的完整路径。再进一步可以加一个极简的 Web 界面Flask 十几行列出maildata/下的邮件点开看内容。这样演示不用切终端浏览器里就能展示「发信 → 收信 → 读信」。注意 Web 界面只读不要在里面做删除避免和 POP3 的删除语义冲突。验证方法上除了手动测试建议写一个自动化脚本用smtplib发 3 封邮件用poplib收 3 封断言数量一致、内容匹配。这个脚本放进课设报告里比截图有说服力。参数上测试时把端口改成 2525/1110避免和系统服务冲突邮件大小控制在 1MB 以内避免recv缓冲区不够。最后一个技巧SMTP 的 EHLO 响应里可以声明SIZE扩展告诉客户端最大邮件尺寸这样真实客户端会提前拒绝超大邮件而不是传到一半断掉。实现就是在 EHLO 后多回一行250-SIZE 10485760。这个细节很多课设不做加上去答辩时能讲出「我考虑了扩展机制」是实打实的加分点。我自己做这类课设最大的教训是别一上来就追求功能全先把「一封纯文本邮件从 SMTP 进、从 POP3 出」这条最小闭环跑通再往上加附件、加多用户、加 Web 界面。我见过太多同学卡在「支持 MIME 附件」上结果基础协议都没调通得不偿失。协议实现这件事跑通比功能多重要得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表