VC++写的简易邮箱客户端源码,带SMTP发信和POP3收信功能
简介:一个轻量级Windows邮件客户端源码项目,用VC++和MFC开发,能直接连接邮箱服务器收发纯文本邮件。代码里已经预设了SMTP和POP3服务器地址,填上账号密码就能运行,支持登录验证(AUTH LOGIN)、获取邮件列表、下载邮件内容(RETR)、删除邮件(DELE)等基础操作。底层用原始Socket通信,包含Base64编解码、协议命令拼接、响应解析等细节实现,所有逻辑模块清晰分离,比如网络层、协议层、界面层各自独立。工程文件兼容VC++ 6.0和早期Visual Studio,打开即可编译,不需要额外依赖库。适合想动手理解邮件协议怎么跑起来的开发者,比如学习POP3怎么取邮件、SMTP怎么发信、邮箱认证流程怎么走。后续加附件上传、本地邮件存档、多账户管理等功能也很方便,在现有结构上扩展就行。
1. 项目概述:为什么一个“简陋”的VC++邮件客户端,反而成了我带新人入门网络编程的首选教具?
在带团队新人做Windows桌面开发培训时,我常被问:“老师,能不能给个‘真实点’的项目练手?别总写计算器、记事本了。”——这话听着像吐槽,其实戳中了关键:新手最缺的不是语法,而是对“程序如何真正和外部世界对话”的体感。而这个用VC++ 6.0写的、连图标都像素风的MyEmailClient,恰恰就是我压箱底的“体感发生器”。它不炫技,没UI框架,甚至不支持附件,但当你亲手敲下socket(AF_INET, SOCK_STREAM, 0),看着程序在控制台里一行行打印出+OK POP3 server ready、350 Send mail data ending with <CRLF>.<CRLF>,再把Base64编码后的密码发出去、收到服务器返回的235 Authentication successful——那一刻,抽象的RFC文档突然有了温度。它把SMTP/POP3协议从教科书里拽出来,摊在你面前:原来发一封邮件,不是调个SendMail()函数,而是要手动拼接AUTH LOGIN命令、处理base64编码的凭据、等待服务器逐字节响应;收一封邮件,也不是点一下“刷新”,而是得先USER xxx@xxx.com、再PASS base64_encoded_pwd、然后LIST查编号、RETR 1取内容、最后DELE 1删标记。整个流程像拆解一台老式机械钟表,每个齿轮(Socket连接、命令发送、响应解析、状态机跳转)都裸露在外,一目了然。关键词里的VC++邮件客户端,核心价值不在“客户端”本身,而在它作为协议教学沙盒的纯粹性;SMTP发送源码和POP3接收实现,也不是功能罗列,而是两套完整、可打断、可调试的协议交互脚本。它不追求工程化,却比任何现代框架更能回答“网络通信到底发生了什么”这个根本问题。如果你正卡在“学完Socket API还是不会写网络程序”的瓶颈上,或者想让实习生三天内搞懂邮箱认证为什么需要base64、为什么POP3要分LIST/RETR/DELE三步走——这个项目就是你的扳手和放大镜。
2. 整体架构与设计思路:为什么坚持用原始Socket+MFC,而不是ATL或第三方库?
拿到这个项目源码,第一反应往往是:“都2024年了,还用VC++ 6.0?不直接上Qt或Electron?”——这恰恰是理解其设计哲学的入口。它的架构选择,每一步都是为“教学穿透力”服务的刻意为之,而非技术落后。
2.1 分层清晰:网络层、协议层、界面层的物理隔离
整个工程目录结构像一本摊开的实验手册:
- Network/ 目录下只有两个文件:CSocketClient.cpp 和 CSocketClient.h。前者只干一件事:封装socket()、connect()、send()、recv()的底层调用,提供ConnectToServer()、SendData()、RecvResponse()三个接口。它不碰任何协议字符串,连\r\n都不硬编码,只负责字节流的可靠传输。
- Protocol/ 目录是真正的“协议引擎”:CSmtpProtocol.cpp 实现SMTP全流程(HELO→AUTH LOGIN→MAIL FROM:→RCPT TO:→DATA→QUIT),CPop3Protocol.cpp 实现POP3全流程(USER→PASS→LIST→RETR→DELE→QUIT)。它们调用CSocketClient的接口,但自身只处理协议逻辑:比如CSmtpProtocol::SendAuthLogin()会先调用Base64Encode()对用户名密码编码,再拼接"AUTH LOGIN\r\n"和编码后的字符串,最后交给网络层发送。
- UI/ 目录(即MFC对话框类)则彻底“无脑”:主对话框CMyEmailClientDlg只负责把用户输入的邮箱、密码、收件人、正文,原样传给CSmtpProtocol::SendMail();收到CPop3Protocol::GetMailList()返回的邮件列表后,直接塞进CListCtrl控件显示。它不解析邮件头,不渲染HTML,甚至连换行符都原样显示。
这种物理隔离带来的好处是:你可以单独编译测试CSocketClient——写个控制台小程序,让它连telnet smtp.gmail.com 587,看是否能稳定收发数据;也可以绕过UI,直接在main()里调用CPop3Protocol::Login("user", "pass"),观察协议交互日志。每一层都像乐高积木,可以独立验证、替换、甚至用Python重写某一层来对比行为。这比把所有逻辑揉进一个QNetworkAccessManager回调里,教学价值高出数倍。
2.2 坚持原始Socket:拒绝黑盒,拥抱“脏活”
项目里没有用CAsyncSocket或CSocket这类MFC封装类,而是直调Winsock API。为什么?因为封装会隐藏关键细节。举个典型例子:SMTP的DATA命令后,服务器要求你发送邮件内容,以单独一行的.(英文句号)结束。很多初学者会写:
send(sock, "Subject: Test\r\n\r\nHello World\r\n.\r\n", ...); // 错!
但实际必须分两步:
send(sock, "Subject: Test\r\n\r\nHello World\r\n", ...); // 先发正文
send(sock, ".\r\n", ...); // 再发结束标记
因为服务器解析.时,是按行读取的,如果混在正文里,会被当成普通文本。用CAsyncSocket的Send()可能因缓冲区机制掩盖这个问题,而原始send()让你立刻看到WSAEWOULDBLOCK错误,逼你去查select()或WSAEventSelect()——这正是网络编程的核心痛点。同样,POP3的RETR响应里,邮件内容以\r\n.\r\n结尾,你需要循环recv()直到匹配到这个序列,而不是期待一次recv()拿回全部。这些“脏活”在原始Socket里无法回避,恰恰是理解TCP流式传输本质的必经之路。
2.3 MFC对话框的“克制”使用:界面只为展示协议状态
MFC在这里不是用来炫酷UI的,而是作为协议状态显示器。主对话框上的几个按钮:
- “连接POP3”:点击后调用CPop3Protocol::Connect(),并在编辑框实时打印"Connecting to pop3.xxx.com..." → "+OK POP3 server ready";
- “登录”:触发CPop3Protocol::Login(),日志显示"Sending USER command..." → "USER ok" → "Sending PASS command..." → "235 Authentication successful";
- “获取列表”:执行CPop3Protocol::ListMail(),解析服务器返回的"1 1234\r\n2 5678\r\n.",在列表控件里显示邮件编号和大小。
关键在于,所有日志输出都来自协议层的printf()或AfxMessageBox(),UI层只是被动接收并展示。这迫使你关注协议交互的时序和状态码含义,而不是纠结于按钮样式。当学生看到"535 Authentication failed"时,他第一反应是检查base64编码是否正确,而不是去调UI主题——这才是我们想要的思维路径。
3. 核心协议实现详解:从AUTH LOGIN到RETR,一行行代码拆解协议握手
现在进入最硬核的部分:把SMTP和POP3的“握手”过程,从RFC文档翻译成可执行的C++代码。这里不讲理论,只聚焦源码里最关键的几段,解释为什么这么写、不这么写会怎样。
3.1 SMTP认证:为什么AUTH LOGIN需要两次Base64编码?
SMTP服务器(如Gmail的smtp.gmail.com)要求AUTH LOGIN时,不是直接发明文密码,而是分三步走:
1. 发送AUTH LOGIN\r\n;
2. 服务器返回334 VXNlcm5hbWU6(base64编码的”Username:”);
3. 客户端发送base64编码的用户名;
4. 服务器返回334 UGFzc3dvcmQ6(base64编码的”Password:”);
5. 客户端发送base64编码的密码;
6. 服务器返回235 Authentication successful。
源码中CSmtpProtocol::SendAuthLogin()的实现如下:
bool CSmtpProtocol::SendAuthLogin(const CString& strUser, const CString& strPass) {
// 步骤1:发送 AUTH LOGIN
if (!m_pSocket->SendData("AUTH LOGIN\r\n")) return false;
// 步骤2:等待服务器提示 "Username:"
CString strResponse;
if (!m_pSocket->RecvResponse(strResponse)) return false;
if (strResponse.Left(3) != "334") return false; // 检查状态码
// 步骤3:发送base64编码的用户名
CString strEncodedUser = Base64Encode(strUser);
if (!m_pSocket->SendData(strEncodedUser + "\r\n")) return false;
// 步骤4:等待服务器提示 "Password:"
if (!m_pSocket->RecvResponse(strResponse)) return false;
if (strResponse.Left(3) != "334") return false;
// 步骤5:发送base64编码的密码
CString strEncodedPass = Base64Encode(strPass);
if (!m_pSocket->SendData(strEncodedPass + "\r\n")) return false;
// 步骤6:等待认证成功
if (!m_pSocket->RecvResponse(strResponse)) return false;
if (strResponse.Left(3) != "235") return false;
return true;
}
提示:
Base64Encode()函数在Utils/目录下,实现标准base64编码表(A-Z,a-z,0-9,+,\),注意末尾补=。这里的关键是:服务器返回的334后面跟着的是base64编码的提示字符串,而非明文!所以你不能写if (strResponse.Find("Username:") != -1),而必须先base64解码strResponse.Mid(4)再比较——但源码没这么做,因为它只校验状态码334,把提示字符串当“黑盒”处理。这是合理的简化:只要状态码对,下一步就发编码后的凭据,服务器自然能识别。
3.2 POP3邮件获取:LIST/RETR/DELE的原子性与状态管理
POP3协议的核心是“状态机”:你必须严格按顺序执行命令,且某些命令(如RETR)依赖前序命令(LIST)的结果。源码中CPop3Protocol::GetMailList()和CPop3Protocol::RetrieveMail(int nMailIndex)的协作逻辑如下:
第一步:获取邮件列表(LIST)
bool CPop3Protocol::GetMailList(CArray<MAIL_INFO>& arrMailInfo) {
// 发送 LIST 命令
if (!m_pSocket->SendData("LIST\r\n")) return false;
// 接收响应:首行是 "+OK",中间是 "1 1234\r\n2 5678\r\n",末行是 "."
CString strResponse;
while (m_pSocket->RecvResponse(strResponse)) {
if (strResponse == ".") break; // 结束标记
if (strResponse.Left(3) == "+OK") continue; // 跳过首行
// 解析单行:格式为 "邮件编号 邮件大小"
int nSpacePos = strResponse.Find(' ');
if (nSpacePos == -1) continue;
MAIL_INFO info;
info.nIndex = _ttoi(strResponse.Left(nSpacePos));
info.nSize = _ttoi(strResponse.Mid(nSpacePos + 1));
arrMailInfo.Add(info);
}
return true;
}
这里的关键细节:LIST响应是多行的,必须循环RecvResponse()直到遇到单独的.。如果一次性recv()拿回所有数据,你需要自己按\r\n分割——但源码采用更稳健的“逐行读取”策略,避免缓冲区溢出风险。
第二步:下载指定邮件(RETR)
bool CPop3Protocol::RetrieveMail(int nMailIndex, CString& strMailContent) {
// 发送 RETR 命令
CString strCmd;
strCmd.Format("RETR %d\r\n", nMailIndex);
if (!m_pSocket->SendData(strCmd)) return false;
// 接收响应:首行 "+OK",中间是邮件内容(含Header和Body),末行 "."
CString strLine;
bool bInContent = false;
while (m_pSocket->RecvResponse(strLine)) {
if (strLine == ".") break; // 结束标记
if (strLine.Left(3) == "+OK") {
bInContent = true;
continue;
}
if (bInContent) {
// 关键:POP3规定,邮件内容中的 "." 行需转义为 ".."
// 所以服务器发来 "..\r\n" 时,客户端应还原为 ".\r\n"
if (strLine == "..") {
strMailContent += ".\r\n";
} else {
strMailContent += strLine + "\r\n";
}
}
}
return true;
}
注意:POP3协议规定,邮件正文里如果出现单独一行的
.,服务器会自动在其前加一个.(即..),客户端收到后必须将..还原为.。这是为了区分“邮件内容结束标记”和“邮件正文里的句号”。源码中if (strLine == "..")的判断,正是对这一规则的忠实实现。漏掉这个,你的邮件正文里所有段落结尾都会多一个点。
第三步:删除邮件(DELE)与状态同步
POP3的DELE命令只是“标记删除”,真正删除要等QUIT命令后服务器才执行。源码中CPop3Protocol::DeleteMail(int nMailIndex)只发DELE命令,不检查响应(因为DELE成功与否不影响后续操作)。但UI层必须记住哪些邮件被标记了,所以在CMyEmailClientDlg::OnBnClickedBtnDelete()里,会把选中的邮件索引存入一个CArray<int>,并在OnBnClickedBtnQuit()时,遍历该数组发送所有DELE命令。这种“客户端状态缓存”是实用技巧:避免每次点击删除都立刻发命令,减少网络往返。
4. 实操部署与编译指南:从零开始跑通收发流程(含Gmail配置避坑)
光看代码不够,必须亲手跑起来。以下是我在Windows 10/11上用VC++ 6.0(或VS2015兼容模式)完整复现的步骤,包含所有踩过的坑和解决方案。
4.1 环境准备:VC++ 6.0的“考古级”安装与配置
虽然项目声称兼容VC++ 6.0,但现代Windows已不原生支持。我的实测方案:
- 工具链:下载Visual Studio 6.0安装包(需从可信渠道获取),安装时勾选“Visual C++ 6.0”和“Platform SDK”;
- 兼容性补丁:安装后,在C:\Program Files\Microsoft Visual Studio\VC98\Bin\下运行vcvars32.bat,确保环境变量正确;
- 替代方案(推荐):用VS2015或VS2019打开.dsp文件(它会自动转换为.vcxproj)。转换后需手动修改两处:
1. 在项目属性 → 配置属性 → 常规 → 字符集,改为“使用多字节字符集”(MBCS),因为原始代码用CString处理ANSI字符串;
2. 在项目属性 → 配置属性 → C/C++ → 语言,关闭“符合标准的异常处理”(/EHsc),改为“否”(/EH-),否则try/catch可能报错。
提示:如果VS转换失败,可直接新建空MFC对话框工程,将源码中的
.cpp/.h文件全部拖入,并在stdafx.h里添加#include <winsock2.h>和#pragma comment(lib, "ws2_32.lib")。
4.2 Gmail服务器配置:开启IMAP/SMTP权限与应用专用密码
现代邮箱(Gmail、Outlook)已禁用“密码直连”,必须用应用专用密码。以Gmail为例:
1. 登录Google账户 → 点击右上角头像 → “管理您的账户”;
2. 左侧菜单 → “安全性” → 开启“两步验证”(必须先开这个);
3. 返回“安全性”页面 → 找到“应用专用密码” → 点击生成;
4. 应用类型选“邮件”,设备选“Windows计算机”,生成一串16位密码(如abcd efgh ijkl mnop);
5. 在程序中填写:
- SMTP服务器:smtp.gmail.com
- SMTP端口:587(非465!465是SSL端口,此项目用STARTTLS)
- POP3服务器:pop.gmail.com
- POP3端口:995(注意:源码默认用110端口,需手动改为995并启用SSL)
注意:源码中
CSocketClient::ConnectToServer()默认使用AF_INET和SOCK_STREAM,但未实现SSL/TLS握手。因此,若要用Gmail,必须将POP3端口改为995(SSL隐式加密),SMTP端口改为465(SSL隐式加密),并替换CSocketClient为支持SSL的版本(如OpenSSL的SSL_connect())。但作为学习项目,我建议先用网易邮箱(163.com) 测试,因其仍支持非SSL的smtp.163.com:25和pop.163.com:110,且只需开通“SMTP/POP3服务”(设置 → POP3/SMTP/IMAP → 开启)。
4.3 编译与调试:关键断点设置与日志追踪
编译成功后,不要急着点按钮。先在关键位置设断点:
- CSmtpProtocol::SendMail()开头:检查用户输入的收件人、主题、正文是否为空;
- CSocketClient::SendData()内部:观察发送的完整字符串,确认\r\n是否正确;
- CSocketClient::RecvResponse()循环内:监视每次recv()返回的字节数和内容,看是否截断。
最有效的调试方式是开启协议日志。在CSocketClient.cpp的SendData()和RecvResponse()里,添加:
// 发送前记录
TRACE(_T("SEND: %s"), strData);
// 接收后记录
TRACE(_T("RECV: %s"), strResponse);
然后在VC++ 6.0的“输出”窗口查看实时日志。你会看到类似:
SEND: AUTH LOGIN\r\n
RECV: 334 VXNlcm5hbWU6
SEND: dXNlcm5hbWU=\r\n
RECV: 334 UGFzc3dvcmQ6
SEND: cGFzc3dvcmQ=\r\n
RECV: 235 Authentication successful
如果卡在某一步(如RECV一直没返回),说明网络不通或服务器拒绝连接——此时立刻检查防火墙、端口、邮箱权限。
4.4 功能验证清单:确保每一步都“看得见、摸得着”
运行程序后,按顺序验证以下动作,每步都应有明确的日志反馈:
| 步骤 | 操作 | 预期日志 | 常见失败原因 |
|------|------|----------|--------------|
| 1 | 点击“连接POP3” | Connecting to pop.163.com... → +OK POP3 server ready | DNS解析失败(检查网络)、端口被封(尝试telnet pop.163.com 110) |
| 2 | 输入账号密码,点“登录” | Sending USER command... → +OK → Sending PASS command... → +OK | 密码错误(163邮箱密码非登录密码,是“授权码”)、账号格式错误(必须填完整邮箱如xxx@163.com) |
| 3 | 点“获取列表” | Sending LIST command... → +OK 3 messages → 列表控件显示3条邮件 | 服务器无新邮件(发一封测试邮件到该邮箱) |
| 4 | 选中一条,点“下载邮件” | Sending RETR 1 command... → +OK → 编辑框显示完整邮件头和正文 | 邮件内容含特殊字符导致解析乱码(检查CPop3Protocol::RetrieveMail()中字符串拼接是否用了+=而非Append()) |
| 5 | 点“发送邮件” | Sending HELO... → AUTH LOGIN... → 235 → MAIL FROM... → 250 → RCPT TO... → 250 → DATA... → 354 → . → 250 OK | 收件人域名不存在(如test@xxx)、正文未以.结束 |
5. 扩展开发指南:如何在现有骨架上添加附件、本地存储等实用功能
这个项目的最大价值,在于它是一块“可生长的土壤”。下面是我基于此源码,为团队扩展的三个高频需求,附带具体实现思路和代码片段。
5.1 添加附件支持:MIME编码与二进制流处理
SMTP发送附件,本质是构造MIME多部分消息。核心步骤:
1. 生成唯一边界字符串(如----=_NextPart_000_001A_01DABC12.34567890);
2. 邮件正文用text/plain部分,附件用application/octet-stream部分;
3. 每部分以--边界开头,以空行分隔,结尾用--边界--。
在CSmtpProtocol::SendMail()中插入:
// 构造MIME头
CString strBoundary = GenerateBoundary(); // 生成随机边界
strMailHeader += "MIME-Version: 1.0\r\n";
strMailHeader += "Content-Type: multipart/mixed; boundary=\"" + strBoundary + "\"\r\n";
// 构造正文部分
CString strBodyPart;
strBodyPart += "--" + strBoundary + "\r\n";
strBodyPart += "Content-Type: text/plain; charset=\"gb2312\"\r\n\r\n";
strBodyPart += strBody + "\r\n";
// 构造附件部分(以txt为例)
CString strAttachPart;
strAttachPart += "--" + strBoundary + "\r\n";
strAttachPart += "Content-Type: text/plain; name=\"attach.txt\"\r\n";
strAttachPart += "Content-Transfer-Encoding: base64\r\n";
strAttachPart += "Content-Disposition: attachment; filename=\"attach.txt\"\r\n\r\n";
// 读取附件文件并base64编码
CFile file;
if (file.Open(_T("c:\\temp\\attach.txt"), CFile::modeRead)) {
DWORD dwSize = file.GetLength();
BYTE* pBuf = new BYTE[dwSize];
file.Read(pBuf, dwSize);
strAttachPart += Base64Encode(CString((char*)pBuf, dwSize));
delete[] pBuf;
file.Close();
}
// 合并所有部分
strFullMail = strMailHeader + "\r\n" + strBodyPart + strAttachPart + "--" + strBoundary + "--\r\n";
注意:
DATA命令后发送的不再是纯文本,而是完整的MIME结构,且结尾的.必须单独一行。因此SendData()后要分两次发:先发MIME内容,再发".\r\n"。
5.2 本地邮件存档:SQLite轻量级数据库集成
为避免每次启动都重新下载邮件,用SQLite存储邮件摘要。步骤:
1. 下载sqlite3.dll和sqlite3.h,加入工程;
2. 在CPop3Protocol::RetrieveMail()成功后,将MAIL_INFO和邮件正文存入数据库:
// 创建表
sqlite3_exec(m_pDB, "CREATE TABLE IF NOT EXISTS mails(id INTEGER PRIMARY KEY, index INTEGER, size INTEGER, subject TEXT, sender TEXT, date TEXT, content TEXT);", 0, 0, 0);
// 插入数据
CString strSql;
strSql.Format("INSERT INTO mails(index, size, subject, sender, date, content) VALUES(%d, %d, '%s', '%s', '%s', '%s');",
info.nIndex, info.nSize, strSubject, strSender, strDate, strContent);
sqlite3_exec(m_pDB, strSql, 0, 0, 0);
- UI层“刷新”按钮改为优先查数据库,无数据时再连POP3下载。
5.3 多账户管理:配置文件驱动与运行时切换
将服务器地址、端口、账号密码从硬编码移到config.ini:
[Account1]
Name=Work Email
SMTP_Server=smtp.exmail.qq.com
SMTP_Port=25
POP3_Server=pop.exmail.qq.com
POP3_Port=110
User=work@company.com
Pass=xxxxxx
[Account2]
Name=Personal Email
SMTP_Server=smtp.163.com
...
在CMyEmailClientDlg::OnInitDialog()中读取:
GetPrivateProfileString(_T("Account1"), _T("Name"), _T(""), m_strAccountName, 256, _T("config.ini"));
GetPrivateProfileInt(_T("Account1"), _T("SMTP_Port"), 25, _T("config.ini"));
UI添加下拉框选择账户,切换时重新初始化CPop3Protocol和CSmtpProtocol实例。
6. 常见问题排查与独家避坑经验:那些文档里不会写的“血泪教训”
最后分享我在带学员实操时,高频出现的5个问题及根治方案。这些问题,90%的教程都不会提,但它们才是卡住新手的真正绊脚石。
6.1 问题:recv()返回0字节,程序卡死
现象:点击“连接POP3”后,日志停在Connecting...,无后续响应。
根因:recv()在连接建立后,若服务器未立即发欢迎消息,会阻塞等待。而某些服务器(如企业邮箱)可能因安全策略延迟发送+OK。
解决方案:在CSocketClient::RecvResponse()中,为recv()添加超时:
// 设置socket接收超时为5秒
int nTimeout = 5000;
setsockopt(m_hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&nTimeout, sizeof(nTimeout));
// recv()后检查错误
int nRet = recv(m_hSocket, szBuffer, sizeof(szBuffer)-1, 0);
if (nRet == 0) {
// 对端关闭连接
return false;
} else if (nRet == SOCKET_ERROR) {
int nErr = WSAGetLastError();
if (nErr == WSAETIMEDOUT) {
// 超时,重试或报错
AfxMessageBox(_T("服务器响应超时,请检查网络"));
return false;
}
}
6.2 问题:中文邮件主题乱码,显示为=?GBK?B?...?=
现象:发送中文主题,收件方看到一堆=?GBK?B?...?=。
根因:源码未实现MIME编码,直接发送UTF-8或GBK字节流,被服务器当作乱码处理。
解决方案:在CSmtpProtocol::SendMail()中,对中文主题进行MIME编码:
// 将GBK字符串转为base64 MIME编码
CString EncodeMimeHeader(const CString& strText) {
// 先转为UTF-8(更通用)
int nLen = WideCharToMultiByte(CP_UTF8, 0, strText, -1, NULL, 0, NULL, NULL);
char* pUtf8 = new char[nLen];
WideCharToMultiByte(CP_UTF8, 0, strText, -1, pUtf8, nLen, NULL, NULL);
CString strBase64 = Base64Encode(CString(pUtf8));
delete[] pUtf8;
return _T("=?UTF-8?B?") + strBase64 + _T("?=");
}
// 使用:strMailHeader += "Subject: " + EncodeMimeHeader(strSubject) + "\r\n";
6.3 问题:DELE后邮件未删除,重启程序又出现
现象:点了删除,列表里消失,但下次启动程序,邮件又回来了。
根因:POP3的DELE只是标记,必须发送QUIT命令,服务器才会真正删除。而源码中QUIT只在退出程序时调用。
解决方案:在UI的“删除”按钮事件中,增加QUIT后重连逻辑:
void CMyEmailClientDlg::OnBnClickedBtnDelete() {
// ... 执行 DELE 命令
m_pop3Protocol.DeleteMail(nSelectedIndex);
// 立即发送 QUIT 并重连,强制服务器执行删除
m_pop3Protocol.Quit();
m_pop3Protocol.Connect(); // 重连后自动清除标记
m_pop3Protocol.Login(m_strUser, m_strPass);
}
6.4 问题:VS2019编译报错error C2664: 'int send(...)' : cannot convert parameter 2 from 'const char *' to 'const char *'
现象:CSocketClient::SendData()中send()调用失败。
根因:VS2015+默认CString是Unicode,strData.GetBuffer()返回wchar_t*,而send()需要char*。
解决方案:强制转换为多字节:
// 在SendData()中
CStringA strA(strData); // CStringA是ANSI版本
send(m_hSocket, strA, strA.GetLength(), 0);
6.5 问题:Gmail始终返回534-5.7.9 Application-specific password required
现象:用应用专用密码仍认证失败。
根因:Gmail的应用专用密码,必须在生成后立即用于首次登录。如果之前用过旧密码登录过,Gmail会锁定该应用密码。
解决方案:
1. 进入Google账户 → 安全性 → 应用专用密码 → 删除所有现有密码;
2. 重新生成一组新密码;
3. 立刻在邮件客户端中输入这组新密码,不要做其他操作;
4. 如果仍失败,检查是否开启了“允许不够安全的应用”(已废弃,必须用应用专用密码)。
这个VC++邮件客户端,就像一把生锈但齿纹清晰的瑞士军刀。它不锋利,不现代,但每一个锯齿都对应着网络协议的一个真实切面。当你亲手把它磨亮,那些曾经模糊的AUTH LOGIN、RETR、base64,就不再是RFC文档里的铅字,而成了你肌肉记忆的一部分。我至今记得第一个学员跑通235 Authentication successful时,盯着控制台日志反复看了三分钟的样子——那不是代码跑通了,是他第一次听见了互联网的心跳。
简介:一个轻量级Windows邮件客户端源码项目,用VC++和MFC开发,能直接连接邮箱服务器收发纯文本邮件。代码里已经预设了SMTP和POP3服务器地址,填上账号密码就能运行,支持登录验证(AUTH LOGIN)、获取邮件列表、下载邮件内容(RETR)、删除邮件(DELE)等基础操作。底层用原始Socket通信,包含Base64编解码、协议命令拼接、响应解析等细节实现,所有逻辑模块清晰分离,比如网络层、协议层、界面层各自独立。工程文件兼容VC++ 6.0和早期Visual Studio,打开即可编译,不需要额外依赖库。适合想动手理解邮件协议怎么跑起来的开发者,比如学习POP3怎么取邮件、SMTP怎么发信、邮箱认证流程怎么走。后续加附件上传、本地邮件存档、多账户管理等功能也很方便,在现有结构上扩展就行。
更多推荐



所有评论(0)