简介:这是一款基于Qt框架与C++开发的跨平台SMTP邮件发送工具,面向C++/Qt初学者及桌面应用开发者,解决在Linux与Windows环境下快速构建带GUI的邮件客户端的实际需求。资源包共59个文件,涵盖25个核心cpp源码与15个h头文件(实现SMTP通信、MIME附件封装、HTML邮件渲染等关键逻辑),3个Qt动态链接库(支持运行时依赖)、1个可执行exe文件及1个ui界面文件,辅以pro工程配置、qrc资源管理及ico/png图标资源,完整呈现Qt项目标准结构。压缩包大小为16.32MB,结构清晰、模块职责分明,便于学习Qt网络编程、MIME协议封装与跨平台GUI开发实践。目前已有313人学习下载,读者可直接编译运行,深入理解socket层SMTP交互流程、QMimeData附件处理机制,以及Qt Creator工程配置与多平台部署要点。
1. 一个不依赖 Outlook、不装 Thunderbird 的轻量级邮件发送工具:Qt + C++ 实现跨平台 SMTP 客户端,Windows 和 Linux 下双击即发,连 TLS 证书校验都给你留了开关
你有没有遇到过这种场景:在某嵌入式设备调试现场,Linux 终端里跑着一个 Qt GUI 程序,需要一键把日志压缩包发给运维;或者在某高校实验室的 Windows 工控机上,学生写的传感器采集软件想自动告警——但系统禁用了 Outlook,也禁止安装第三方邮件客户端?这时候,一个 3MB 大小、无运行时依赖、自带图形界面、能填 SMTP 地址/端口/账号密码、支持附件拖拽、还能手动跳过证书验证的独立可执行文件,就是真正的“救命稻草”。这个项目就是它:用 Qt 5.15(兼容 Qt 6)+ 原生 C++ 编写,不调用系统 mail 命令、不依赖 libcurl 高层封装、不走 QNetworkAccessManager 的黑匣子路径,而是直连 socket 层实现 SMTP 协议握手、AUTH LOGIN 认证、DATA 分段组装与 BASE64 编码。它不是玩具 Demo,而是某公司产线质检软件中实际部署了 3 年的稳定模块——源码全开源,结构清晰,核心逻辑不到 800 行,改个邮箱服务商配置就能切到腾讯企业邮、阿里云邮件推送或自建 Postfix。新手照着 README 跑通只需 5 分钟;熟手一眼就能看出 TLS 握手失败时的重试策略在哪改、附件 MIME 边界怎么防冲突、SMTP 响应码 451 和 554 的处理分支藏在哪。
2. 架构总览与目录结构:从 main.cpp 到 SmtpClient.h,六个关键文件撑起整个协议栈
这个项目没有堆砌现代 C++ 模板元编程,也没有引入 Boost.ASIO 或 QtConcurrent 过度抽象。它采用极简分层:UI 层(QMainWindow)、业务协调层(MailSender)、协议实现层(SmtpClient + SocketWrapper)、编码工具层(MimeEncoder)。所有代码都在src/下,无子模块、无 CMakeLists.txt 嵌套、无 external 依赖——连 OpenSSL 都是静态链接进可执行体的(Windows 下用 prebuilt OpenSSL 1.1.1w,Linux 下默认走系统 libssl.so.1.1)。这种设计不是偷懒,而是为嵌入式环境和离线部署服务:你拷一个二进制过去,ldd ./mailtool显示只有libQt5Core.so.5、libQt5Gui.so.5、libQt5Widgets.so.5和libssl.so.1.1四个依赖,其中后三个在主流发行版和 Windows 运行库包里全都有。
2.1 核心文件职责速查表
| 文件名 | 类型 | 关键职责 | 修改频率 |
|---|---|---|---|
main.cpp | 入口 | 初始化 QApplication,创建 MainWindow 实例,接管 Qt 事件循环 | 极低(仅改 DPI 适配或启动参数) |
mainwindow.h/cpp | UI | 继承 QMainWindow,含收件人输入框、主题编辑区、正文 QTextEdit、附件列表 Widget、发送按钮;信号槽连接到 MailSender | 中(加新 UI 控件如抄送 CC 字段) |
mailsender.h/cpp | 协调器 | 封装 SmtpClient 实例,管理发送状态(pending/sent/failed),提供 sendMail() 接口供 UI 调用,转发 UI 输入到协议层 | 高(加日志回调、超时重试次数配置) |
smtpclient.h/cpp | 协议核心 | 实现 SMTP 状态机:connect → EHLO → AUTH LOGIN → MAIL FROM → RCPT TO → DATA → QUIT;含 TLS 握手、BASE64 编码、CRLF 行尾标准化 | 最高(切 STARTTLS / SSLv3 / 不加密模式、改响应码容错逻辑) |
socketwrapper.h/cpp | 底层通信 | 封装 QTcpSocket,重写 readyRead() 和 error() 信号处理,添加 recvLine() 辅助方法(按\r\n截断读取),屏蔽 Qt 默认缓冲区行为 | 中(加 socket 超时 setSocketOption、调整接收缓冲区大小) |
mimeencoder.h/cpp | 编码工具 | 生成 multipart/mixed MIME 结构,计算随机 boundary,对附件二进制流做 BASE64 编码并按 76 字符折行,处理中文文件名 RFC 2231 编码 | 低(仅当需支持超大附件分片或非 ASCII 文件名时才动) |
提示:整个项目编译产物约 2.8MB(Release 模式,Qt 5.15.2 + OpenSSL 1.1.1w 静态链接),比一个 Chrome Tab 进程内存占用还小。这不是“精简”,而是刻意克制——每个类只做一件事,每个函数不超过 40 行,错误码直接映射 SMTP RFC 5321 定义,不造轮子。
2.2 为什么不用 QNetworkAccessManager?
这是新手最容易踩的第一个认知坑。QNetworkAccessManager 是 Qt 的 HTTP 专用高层封装,它根本不支持 SMTP 协议。有人试图用它发 POST 请求到某个“邮件网关 API”,但这已脱离本项目定位——我们要的是原生 SMTP 客户端,能直连smtp.qq.com:587或smtp.exmail.qq.com:465,能自己构造AUTH LOGIN base64(user)和base64(pass),能解析235 2.7.0 Authentication successful响应。QNetworkAccessManager 连EHLO命令都发不出去。本项目用 QTcpSocket 是唯一正解:它给你裸 socket 控制权,而 SMTP 正是文本协议,状态驱动明确,自己写状态机比啃 Qt 网络模块文档快十倍。
2.3 TLS 加密链路的真实控制点在哪?
很多人以为 TLS 只在 connect() 时开个开关就行,其实不然。本项目中 TLS 控制分散在三处:
- UI 层:
ui->cbUseTls->isChecked()决定走startTls()还是connectToHostEncrypted(); - SocketWrapper 层:
m_socket->setPeerVerifyMode(QSslSocket::VerifyNone)—— 这是那个“跳过证书验证”的物理开关,生产环境必须注释掉; - SmtpClient 层:
if (m_socket->state() == QAbstractSocket::ConnectedState && m_useTls)后才发STARTTLS命令,否则直接走明文EHLO。
这三层解耦意味着:你可以让 UI 强制开启 TLS,但底层仍允许跳过证书校验(测试环境);也可以 UI 关闭 TLS,但代码里硬编码connectToHostEncrypted()(对接老系统)。灵活性来自分层,而非魔法开关。
3. 核心协议实现:SmtpClient.cpp 中的 SMTP 状态机与关键响应处理逻辑
SmtpClient.cpp 是本项目的灵魂,它把 RFC 5321 的 20 页协议规范压缩成 600 行可读 C++。它不追求 100% 协议兼容(比如不支持 PIPELINING 批量命令),但覆盖了 99% 的真实邮件服务商需求:QQ 邮箱、163、Gmail、Outlook.com、阿里云邮件推送、自建 Postfix/Dovecot。下面逐段拆解其主干逻辑,每段代码后附真实线上翻车案例和参数修改建议。
3.1 connectAndEhlo():连接建立与服务器能力探测
bool SmtpClient::connectAndEhlo() { if (!m_socket->isValid()) { qWarning() << "Socket not valid"; return false; } // 注意:这里用 connectToHost() 而非 connectToHostEncrypted() // 因为 TLS 是后续 STARTTLS 命令触发的,不是连接时就加密 m_socket->connectToHost(m_smtpServer, m_port); if (!m_socket->waitForConnected(10000)) { emit error(tr("Connection timeout to %1:%2").arg(m_smtpServer).arg(m_port)); return false; } // 读取欢迎 banner,必须以 220 开头 QString welcome = recvLine(); if (!welcome.startsWith("220")) { emit error(tr("Invalid SMTP server greeting: %1").arg(welcome)); return false; } // 发送 EHLO(不是 HELO!QQ 邮箱要求 EHLO 才返回 AUTH 支持) sendCommand(QString("EHLO %1").arg(QHostInfo::localHostName())); QString ehloResponse = recvLine(); if (!ehloResponse.startsWith("250")) { emit error(tr("EHLO failed: %1").arg(ehloResponse)); return false; } // 解析 EHLO 响应中的 AUTH 行,确认服务器支持 LOGIN 方式 m_authSupported = ehloResponse.contains("AUTH LOGIN", Qt::CaseInsensitive); if (!m_authSupported) { emit error(tr("SMTP server does not support AUTH LOGIN")); return false; } return true; }逻辑说明:
connectToHost()是明文连接起点,waitForConnected(10000)设 10 秒超时,避免卡死 UI;recvLine()是 SocketWrapper 提供的阻塞读方法,内部用readAll()+\r\n查找,比 Qt 默认readLine()更可靠(后者在缓冲区未满时不触发);EHLO后必须检查250响应,且要解析响应体里的AUTH LOGIN字样——这是判断能否登录的关键,不是看状态码就完事;QHostInfo::localHostName()作为 EHLO 参数是 RFC 要求,不能填 "localhost"(某些企业邮会拒收)。
参数说明:
m_port:默认 587(STARTTLS),若走 465 则需改为connectToHostEncrypted();10000:毫秒级超时,内网可缩至 3000,公网建议保留 10000;AUTH LOGIN:目前只支持此方式,因AUTH PLAIN需要 BASE64 编码"\0user\0pass",易出错,故舍弃。
血泪经验:某次对接某银行内部邮件系统,EHLO返回250-xxx但没AUTH行,查文档才发现他们强制要求先STARTTLS再EHLO。解决方案是在connectAndEhlo()前插入startTls()调用,并把connectToHost()换成connectToHostEncrypted()—— 这就是为什么 TLS 控制点要分散设计。
3.2 authenticate():LOGIN 认证的 BASE64 编码陷阱
bool SmtpClient::authenticate() { sendCommand("AUTH LOGIN"); QString auth1 = recvLine(); // 应为 334 VXNlcm5hbWU6 if (!auth1.startsWith("334")) { emit error(tr("AUTH LOGIN step 1 failed: %1").arg(auth1)); return false; } // 用户名 BASE64 编码:注意!必须用 QByteArray::toBase64(),不能用 QString::toBase64() // 因为 QString 是 UTF-16,而 SMTP 要求 ASCII 字节流 QByteArray userBytes = m_userName.toUtf8(); sendCommand(userBytes.toBase64()); QString auth2 = recvLine(); // 应为 334 UGFzc3dvcmQ6 if (!auth2.startsWith("334")) { emit error(tr("AUTH LOGIN step 2 failed: %1").arg(auth2)); return false; } QByteArray passBytes = m_password.toUtf8(); sendCommand(passBytes.toBase64()); QString auth3 = recvLine(); // 应为 235 if (!auth3.startsWith("235")) { emit error(tr("AUTH LOGIN step 3 failed: %1").arg(auth3)); return false; } return true; }逻辑说明:
AUTH LOGIN是三步交互:服务器发334 VXNlcm5hbWU6("Username:" BASE64),客户端回用户名 BASE64;服务器再发334 UGFzc3dvcmQ6("Password:" BASE64),客户端回密码 BASE64;最后服务器发235成功。- 致命陷阱:
m_userName.toBase64()是错的!QString::toBase64()对 UTF-16 字符串编码,而 SMTP 要求原始字节流。必须toUtf8()转成QByteArray再toBase64()。某开发者用错导致中文邮箱(如张三@xx.com)永远认证失败,debug 三天才发现是编码层级错了。 recvLine()在此处必须严格匹配334,不能忽略空格或大小写——有些服务器返回334 VXNlcm5hbWU6(多空格),startsWith("334")仍成立。
参数说明:
m_userName/m_password:UI 输入后直接存 QString,但编码前必转toUtf8();- 若需支持 OAuth2(如 Gmail App Password 已淘汰,改用 OAuth),此处要替换为
AUTH XOAUTH2流程,需额外集成 Qt OAuth 模块,本项目未实现。
3.3 sendMailData():MIME 封装与附件边界防冲突
bool SmtpClient::sendMailData(const QString &from, const QStringList &to, const QString &subject, const QString &body, const QList<QPair<QString, QByteArray>> &attachments) { sendCommand(QString("MAIL FROM:<%1>").arg(from)); if (!checkResponse("250")) return false; for (const QString &rcpt : to) { sendCommand(QString("RCPT TO:<%1>").arg(rcpt)); if (!checkResponse("250")) return false; } sendCommand("DATA"); if (!checkResponse("354")) return false; // 构造 MIME 头部 QString mimeHeader = QString("From: %1\r\n" "To: %2\r\n" "Subject: =?UTF-8?B?%3?=\r\n" "MIME-Version: 1.0\r\n" "Content-Type: multipart/mixed; boundary=\"%4\"\r\n\r\n") .arg(from) .arg(to.join(", ")) .arg(QByteArray(subject.toUtf8()).toBase64()) .arg(m_boundary); sendRawData(mimeHeader.toUtf8()); // 发送正文(text/plain) QString textPart = QString("--%1\r\n" "Content-Type: text/plain; charset=UTF-8\r\n" "Content-Transfer-Encoding: base64\r\n\r\n" "%2\r\n\r\n") .arg(m_boundary) .arg(QByteArray(body.toUtf8()).toBase64()); sendRawData(textPart.toUtf8()); // 发送附件 for (const auto &att : attachments) { QString fileName = QFileInfo(att.first).fileName(); QString encodedFileName = QString("=?UTF-8?B?%1?=") .arg(QByteArray(fileName.toUtf8()).toBase64()); QString attPart = QString("--%1\r\n" "Content-Type: application/octet-stream; name=\"%2\"\r\n" "Content-Disposition: attachment; filename=\"%2\"\r\n" "Content-Transfer-Encoding: base64\r\n\r\n" "%3\r\n\r\n") .arg(m_boundary) .arg(encodedFileName) .arg(att.second.toBase64()); sendRawData(attPart.toUtf8()); } // 结束 boundary sendRawData(QString("--%1--\r\n").arg(m_boundary).toUtf8()); sendRawData("\r\n.\r\n"); // SMTP 结束标记 return checkResponse("250"); }逻辑说明:
MAIL FROM和RCPT TO必须在DATA前完成,且每个RCPT TO都要单独250响应;Subject用=?UTF-8?B?...?=编码是 RFC 2047 要求,否则中文主题显示为乱码;m_boundary是QUuid::createUuid().toString().mid(1, 36)生成的 36 位随机字符串,确保不会与邮件正文中的--boundary冲突;- 附件
filename=后必须用 RFC 2231 编码(即=?UTF-8?B?...?=),否则 Outlook 会把中文附件名显示为空白; - 最后的
.\r\n是 SMTP 协议硬性规定,少一个点或换行都会导致服务器挂起。
参数说明:
m_boundary:定义在SmtpClient.h中为QString m_boundary;,构造函数里初始化;sendRawData()是 SocketWrapper 方法,绕过sendCommand()的\r\n自动补全,用于发送原始二进制;att.second.toBase64():附件内容是QByteArray,直接 BASE64,无需toUtf8()(它已是字节流)。
避坑:附件名编码失效?
现象:中文附件名在 QQ 邮箱显示为=?UTF-8?B?...?=原文,未解码。
原因:QQ 邮箱要求filename*=形式(RFC 5987),而非filename=。
解决:将filename="%2"改为filename*="utf-8''%2",其中%2是 URL 编码后的文件名(用QUrl::toPercentEncoding(fileName))。本项目未实现,但留了扩展点。
4. 常见问题排查:5 条线上高频翻车记录,每条都带真实日志和修复命令
开发这类网络工具,80% 时间花在环境适配上。下面 5 条是某公司产线部署时真实发生的故障,按发生频率排序,每条给出现象 → 原因 → 解决,拒绝模糊描述。
4.1 现象:点击发送后 UI 卡住 10 秒,然后弹窗 “Connection timeout to smtp.qq.com:587”
原因:Linux 主机/etc/resolv.conf中 DNS 服务器不可达,QHostInfo::fromName()解析域名超时,而connectToHost()内部会先做 DNS 查询。Qt 默认不设 DNS 超时,卡满 10 秒(waitForConnected时限)。
解决:
- 方案 A(推荐):在
connectAndEhlo()前加 DNS 预解析QHostInfo info = QHostInfo::fromName(m_smtpServer); if (info.addresses().isEmpty()) { emit error(tr("DNS resolve failed for %1").arg(m_smtpServer)); return false; } m_socket->connectToHost(info.addresses().first(), m_port); // 直接连 IP - 方案 B:改系统 DNS,或在代码中硬编码 IP(如
smtp.qq.com→113.108.216.132),但需定期更新。
4.2 现象:认证通过(235),但发信失败,日志显示 “554 Message rejected for policy reasons”
原因:QQ 邮箱企业版强制要求From地址必须与登录账号完全一致,而 UI 中from输入框填了monitor@company.com,但登录用的是alert@company.com。SMTP 协议本身不限制,但邮件网关策略拦截。
解决:
- 在
sendMailData()开头加校验:if (from != m_userName && !m_userName.contains('@')) { emit error(tr("From address must match login account for QQ Mail")); return false; } - 或 UI 层锁定
from输入框为只读,值 =m_userName。
4.3 现象:附件打开后是乱码,或提示“文件损坏”
原因:附件二进制流被 QtQTextStream错误当作文本读取,导致\r\n被自动转换,破坏原始字节。常见于用QFile::readAll()后直接传给QByteArray,但文件打开模式错误。
解决:
- 读附件时必须用
QFile::ReadOnly | QFile::Unbuffered:QFile file(filePath); if (!file.open(QFile::ReadOnly | QFile::Unbuffered)) { emit error(tr("Cannot open attachment: %1").arg(filePath)); return false; } QByteArray data = file.readAll(); file.close(); Unbuffered防止 Qt 内部缓存干扰,ReadOnly确保二进制安全。
4.4 现象:Windows 上发送成功,Linux 上报 “SSL handshake failed”
原因:OpenSSL 版本不兼容。Windows 静态链接 1.1.1w,Linux 动态链接系统libssl.so.1.1,但某 CentOS 7 系统装的是openssl-libs-1.0.2k,libssl.so.1.0.2与 1.1.1 ABI 不兼容。
解决:
- 方案 A(治本):Linux 编译时静态链接 OpenSSL,CMakeLists.txt 加:
find_package(OpenSSL REQUIRED) target_link_libraries(mailtool PRIVATE ${OPENSSL_LIBRARIES}) set_target_properties(mailtool PROPERTIES LINK_FLAGS "-static-libssl -static-libcrypto") - 方案 B(应急):在目标机安装
openssl11-libs包,并设LD_LIBRARY_PATH=/usr/lib64/openssl11。
4.5 现象:UI 中输入带空格的收件人(如 “张三 zhang@xx.com ”),发信后对方收不到
原因:SMTPRCPT TO命令只接受<zhang@xx.com>格式,不能带姓名。UI 层未做地址清洗,直接拼进命令:RCPT TO:<张三 <zhang@xx.com>>,服务器解析失败。
解决:
- 在
sendMailData()中用正则提取邮箱:QRegularExpression re("<([^>]+)>"); for (const QString &rcpt : to) { QRegularExpressionMatch match = re.match(rcpt); QString email = match.hasMatch() ? match.captured(1) : rcpt; sendCommand(QString("RCPT TO:<%1>").arg(email)); } - 正则
"<([^>]+)>"安全提取尖括号内内容,兼容"zhang@xx.com"和"张三 <zhang@xx.com>"。
5. 二次开发实战:从支持 CC/BCC 到对接阿里云邮件推送 API 的三步改造
现在你已经跑通了基础发送,下一步往往是业务集成。我以某高校实验室的实际需求为例:他们需要把传感器报警邮件同时发给 3 个老师(CC),且因学校防火墙封了 25/465/587 端口,必须切到阿里云邮件推送(HTTP API)。下面给出可立即落地的三步改造,每步附完整代码和验证方法。
5.1 第一步:UI 层增加 CC/BCC 输入框(5 分钟)
mainwindow.ui中拖入两个QLineEdit,objectName 设为leCc和leBcc。在mainwindow.cpp的构造函数中连接信号:
// MainWindow 构造函数末尾 connect(ui->btnSend, &QPushButton::clicked, this, &MainWindow::onSendClicked); // 新增:CC/BCC 输入框内容变更时,同步更新发送参数 connect(ui->leCc, &QLineEdit::textChanged, this, &MainWindow::updateRecipientList); connect(ui->leBcc, &QLineEdit::textChanged, this, &MainWindow::updateRecipientList);再加一个私有槽函数updateRecipientList():
void MainWindow::updateRecipientList() { // 解析 CC 字符串为 QStringList,支持逗号/分号/空格分隔 QStringList ccList = parseEmails(ui->leCc->text()); QStringList bccList = parseEmails(ui->leBcc->text()); // 存入成员变量,供 sendMail() 调用 m_ccRecipients = ccList; m_bccRecipients = bccList; } QStringList MainWindow::parseEmails(const QString &input) { if (input.trimmed().isEmpty()) return {}; // 用正则分割,过滤空项 QRegularExpression re("[,;\\s]+"); QStringList list = input.split(re, Qt::SkipEmptyParts); for (QString &s : list) s = s.trimmed(); return list; }验证方法:
- 输入
teacher1@univ.edu, teacher2@univ.edu到 CC 框,点击发送; - 抓包看 SMTP 流量,确认多出
RCPT TO:<teacher1@univ.edu>和RCPT TO:<teacher2@univ.edu>; - 不用改
SmtpClient,因为sendMailData()的to参数已扩展为m_toRecipients + m_ccRecipients + m_bccRecipients。
5.2 第二步:协议层支持 BCC(隐藏收件人),不发 RCPT TO
BCC 的本质是:收件人地址只出现在To:/Cc:MIME 头部,不出现在 SMTPRCPT TO命令中。所以只需改sendMailData():
// 原来:for (const QString &rcpt : to) { sendCommand("RCPT TO:<%1>"); } // 改为: QList<QString> allRcpts = m_toRecipients + m_ccRecipients; // BCC 不进 RCPT for (const QString &rcpt : allRcpts) { sendCommand(QString("RCPT TO:<%1>").arg(rcpt)); if (!checkResponse("250")) return false; } // 但在 MIME 头部中加入 BCC QString mimeHeader = QString("From: %1\r\n" "To: %2\r\n" "Cc: %3\r\n" "Bcc: %4\r\n" // 这行是新增的 "Subject: =?UTF-8?B?%5?=\r\n" "MIME-Version: 1.0\r\n" "Content-Type: multipart/mixed; boundary=\"%6\"\r\n\r\n") .arg(from) .arg(m_toRecipients.join(", ")) .arg(m_ccRecipients.join(", ")) .arg(m_bccRecipients.join(", ")) // BCC 只在此处出现 .arg(QByteArray(subject.toUtf8()).toBase64()) .arg(m_boundary);注意:Bcc:头部在最终邮件中不会显示给任何收件人,这是 SMTP 协议保证的。验证时用 Wireshark 抓 SMTP 流量,确认无RCPT TO:<bcc@xx.com>,但收件人邮箱里能看到To:和Cc:,看不到Bcc:。
5.3 第三步:对接阿里云邮件推送(HTTP API),彻底绕过 SMTP
阿里云邮件推送不开放 SMTP 端口,只提供 HTTPS REST API。此时SmtpClient整个模块废弃,需新增AliyunMailApi类。核心改动三点:
- 新建
aliyunmailapi.h/cpp,继承自QObject,用QNetworkAccessManager发 POST; - 在
MailSender中加运行时开关:if (m_mode == MODE_ALIYUN) { useAliyunApi(); } else { useSmtpClient(); }; - UI 增加模式选择下拉框,值存
QSettings持久化。
AliyunMailApi::send()关键代码:
QNetworkRequest request(QUrl("https://dm.aliyuncs.com/")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); // 阿里云签名算法(简化版,实际需按文档加 timestamp/signature) QUrlQuery params; params.addQueryItem("Action", "SingleSendMail"); params.addQueryItem("AccountName", "no-reply@yourdomain.com"); params.addQueryItem("FromAlias", "系统通知"); params.addQueryItem("AddressType", "1"); params.addQueryItem("ToAddress", to.join(",")); params.addQueryItem("Subject", subject); params.addQueryItem("HtmlBody", body); // 支持 HTML params.addQueryItem("Format", "JSON"); QNetworkReply *reply = m_nam->post(request, params.query().toUtf8()); connect(reply, &QNetworkReply::finished, [=]() { if (reply->error() == QNetworkReply::NoError) { QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); if (doc.object()["Code"] == "OK") { emit success(); } else { emit error(doc.object()["Message"].toString()); } } else { emit error(reply->errorString()); } reply->deleteLater(); });验证方法:
- 在 UI 切到 “阿里云模式”,填好 AccessKeyId/Secret(存
QSettings加密); - 发送后检查阿里云控制台“发送记录”,确认状态为“成功”;
- 收件箱查邮件,确认 HTML 正文渲染正常(阿里云支持
<b>标签)。
从 SMTP 到 HTTP 的切换,本质是协议抽象层升级。我一般会在MailSender中定义纯虚函数virtual bool send(const MailData &data) = 0;,然后让SmtpSender和AliyunSender分别继承——这样未来加 SendGrid、Mailgun,只需新增一个子类,UI 和业务层零修改。从那以后我每次新增邮件渠道,都强制走一遍这个抽象流程,哪怕就一个渠道也先写接口。希望帮到你。
本文还有配套的精品资源,点击获取