
简介这套资源是一份基于C编写、可在Visual Studio 2008下编译运行的SMTP邮件发送客户端程序主要面向需要学习邮件协议原理、进行网络编程或希望为项目加入邮件通知功能的开发者。压缩包共42个文件包含13个头文件、12个C源文件以及dsw、vcproj、sln、rc等Visual Studio工程与界面资源文件整体仅1.23MB结构紧凑便于快速阅读和移植。目前已有131人学习下载。源码按功能拆分为SMTP类、邮件类、网络连接类、MIME编码与Base64封装等模块覆盖TCP连接、HELO/EHLO、AUTH认证、MAIL FROM、RCPT TO、DATA到QUIT的完整SMTP命令交互流程头文件与cpp文件一一对应如SMTP.h/.cpp负责协议交互、MIMEMessage.h/.cpp处理邮件结构与Base64编码调试和二次开发时可按需定位。通过分析MIMEMessage、MailMessage、SMTP等核心文件读者可以理解邮件头部与正文的构造方式、socket通信细节以及错误处理技巧并可将这套代码扩展为支持附件、HTML邮件和多收件人的客户端具有很强的参考和复用价值。1. SMTP.rar 在讲什么C 在 VS 里发邮件先搞清楚这件“小事”的代价拿到一个叫 SMTP.rar 的压缩包里面十有八九是 C 写的 SMTP 邮件发送源码配套 Visual Studio 工程使用。这个组合听起来小实际做起来却常让人翻车明明代码能编译发出去的邮件却进垃圾箱明明执行返回了成功对方就是收不到换个端口整个链路又不通。这类工程的核心不是“会不会调 socket”而是把 SMTP 协议、MIME 格式、认证编码和 VS 的工程配置四件事同时做对。这篇文章按选型、实现、排错的顺序把 C 在 VS 里做邮件发送讲透适合给上位机或监控程序加邮件告警的开发者也适合拿到源码包却编译不过、想自己改通的人。先确定走哪条技术路线再谈具体怎么写。2. C 发邮件的三条技术路线先选型再动手别上来就解压编译很多人解压 SMTP.rar 后第一件事是双击 .sln 点编译然后被一堆链接错误劝退。我一般会先花十分钟判断这个包里的代码走的是哪条路线因为三条路线的依赖、调试方式和坑完全不一样用错思路去改代码会越改越乱。2.1 三条路线的对比第三方库、COM 组件、Winsock 裸协议C 在 Windows 上发邮件常见做法无非三种集成 libcurl 这类第三方库、调用系统 COM 组件、直接用 Winsock 写 SMTP 协议。它们的差别非常大先看一张对比表。技术路线典型形态依赖SSL/TLS 支持可控性最容易踩的坑libcurlcurl_easy APIlibcurl.dll 或静态库完整支持 SMTPS 和 STARTTLS中动态库版本与 /MT、/MD 不匹配CDO.MessageCOM 组件系统自带支持低不同 Windows 版本行为不一致错误信息不透明Winsock 裸协议自己写 socket 会话ws2_32.lib需自己实现或绕开高Base64 编码、回码判断、TLS 握手容易出错三条路里我建议优先考虑 libcurl。原因很直接SMTP 发送不是一个单独的函数而是一串状态交互——连接、EHLO、AUTH、MAIL FROM、RCPT TO、DATA、QUIT每一步都要处理服务端返回的状态码。libcurl 把这些会话细节封装成了几个选项同时把 Base64、MIME 分块、TLS 证书校验都处理掉了你要维护的代码量会少一个量级。CDO.Message 写起来代码最少但它是 COM 组件发布到服务器上时可能出现“操作失败”之类让人摸不着头脑的错误而且没法精细控制超时和 TLS 策略监控类程序发告警时很不放心。Winsock 裸协议则完全是另一回事你拿到 SMTP.rar 里如果是这种源码好处是你能看到每一步在干什么出问题时可以精确指出是哪条命令被服务端拒绝坏处是 SMTP 协议里的细节多得超出预期比如行结束符必须是 CRLFAUTH LOGIN 的账号密码要做 Base64DATA 阶段要以单独一行的英文句号结束。这些细节错一个邮件就发不出去而且服务端回的错误码往往是 554 这种非常笼统的信息。选型的核心判断标准是如果你的项目是告警通知、状态上报这类功能性需求用 libcurl如果是为了学习协议、做 SMTP 压力测试或者包里的代码已经是可用的裸协议实现才值得去啃 Winsock。2.2 用文件清单判断 SMTP.rar 属于哪条路判断包里是哪条路不需要先编译列出文件清单就能看个大概。在 Windows 上如果你装了 7-Zip可以直接在命令行里看7z l SMTP.rar这个命令列出压缩包里的所有文件名、大小和路径不真正解压。我判断的依据很简单出现 curl.h、libcurl、curl_easy_setopt 字样就是 libcurl 路线出现 CDO、IMail、Outlook 之类关键字是 COM 路线出现 Winsock、socket、base64、SmtpClient.cpp 这类文件基本就是裸协议路线。多数网上流传的 SMTP.rar 属于第三种因为它体积小、文件纯粹通常就是几个 .cpp 和 .h没有一堆第三方库文件。文件清单还能帮你避开一个暗坑如果包里带着 .lib 和 .dll比如 libcurl.lib、libcurl.dll说明它依赖动态库你在 VS 里编译没问题但把生成的 exe 拷到没有这些 DLL 的机器上就会启动失败。如果包里只有头文件和源码那就是静态编译发布时省心一些但你需要自己在 VS 里配置附加依赖项。我拿到包后还会看一眼有没有 .vcxproj 文件有的话直接看里面的AdditionalDependencies标签里面写的 ws2_32.lib、libcurl.lib 就是它的依赖底座一目了然。2.3 VS 工程配置的依赖底座WS2_32 和 VC Redistributable不管是哪条路线在 Visual Studio 里跑 C 邮件发送都绕不开两个底层的工程配置问题。第一个是链接 Winsock 库。只要是裸协议路线就必须在你的工程里加上 ws2_32.lib否则链接器会报一堆 unresolved external symbol常见做法是#pragma comment(lib, ws2_32.lib)或者在项目属性 → 链接器 → 输入 → 附加依赖项里手动写。第二个是运行库匹配问题。VS 编译出来的 C 程序默认使用动态运行库 /MD这意味着目标机器上要有对应版本的 VC 运行库。网上很多 SMTP.rar 里的工程是老项目可能在 VS2010、VS2015 时代写的拿到 VS2022 里打开后编译器会提示升级工程。升级本身问题不大但如果你把生成的 exe 丢到一台干净的 Windows Server 上很可能会报“无法启动此程序因为计算机中丢失 vcruntime140.dll”。这跟代码没关系纯粹是目标机器缺少 Visual C Redistributable 运行库。处理方式要么在目标机器装对应版本的运行库要么把工程改成 /MT 静态链接让运行库直接编进 exe。这个配置在项目属性 → C/C → 代码生成 → 运行库里改Debug 和 Release 都要改改完别忘点“重新生成”。提示拿到 SMTP.rar 先别急着编译先确认工程是 /MD 还是 /MT。如果它依赖 libcurl.dll 而工程又用了 /MT链接时大概率会摔在“无法解析的外部符号 __imp__curl_easy_init”这类错误上。动态库配动态运行库静态库配静态运行库这是 VC 下绕不过的匹配规则。3. 用 libcurl 在 VS2019/2022 跑通 SMTP 发送最小工程、五个必调参数与调试开关如果你决定用 libcurl 路线这一章可以直接照着做。我以 VS2019 和 VS2022 为例因为这两个版本是目前最常见的 C 开发环境操作路径基本一致。目标是用最小的代码量让一封带中文标题的纯文本邮件从你的程序里发出去。3.1 准备 libcurlvcpkg 安装与工程属性在 Windows 上给 VS 装 libcurl我一般用 vcpkg它能把 curl 的 Windows 版本头文件和库文件自动放到 VS 能找到的地方。如果你机器上还没装 vcpkg先克隆仓库并执行 bootstrap然后安装 curlvcpkg install curl:x64-windows vcpkg integrate install第一条命令安装 curl 的动态库版本x64-windows 表示 64 位动态库第二条命令让 VS 自动识别 vcpkg 的 include 和 lib 路径。如果你想要静态版用curl:x64-windows-static-md但要记住静态版要求你的工程运行库也选择 /MD否则链接阶段会报错。装完之后在 VS 里新建一个控制台工程然后检查两处配置项目属性 → C/C → 常规 → 附加包含目录应该能看到 vcpkg 的 include 路径链接器 → 输入 → 附加依赖项里需要加上libcurl.lib;ws2_32.lib。vcpkg integrate 会自动设置大部分路径但 ws2_32.lib 不一定被带上手动加一下更保险因为 curl 在 Windows 上依赖 Winsock。配置完后先编译一个空工程确认环境没问题再往下写代码。3.2 最小发送代码一次完整的 SMTP 会话调用下面是一段可以直接放进控制台工程 main 函数的代码作用是往指定收件人发一封纯文本邮件#include cstdio #include cstring #include string #include curl/curl.h // 上传回调libcurl 会反复调用它来读取邮件正文 struct UploadData { std::string data; size_t pos 0; }; static size_t payload_source(char* buffer, size_t size, size_t nitems, void* userp) { UploadData* upload static_castUploadData*(userp); size_t max size * nitems; if (upload-pos upload-data.size()) return 0; // 返回 0 表示数据读完 size_t n upload-data.size() - upload-pos; if (n max) n max; memcpy(buffer, upload-data.c_str() upload-pos, n); upload-pos n; return n; } int main() { CURL* curl curl_easy_init(); if (!curl) { fprintf(stderr, curl init failed\n); return -1; } // 邮件正文注意行尾必须是 CRLF这是 SMTP 协议的要求 std::string payload From: \Monitor\ senderexample.com\r\n To: engineerexample.com\r\n Subject: Alarm from C\r\n MIME-Version: 1.0\r\n Content-Type: text/plain; charsetUTF-8\r\n \r\n Disk usage exceeded 90%.\r\n; UploadData upload{ payload, 0 }; curl_easy_setopt(curl, CURLOPT_URL, smtp://mail.example.com:25); curl_easy_setopt(curl, CURLOPT_MAIL_FROM, senderexample.com); struct curl_slist* rcpt nullptr; rcpt curl_slist_append(rcpt, engineerexample.com); curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, rcpt); curl_easy_setopt(curl, CURLOPT_READFUNCTION, payload_source); curl_easy_setopt(curl, CURLOPT_READDATA, upload); curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); CURLcode res curl_easy_perform(curl); if (res ! CURLE_OK) { fprintf(stderr, send failed: %s\n, curl_easy_strerror(res)); } curl_slist_free_all(rcpt); curl_easy_cleanup(curl); return res CURLE_OK ? 0 : -1; }这段代码的逻辑是先用curl_easy_init创建 curl 会话然后构造一个符合 RFC 5322 格式的邮件正文包括 From、To、Subject、MIME 头和一个空行分隔后的邮件体。payload_source函数是上传回调libcurl 在发送 DATA 阶段会反复调用它来读取数据每次读一块读完返回 0 通知 curl 数据结束。这里的关键点是SMTP 的邮件头和正文之间必须有一个空行每行必须以 CRLF也就是\r\n结尾如果写成\n部分邮件服务商会拒绝接收。参数里值得细说的是CURLOPT_UPLOAD。这个选项置 1 表示让 curl 以上传模式工作对 SMTP 来说就是“发送邮件”而不是“读取邮件”。忘记设这个参数curl 会尝试用 POP3 或 IMAP 的语义去操作表现是发不出去而且报错信息很怪。CURLOPT_MAIL_RCPT接收的是一个 curl_slist 链表可以挂多个收件人实现群发但注意它不解析“收件人姓名”直接写邮箱地址就行。CURLOPT_TIMEOUT我习惯设 30 秒防止 SMTP 服务器无响应时程序卡死。3.3 必调的 5 个参数和调试开关上面那段代码能跑通但换个环境就不一定了。我把实际项目中必须调的参数列一下方便你根据自己的邮箱服务商调整。参数作用我的常用配置CURLOPT_URL服务器地址和端口25 端口用 smtp://465 用 smtps://587 用 smtp:// 加 USE_SSLCURLOPT_USE_SSL是否启用 TLS587 端口必须设 CURLUSESSL_ALLCURLOPT_USERNAME / CURLOPT_PASSWORDAUTH 登录凭据很多邮箱要用“授权码”而非登录密码CURLOPT_SSL_VERIFYPEER证书校验测试时可设 0生产必须留 1CURLOPT_CONNECTTIMEOUT连接阶段超时10 秒和总超时分开设我调试邮件发送时最常用的手段是打开CURLOPT_VERBOSE。在curl_easy_perform之前加一行curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);然后跑程序就能在控制台看到类似* Connected to mail.example.com和 250 OK这样的完整会话过程。带的是服务器返回的状态行带的是你的程序发给服务器的命令。出问题时先看这里如果服务器一直回535就是认证失败如果卡在230附近不动多半是 TLS 协商异常。这个开关比任何断点都好用因为它展示的是协议层发生的事。4. 拆读 SMTP.rar 里的 Winsock 裸协议从连接握手到回码表的完整链路如果你手里的 SMTP.rar 是裸协议源码这一章就是你要补的课。裸协议路线不看懂状态机是没法改通的它不是“调用一个函数”那样的事而是一次有来有回的对话每一步都要等对方把话说完。4.1 从 WSAStartup 到 QUIT一次会话的状态机一次完整的 SMTP 发送在协议层的顺序是固定的我先把状态序列写出来你拿着源码对照着看WSAStartup - socket - connect(25端口) - 收 220 - 发 EHLO/HELO - 收 250 - 发 AUTH LOGIN - 收 334 - 发 base64(账号) - 收 334 - 发 base64(密码) - 收 235 - 发 MAIL FROM - 收 250 - 发 RCPT TO - 收 250 - 发 DATA - 收 354 - 发邮件正文 \r\n.\r\n - 收 250 - 发 QUIT - 收 221 - closesocket这里有个经常被新手的代码去掉的细节EHLO 之后服务器回的是 250AUTH LOGIN 之后服务器回的是 334这两个状态码如果没按预期收到后面的步骤就不能继续。很多网上流传的源码把 recv 的返回值只做了“大于 0”的判断不检查内容结果就是一个小错误被放大成“发送失败”却不知道发生在哪一步。一个最小可编译的连接骨架是这样的#include winsock2.h #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsa; WSAStartup(MAKEWORD(2, 2), wsa); SOCKET sock socket(AF_INET, SOCK_STREAM, 0); if (sock INVALID_SOCKET) return -1; sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(25); inet_pton(AF_INET, 202.106.0.10, addr.sin_addr); // 替换成你的 SMTP 服务器 IP if (connect(sock, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { int err WSAGetLastError(); // 10060 是连接超时10061 是连接被拒绝 closesocket(sock); WSACleanup(); return -1; } char buf[1024]; int n recv(sock, buf, 1023, 0); buf[n] \0; // 正常情况下 buf 里应该以 220 开头 closesocket(sock); WSACleanup(); return 0; }这段代码的逻辑是初始化 Winsock 库创建一个 TCP socket填入服务器 IP 和端口后发起连接然后接收服务器发来的第一条欢迎消息。inet_pton只能接受点分十进制 IP如果你只有域名得先用getaddrinfo做解析这是很多老代码没处理好的地方。WSAGetLastError返回的 10060 和 10061 要区分开前者通常是网络不通或防火墙拦截后者是服务器明确拒绝了连接。看到 10060 时别反复重试先排查网络和端口看到 10061 时查一下服务器 SMTP 服务是否启动、端口是否被占用。4.2 Base64 与 AUTH LOGIN最容易被抄错的一段AUTH LOGIN 是 SMTP 认证里最常见的流程它的麻烦在于用户名和密码都要先做 Base64 编码再发送。很多裸协议源码里的 Base64 函数是从老项目里抄来的变量名都懒得改算法本身也容易在补位符号上出错。我写了一个最小实现可以直接替换包里的对应函数std::string base64_encode(const std::string in) { static const char tbl[] ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/; std::string out; int val 0, bits 0; for (unsigned char c : in) { val (val 8) | c; bits 8; while (bits 6) { out.push_back(tbl[(val (bits - 6)) 0x3F]); bits - 6; } } if (bits 0) { out.push_back(tbl[(val (6 - bits)) 0x3F]); out.push_back(); if (bits 2) out.push_back(); } return out; }这个函数把每 3 个字节的二进制数据编码成 4 个 Base64 字符。逻辑是不断把字节移入一个整数缓冲区每满 6 位就查表输出一个字符处理完所有字节后如果还剩 2 位或 4 位有效数据就补一个或两个字符把输出补齐到 4 的倍数。如果你把最后的补位逻辑写错比如无论剩余多少都补两个等号编码结果会错服务器会回 535 认证失败。在 AUTH LOGIN 阶段发送的格式是这样的先发字符串AUTH LOGIN\r\n收到服务器回334 VXNlcm5hbWU6这是“Username:”的 Base64 编码再发送base64(账号) \r\n收到334 UGFzc3dvcmQ6这是“Password:”的编码最后发送base64(密码) \r\n收到235就说明认证通过。注意这里发送的编码结果后面必须加 CRLF只发编码不加行尾符服务器会一直等你的下一行然后超时断开。这是裸协议实现里出现频率最高的 bug。4.3 在 VS 调试器里观察回码断点、监视窗口和状态机裸协议代码出问题时我建议在 VS 里用调试器直接看 recv 返回的内容而不是靠 printf 猜。方法很简单在每次recv调用之后那一行打一个断点把接收缓冲区变量加到监视窗口然后单步执行。VS 的调试器能直接显示 char 数组的字符串内容你马上就能看到服务器实际回了什么。回码的状态判断是这类的核心我给你一张常用回码表调试时对照着用回码含义你的下一步动作220服务就绪发送 EHLO250请求完成继续下一条命令334等待认证数据发送 Base64 编码的账号或密码235认证成功发送 MAIL FROM354开始输入邮件发送邮件正文并以.\r\n结束221会话关闭关闭 socket用断点观察回码时有一个“黑匣子”现象值得注意很多邮件服务器不会严格按照 RFC 报错比如你发的邮箱地址格式不对它可能回 250 表示接受然后在你发 DATA 之后才回 554 拒收。所以只看前几步的回码是不够的必须把整个会话的回码都记录下来。我通常会在代码里把每次 recv 的内容和发送的命令全部写进一个日志文件出问题时拉完整日志来看而不是只靠调试器看某一帧的状态。这样的日志在排查“为什么对方收不到”时价值比断点大得多。5. 邮件发送避坑端口、编码、授权码、附件与运行时的 5 个排查记录邮件发送的坑大多数不是代码逻辑本身的问题而是协议细节和服务器策略的问题。这里整理 5 个我实际遇到过、而且网上源码包最容易踩的坑每一条都按照现象、原因、解决的顺序来。5.1 现象连接 25 端口超时换服务器也一样用裸协议代码连smtp.example.com:25connect 一直超时换了几个 SMTP 服务器还是连不上。用 telnet 手动连同一个地址也连不通但用浏览器能正常访问网页。原因是端口策略问题。很多云服务器和企业邮件服务商为了反垃圾邮件默认封禁了出站的 25 端口或者只对白名单开放。465 端口是 SSL 隐式加密587 端口是 STARTTLS 加密这两个端口通常不会封。你需要在代码里把端口从 25 改成 587同时处理 TLS 握手。如果你用的是 Winsock 裸协议自己实现 TLS 非常麻烦常见做法是改用 libcurl 并把CURLOPT_URL设为smtp://mail.example.com:587再设CURLOPT_USE_SSL, CURLUSESSL_ALL。如果是 465 端口则直接设成smtps://mail.example.com:465。5.2 现象中文主题变成乱码英文标题正常邮件能发出去但收件人那边看到的主题是?UTF-8?B?xxxxx?或者一片诡异的符号。正文里的中文如果直接放在 DATA 阶段大部分服务商能正确识别但主题属于邮件头邮件头对非 ASCII 字符有严格限制。原因是 SMTP 协议本身是 ASCII 协议邮件头里的非 ASCII 字符必须经过 MIME 编码。规范做法是把 UTF-8 编码后的标题内容再做 Base64包成?UTF-8?B?...?的格式。比如“磁盘告警”四个字先转成 UTF-8 字节再做 Base64得到一个字符串然后拼成Subject: ?UTF-8?B?这个字符串?。如果你用的是 libcurl在构造 payload 时就要按这个格式拼如果你在裸协议代码里看到Subject:后面直接跟中文那基本可以判定这代码在非中文环境会翻车。5.3 现象AUTH LOGIN 返回 535 5.7.8 认证失败代码看起来完全正确Base64 编码也没问题账号密码都对但服务器就是回 535。很多邮箱服务商有独立的“客户端授权码”机制你用浏览器登录时验证的是登录密码但用 SMTP 客户端发邮件时必须使用在邮箱设置里单独生成的“授权码”或“客户端专用密码”。解决方法是登录邮箱网页端找到“客户端设置”或“安全设置”里生成授权码的入口生成一个新的授权码把它作为CURLOPT_PASSWORD或裸协议里 Base64 编码的密码。特别提醒一句授权码通常只在生成时显示一次如果你在代码里硬编码了它建议不要把源码提交到公共仓库。有些服务商还要求账号必须是完整邮箱地址不能只写 前面的部分。5.4 现象带附件的邮件发出去了但附件内容损坏或正文被截断基于裸协议写的发送代码在 DATA 阶段把邮件正文和附件一起发收件人收到的附件打不开或者正文缺了一半。原因几乎总是两个一是行结束符用了\n而不是\r\n二是邮件结束标记写错SMTP 要求正文结束后单独发一行英文句号加回车换行也就是\r\n.\r\n很多代码只发了\n.\n。解决方法是统一所有网络发送的行尾符。邮件头和正文的每一行之间用\r\n分隔附件如果是 Base64 编码每 76 个字符要换一次行换行符同样用\r\n。正文结束标记前面还要空一行整个 DATA 阶段的最后四个字节必须是\r\n.\r\n序列。排查时可以在日志里检查收发的字节流确认服务器收到的是一个完整的.\r\n\r\n结尾。如果你不想在这些细节上耗时间用 libcurl 会更省心它内部对 MIME 分块的构造已经处理好了这些边界。5.5 现象程序拷贝到别的机器上启动报错丢失 vcruntime140.dll代码在开发机上编译运行都正常把 exe 拷到客户机器上双击提示“无法启动此程序因为计算机中丢失 vcruntime140.dll”或者提示找不到 libcurl.dll。这不是代码问题而是你依赖的 VC 运行库和目标机器的环境不一致。原因是 VS 默认使用动态运行库 /MD生成的可执行文件依赖vcruntime140.dll、msvcp140.dll这些运行库文件。目标机器如果没有安装对应版本的 Visual C Redistributable程序就会启动失败。解决方法是两个方向一是让客户机器安装对应版本的 VC Redistributable 运行库注意要装和你的工程位数一致的 x64 或 x86 版本二是把工程改成静态链接 /MT让运行库代码直接编进 exe。改成 /MT 后发布时还要留意如果你的程序依赖 libcurl.dll这个 DLL 本身也可能是动态编译的到时候还是需要把 DLL 一起带上。我一般会同时做两件事工程用 /MT 编译发布包带上用到的所有 DLL这样不管对方装没装运行库都能跑。6. 确认邮件真的发出去会话日志、Received 链与自检清单写完发送代码之后最重要的一步是验证它真的“发出去”了。这里的“发出去”不是指程序不报错而是指邮件进入了对方邮件系统的投递队列。判断标准不是 curl 返回 CURLE_OK 或者你的 send 函数返回成功而是整条 SMTP 会话的最终状态。我常用的验证方法是开启完整会话日志。libcurl 环境下设置CURLOPT_VERBOSE, 1L后运行输出里能看到每一步的交互过程。你要找的关键证据是最后几行如果邮件被接收会看到 250结尾的回码如果被拒收服务器会回554或550这类错误。裸协议代码也一样把自己的日志文本保存下来检查是否完整走完了220 → 250 → 235 → 250 → 354 → 250 → 221这条链路。任何一个环节没走到都说明最后那封邮件其实没发成功。比会话日志更进一步的验证是发给自己或一个专门用来测试的邮箱然后打开收到的邮件查看完整邮件头里的 Received 行。一封正常的邮件从你的程序发出后会在邮件头里留下一串 Received 记录第一条应该标识你的客户端 IP 和发送时间。如果收件箱里没有去垃圾箱里找垃圾箱里也没有基本可以断定邮件没有被对方服务器接受。另一个常见的反垃圾问题是发件域没有配置 SPF 记录导致邮件被对方服务商标记为垃圾邮件这类问题跟代码无关需要你在发件域的 DNS 里加一条 SPF TXT 记录声明哪些 IP 允许发送该域名的邮件。我过去有一段时间只确认了curl_easy_perform返回 CURLE_OK 就认为邮件发送成功直到对方说没收到才打开会话日志看到服务器在 DATA 阶段回了 451 临时失败。原因是同一时间内连发了三条告警触发了限流。那之后我养成了一个习惯所有邮件发送代码保留一个日志开关默认关掉线上出问题就打开看完整会话并且发完邮件后主动查一下 Received 头确认投递链路。这条经验帮我省了不少排查时间希望你也能用得上。本文还有配套的精品资源点击获取