使用libcurl库实现C++程序发送邮件的功能

目录

1、为啥要选择libcurl库去实现邮件的发送

2、调用libcurl库的API接口实现邮件发送

3、构造待发送的邮件内容

4、开通163发送邮件账号的SMTP服务

5、排查接收的邮件内容为空的问题


       libcurl是一个免费开源的网络传输库,支持ftp、ftps、tftp,http、https、telnet、ldap、pop3、smtp等多种协议。libcurl中封装了支持这些协议的网络通信模块,支持跨平台,支持Windows,Unix,Linux等多个操作系统。libcurl提供了一套统一样式的API接口,我们不用关注各种协议下网络通信的实现细节,只需要调用这些API就能轻松地实现基于这些协议的数据通信。本文将简单地讲述一下使用libcurl实现邮件发送的相关细节。

1、为啥要选择libcurl库去实现邮件的发送

使用libcurl库实现C++程序发送邮件的功能

       如果我们自己去使用socket套接字去编码,实现连接smtp邮件服务器,并完成和服务器的smtp协议的交互,整个过程走下来会非常地复杂,特别是要处理网络通信过程中的多种异常,整个流程的稳定性和健壮性没有保证。

       而libcurl中已经实现了smtp协议的所有流程,我们不需要去关注协议的具体实现细节,我们只要去调用libcurl的API接口就能实现发送邮件的功能。libcurl库的稳定性是毋庸置疑的。

       我们可以到官网上下载libcurl开源库最新的源码,直接使用Visual Studio编译出要用的dll库,至于使用Visual Studio如何编译libcurl代码,后面我会写一篇文章去详细介绍。

2、调用libcurl库的API接口实现邮件发送

       先调用curl_easy_init接口初始化libcurl库,然后调用curl_easy_setopt(使用CURLOPT_URL选项)设置url请求地址,正是通过该url的前缀确定具体使用哪种协议。比如本例中发送邮件时需要使用smtp协议:

char urlBuf[256] = { 0 };
sprintf( urlBuf, "smtp://%s:%s", m_strServerName.c_str(), m_strPort.c_str() );
curl_easy_setopt(curl, CURLOPT_URL, urlBuf); 

设置url时使用的就是smtp前缀,然后带上目标服务器的IP和端口。

       在使用相关协议完成数据交互时,可能还要设置一些其他的信息,比如用户名和密码等,都是通过调用curl_easy_setopt设置的:

curl_easy_setopt(curl, CURLOPT_USERNAME, m_strUserName.c_str());  
curl_easy_setopt(curl, CURLOPT_PASSWORD, m_strPassword.c_str()); 

要发送的数据,则通过CURLOPT_READDATA选项去设置:

std::stringstream stream;  
stream.str(m_strMessage.c_str());  
stream.flush();
  
/* We're using a callback function to specify the payload (the headers and 
* body of the message). You could just use the CURLOPT_READDATA option to 
* specify a FILE pointer to read from. */  
curl_easy_setopt(curl, CURLOPT_READFUNCTION, &CSmtpSendMail::payload_source);  
curl_easy_setopt(curl, CURLOPT_READDATA, (void *)&stream);  
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);  

       最后调用curl_easy_perform或者curl_multi_perform接口发起请求,该接口内部将去连接url中指定的服务器,并完成指定的协议协商与交互,并最终完成与服务器之间的数据通信。

       调用libcurl库发送邮件的完整代码如下所示:

CURLcode CSmtpSendMail::SendMail()  
{  
	CreatMessage();  
	bool ret = true;  
	CURL *curl;  
	CURLcode res = CURLE_OK;  
	struct curl_slist *recipients = NULL;  

	curl = curl_easy_init();  
	if (curl) {  
		/* Set username and password */                                        
		curl_easy_setopt(curl, CURLOPT_USERNAME, m_strUserName.c_str());  
		curl_easy_setopt(curl, CURLOPT_PASSWORD, m_strPassword.c_str());  

		char urlBuf[256] = { 0 };
		sprintf( urlBuf, "smtp://%s:%s", m_strServerName.c_str(), m_strPort.c_str() );
		curl_easy_setopt(curl, CURLOPT_URL, urlBuf);  
		/* If you want to connect to a site who isn't using a certificate that is 
		* signed by one of the certs in the CA bundle you have, you can skip the 
		* verification of the server's certificate. This makes the connection 
		* A LOT LESS SECURE. 
		* 
		* If you have a CA cert for the server stored someplace else than in the 
		* default bundle, then the CURLOPT_CAPATH option might come handy for 
		* you. */  
#ifdef SKIP_PEER_VERIFICATION  
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);  
#endif  

		/* If the site you're connecting to uses a different host name that what 
		* they have mentioned in their server certificate's commonName (or 
		* subjectAltName) fields, libcurl will refuse to connect. You can skip 
		* this check, but this will make the connection less secure. */  
#ifdef SKIP_HOSTNAME_VERIFICATION  
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);  
#endif  

		/* Note that this option isn't strictly required, omitting it will result 
		* in libcurl sending the MAIL FROM command with empty sender data. All 
		* autoresponses should have an empty reverse-path, and should be directed 
		* to the address in the reverse-path which triggered them. Otherwise, 
		* they could cause an endless loop. See RFC 5321 Section 4.5.5 for more 
		* details. 
		*/  
		//curl_easy_setopt(curl, CURLOPT_MAIL_FROM, FROM);   
		curl_easy_setopt(curl, CURLOPT_MAIL_FROM, m_strSendMail.c_str());  
		/* Add two recipients, in this particular case they correspond to the 
		* To: and Cc: addressees in the header, but they could be any kind of 
		* recipient. */  
		for (size_t i = 0; i < m_vRecvMail.size(); i++) {  

			recipients = curl_slist_append(recipients, m_vRecvMail[i].c_str());  
		}  
		curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);  

		std::stringstream stream;  
		stream.str(m_strMessage.c_str());  
		stream.flush();  
		/* We're using a callback function to specify the payload (the headers and 
		* body of the message). You could just use the CURLOPT_READDATA option to 
		* specify a FILE pointer to read from. */  

		// 注意回调函数必须设置为static
		curl_easy_setopt(curl, CURLOPT_READFUNCTION, &CSmtpSendMail::payload_source);  
		curl_easy_setopt(curl, CURLOPT_READDATA, (void *)&stream);  
		curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);  

		/* Since the traffic will be encrypted, it is very useful to turn on debug 
		* information within libcurl to see what is happening during the 
		* transfer */  
		curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);  

		curl_multi_perform()
		/* Send the message */  
		res = curl_easy_perform(curl);  
		CURLINFO info = CURLINFO_NONE;  
		curl_easy_getinfo(curl, info);  
		/* Check for errors */  

		if (res != CURLE_OK) {  
			fprintf(stderr, "curl_easy_perform() failed: %s\n\n",  
				curl_easy_strerror(res));  

			char achErrInfo[512] = {0};
			sprintf( achErrInfo, "curl_easy_perform() failed, error info: %s\n\n", curl_easy_strerror(res) );
			::MessageBoxA( NULL, achErrInfo, "Tip", MB_OK);
			ret = false;  

			m_strErrDesription = achErrInfo;

			/*				Sleep( 100 );
			res = curl_easy_perform(curl); */ 
		}  
		else
		{
			m_strErrDesription = "";
		}

		/* Free the list of recipients */  
		curl_slist_free_all(recipients);  

		/* Always cleanup */  
		curl_easy_cleanup(curl);  
	}  
	else
	{
		res = CURLE_FAILED_INIT;
		char achErrInfo[512] = {0};
		sprintf( achErrInfo, "curl_easy_init() failed, error info: %s\n\n", curl_easy_strerror(res) );
		m_strErrDesription = achErrInfo;
    }
	return res;  
}  

3、构造待发送的邮件内容

        libcurl负责和smtp邮件服务器建链,完成smtp简单邮件协议的协商与交互,但要发送的邮件内容则需要我们自己去根据协议的规范去构建。那邮件发送的内容的数据格式到底是什么样子的呢?其实很简单,找一个支持发送邮件的软件,发送邮件时抓一下包,就能抓出对应的格式,比如:

使用libcurl库实现C++程序发送邮件的功能

按照上面的格式构建就可以了,相关代码如下:

void CSmtpSendMail::CreatMessage()  
{  
	//m_strMessage = "Date: 13 Nov 2021 12:52:14 +0800";
	m_strMessage = "From: ";  
	m_strMessage += m_strSendMail;  
	m_strMessage += "\r\nReply-To: ";  
	m_strMessage += m_strSendMail;  
	m_strMessage += "\r\nTo: ";  
	for (size_t i = 0; i < m_vRecvMail.size(); i++)  
	{  
		if (i > 0) {  
			m_strMessage += ",";  
		}  
		m_strMessage += m_vRecvMail[i];  
	}  
	m_strMessage += "\r\n";  
	m_strMessage += m_strSubject;  
	m_strMessage += "\r\nX-Mailer: The Bat! (v3.02) Professional";  
	m_strMessage += "\r\nMime-Version: 1.0";  
	m_strMessage += "\r\nContent-Type: multipart/mixed;";  
	m_strMessage += "boundary=\"simple boundary\"";  //__MESSAGE__ID__54yg6f6h6y456345
	//m_strMessage += "\r\nThis is a multi-part message in MIME format.";  
	m_strMessage += "\r\n\r\n--simple boundary";  
	//正文  
	m_strMessage += "\r\nContent-Type: text/html;";  
	m_strMessage += "charset=";  
	//m_strMessage += "\"";  
	m_strMessage += m_strCharset;  
	//m_strMessage += "\"";  
	m_strMessage += "\r\nContent-Transfer-Encoding: 7bit";  
	m_strMessage += "\r\n";  
	m_strMessage += m_strContent;  

	//附件  
	std::string filename = "";  
	std::string filetype = "";  
	for (size_t i = 0; i < m_vAttachMent.size(); i++)  
	{  
		m_strMessage += "\r\n--simple boundary";  
		GetFileName(m_vAttachMent[i], filename);  
		GetFileType(m_vAttachMent[i], filetype);  
		SetContentType(filetype);  
		SetFileName(filename);  

		m_strMessage += "\r\nContent-Type: ";  
		m_strMessage += m_strContentType;  
		m_strMessage += "\tname=";  
		m_strMessage += "\"";  
		m_strMessage += m_strFileName;  
		m_strMessage += "\"";  
		m_strMessage += "\r\nContent-Disposition:attachment;filename=";  
		m_strMessage += "\"";  
		m_strMessage += m_strFileName;  
		m_strMessage += "\"";  
		m_strMessage += "\r\nContent-Transfer-Encoding:base64";   
		m_strMessage += "\r\n\r\n";  


		FILE *pt = NULL;  
		if ((pt = fopen(m_vAttachMent[i].c_str(), "rb")) == NULL) {  

			std::cerr << "打开文件失败: " << m_vAttachMent[i] <<std::endl;  
			continue;  
		}  
		fseek(pt, 0, SEEK_END);  
		int len = ftell(pt);  
		fseek(pt, 0, SEEK_SET);  
		int rlen = 0;  
		char buf[55];  
		for (size_t i = 0; i < len / 54 + 1; i++)  
		{  
			memset(buf, 0, 55);  
			rlen = fread(buf, sizeof(char), 54, pt);  
			m_strMessage += base64_encode((const unsigned char*)buf, rlen);  
			m_strMessage += "\r\n";  
		}  

		fclose(pt);  
		pt = NULL;  
	}  

	m_strMessage += "\r\n--simple boundary--\r\n";  
}  

4、开通163发送邮件账号的SMTP服务

       上述代码处理好后,运行如下的测试程序:

使用libcurl库实现C++程序发送邮件的功能

在上述界面中输入163的smtp服务器地址,使用默认的25端口,并填写发送邮件地址和发送邮件的密码,点击“发送测试邮件”按钮,结果邮件并没有发送成功。

       在代码中添加断点调试,发现curl_easy_perform接口返回的错误码为CURLE_LOGIN_DENIED,如下所示:

使用libcurl库实现C++程序发送邮件的功能

于是通过CURLE_OK go到错误码定义的头文件中,去查看CURLE_LOGIN_DENIED错误码的含义:

使用libcurl库实现C++程序发送邮件的功能

注释中提示可能是发送邮件的用户名或密码错误引起的。用户名和密码填写的应该没问题啊?于是账号到网页上登陆一下163邮箱,可以成功登陆的,说明账号和密码是没问题的。那到底是咋回事呢?

       后来想到,是不是要到发送邮件账号中去开启一下smtp服务才可以登陆到163的smtp服务器上?于是到网页上登陆,按下列的操作步骤找到开启当前账号的smtp服务入口:

使用libcurl库实现C++程序发送邮件的功能

使用libcurl库实现C++程序发送邮件的功能

使用libcurl库实现C++程序发送邮件的功能

点击开启按钮,会弹出如下的提示框:

使用libcurl库实现C++程序发送邮件的功能

点击继续开启,进入下面的页面:

使用libcurl库实现C++程序发送邮件的功能

提示需要扫码发送短信进行验证。于是使用网易邮件大师APP扫描了一下,自动跳转到发送短信的页面,发送验证短信即可。最后弹出如下的授权密码页面:

使用libcurl库实现C++程序发送邮件的功能

要将这个授权密码记录下来,登陆smtp服务器时需要使用这个授权密码,而不是账号的密码!

       于是在测试页面中输入授权码,邮件就能发送成功了。

5、排查接收的邮件内容为空的问题

         邮件是能正常发送出去了,邮件也能正常接收到,但接收到的邮件内容是空的:

使用libcurl库实现C++程序发送邮件的功能

这是啥情况?明明发送邮件时有设置邮件内容的,为啥收到的邮件内容是空的呢?

       上述代码在几年前测试过,好像没问题的,难道163邮箱系统升级了,不再兼容老的数据格式了?于是想到了海康的视频监控客户端,该客户端可以到海康官网上下载,免费使用,其中系统设置中有个发送邮件的功能:

使用libcurl库实现C++程序发送邮件的功能

海康的上述界面中发送测试邮件是没问题的,接收到的邮件也是有内容的。于是赶紧抓一下海康发送邮件的数据包,以tcp.port==25过滤了一下,抓出海康发出去的邮件内容:

使用libcurl库实现C++程序发送邮件的功能

又抓取了一下我们软件发出去的邮件内容如下:

使用libcurl库实现C++程序发送邮件的功能

       于是详细地对比了海康与我们发出去的数据内容,多次尝试修改我们构建邮件数据的代码,比如charset编码格式、boundry类型等,甚至是否会空行。最后经过多次尝试找到了原因,是在具体的邮件内容上面需要人为加上一个空行,我们代码在构造邮件数据时没有加空行,导致接收到的邮件内容是空的!

上一篇:chrome80默认SameSite导致iframe无法获取cookie问题解决方法


下一篇:实名校验和微信实名信息是否一致(PHP对接小程序城市服务实名信息校验接口)