带 SSL/TLS 的 SMTP 客户端






4.93/5 (131投票s)
C++ SMTP 客户端,支持 SSL 和 TLS 加密连接到 SMTP 服务器
引言
我需要在用 C++ 编写的产品中发送电子邮件,所以我搜索了互联网,并找到了一篇很棒的文章:《SMTP 客户端》,由 Jakub Piwowarczyk 撰写。然而,我的许多客户使用的 SMTP 服务器需要安全连接(TLS 或 SSL),而 SMTP 客户端不支持。因此,我必须为《SMTP 客户端》中的 CSmtp
类添加 SSL/TLS 支持,然后才能在我的产品中使用它。由于我对 SSL/OpenSSL 是新手,花了我不少时间才学会如何正确使用它,并使代码能够与几个流行的 SMTP 服务器协同工作。我还看到人们在互联网上搜索 C++ 实现的 SMTP/SSL/TLS,但就是找不到。所以我决定分享我写的这个,希望它能为不熟悉 SSL 的人们节省一些时间。
请注意,本文不涉及 SMTP 的详细信息。如果您需要了解更多关于 SMTP 的信息,请参阅原文《SMTP 客户端》。
背景
SMTP 有两种安全连接方式,一种是 SSL,另一种是 TLS。有些 SMTP 服务器只支持一种,有些则两种都支持。总的来说,SSL 的端口通常是 465,TLS 的端口通常是 587,但这并非总是如此。除了端口不同之外,SMTP/SSL 与 SMTP/TLS 的区别在于,SMTP/SSL 在底层 TCP 连接建立后立即协商加密连接,而 SMTP/TLS 要求客户端在协商加密连接之前向服务器发送 STARTLS
命令。
SMTP/SSL 所涉及的步骤如下:
- 客户端通过 TCP 连接到服务器。
- 客户端与服务器协商加密连接。
- 服务器通过加密连接向客户端发送欢迎消息。
- 客户端通过加密连接向服务器发送 EHLO 命令。
- 服务器通过加密连接响应 EHLO 命令。
SMTP/TLS 所涉及的步骤如下:
- 客户端通过 TCP 连接到服务器。
- 服务器通过未加密的连接向客户端发送欢迎消息。
- 客户端通过未加密的连接向服务器发送 EHLO 命令。
- 服务器通过未加密的连接响应 EHLO 命令。
- 客户端通过未加密的连接向服务器发送
STARTTLS
命令。 - 服务器通过未加密的连接响应
STARTTLS
命令。 - 客户端与服务器协商加密连接。
- 客户端通过加密连接向服务器发送 EHLO 命令。
- 服务器通过加密连接响应 EHLO 命令。
Using the Code
我在示例代码中使用了 openssl(http://www.openssl.org)。示例代码中的 "openssl-0.9.8l" 目录包含所有必需的头文件和两个预编译的 静态 openssl
库。如果您也想在您的代码中使用此版本的 openssl
,请务必将整个 "openssl-0.9.8l" 目录复制到您的项目根目录,并将 "openssl-0.9.8l\inc32" 添加到 "其他包含目录",还将 "openssl-0.9.8l\out32" 添加到 "其他库目录"。
如果您想自己构建 openssl
,请参考 http://www.openssl.org 获取详细说明。
#define test_gmail_tls
CSmtp mail;
#if defined(test_gmail_tls)
mail.SetSMTPServer("smtp.gmail.com",587);
mail.SetSecurityType(USE_TLS);
#elif defined(test_gmail_ssl)
mail.SetSMTPServer("smtp.gmail.com",465);
mail.SetSecurityType(USE_SSL);
#elif defined(test_hotmail_TLS)
mail.SetSMTPServer("smtp.live.com",25);
mail.SetSecurityType(USE_TLS);
#elif defined(test_aol_tls)
mail.SetSMTPServer("smtp.aol.com",587);
mail.SetSecurityType(USE_TLS);
#elif defined(test_yahoo_ssl)
mail.SetSMTPServer("plus.smtp.mail.yahoo.com",465);
mail.SetSecurityType(USE_SSL);
#endif
mail.SetLogin("***");
mail.SetPassword("***");
mail.SetSenderName("User");
// ......
mail.Send();
如果您使用非特权用户帐户测试 Yahoo,邮件将发送失败。Yahoo SMTP 服务器返回的错误消息是 "530 Access denied: Free users cannot access this server"(530 访问被拒绝:免费用户无法访问此服务器)。
注释
- 代码不验证服务器身份,也就是说,它不检查服务器证书。如果我们确保向程序提供正确的服务器地址,这通常不是一个大问题。然而,如果我们在不检查证书的情况下,仍然有可能与冒充者通信,这一点值得一提。
- 不允许使用本文中的代码进行垃圾邮件发送。
参考文献
- SMTP 客户端,作者 Jakub Piwowarczyk
- http://www.openssl.org
- 《OpenSSL 编程入门》,作者 Eric Rescorla,链接
历史
- 版本 2.4,2015/10/22
感谢大家有效的众包!请继续改进我们的库!
感谢大家有效的众包!请继续改进我们的库!
- 根据 o15s19 贡献的修复,从邮件头中移除了 Bcc。
- 根据 uni_gauldoth 贡献的修复,增加了处理文件名中包含保留字符的附件的功能。
- 根据 GKarRacer 贡献的改进,改进了检查附件文件大小的方法。
- 根据 jim fred 贡献,为 Linux 编译添加了
#include <unistd.h>
。
- 根据 GKarRacer 贡献的修复,修复了在处理消息正文的某一行之前,对
MsgBody.size()
的范围检查不正确的问题。- 将内存分配和检查所有附件是否可以打开的操作移到了
- 将所有
sprintf
命令更改为snprintf
以增加安全性。为 MS Visual C 上的 snprintf 宏定义为sprintf_s
。由于 MS Visual C 版的strcpy_s
函数参数顺序不同,无法在不影响移植性的情况下使用它,因此还更改了大多数strcpy
调用为snprintf
。- 根据 jcyangzh 贡献的修复,修复了
SayQuit
函数中可能出现的无限循环问题。- 根据 sbrytskyy 贡献的修复,添加了使
AUTH PLAIN
登录正常工作所需的修复。
- 版本 2.1,2012/11/06
- 根据 Alan P Brown 贡献的修复,修复了对
SMTP_SECURITY_TYPE
枚举的无效引用。 - 根据 Oleg Dolgov 贡献的修改,添加了允许在 Debian Linux 中编译的功能。
- 根据 Leon Huang 和 John Tang 启发的功能,添加了通过调用新的成员函数
SetCharSet()
将字符集从默认的 US-ASCII 更改为您喜欢的任何字符集的功能。 - 根据 Gospa 启发的功能,添加了通过调用新的成员函数
SetReadReceipt()
来请求已读回执的功能。 - 根据 Spike 启发的功能,添加了区分附件路径中 Linux 和 Windows 路径分隔符的功能。
- 根据 Spike 启发的功能,添加了使用更简单、更可移植的方法设置
std::string
变量的功能。 - 根据 Jerko 贡献的功能,添加了通过调用新的
SetLocalHostName()
成员函数来设置本地主机名的功能。如果您不调用此函数,它将与以前一样工作。 - 根据 Korisk 贡献的功能,添加了使其在 Linux 中编译更干净的功能。
- 根据 Angenua Grupoi 贡献的修复,修复了当
m_sNameFrom
变量未填充时的行为。
- 根据 Alan P Brown 贡献的修复,修复了对
- 版本 2.0,2011/06/23
- 添加了
m_bAuthenticate
成员变量,以便能够禁用身份验证,即使服务器可能支持它。它默认为true
,因此如果未设置,库将按以前的方式工作。 - 将安全类型
m_type
、新的m_Authenticate
标志、登录名和密码添加到ConnectRemoteServer
函数中。如果调用中不包含这些新参数,函数将按以前的方式工作。 - 将新的
m_Authenticate
标志添加到SetSMTPServer
函数中。如果未提供,函数将按以前的方式工作。 - 根据 Martin Kjallman 贡献的修复。
- 根据 Karpov Andrey 贡献的修复。
- 根据 Jakub Piwowarczyk 贡献的修复。
- 添加了
- 版本 1.9,2010/08/19
- 添加了 PLAIN、CRAM-MD5 和 DIGESTMD5 授权。
- 添加了
DisconnectRemoteServer()
函数,并重新配置了Send()
函数,使得如果您已经调用了ConnectRemoveServer()
函数,它将使用现有连接并保持连接打开。这允许您调用ConnectRemoteServer()
,然后在同一连接上发送多条消息。如果使用此方法,您必须调用DisconnectRemoteServer()
来关闭连接。如果您在未调用ConnectRemoteServer()
的情况下调用Send()
,它将在发送后关闭连接。此更改应完全向后兼容。
- 版本 1.8,2010/08/09
- 根据 Hector Santos 的评论 进行更新。
- 2010/08/03
- 修改了引言。
- 2010/08/02
- 添加了注释。
- 2010/08/01
- 初次发布