C++調(diào)用libcurl開(kāi)源庫(kù)實(shí)現(xiàn)郵件的發(fā)送功能流程詳解
libcurl中封裝了支持這些協(xié)議的網(wǎng)絡(luò)通信模塊,支持跨平臺(tái),支持Windows,Unix,Linux等多個(gè)操作系統(tǒng)。libcurl提供了一套統(tǒng)一樣式的API接口,我們不用關(guān)注各種協(xié)議下網(wǎng)絡(luò)通信的實(shí)現(xiàn)細(xì)節(jié),只需要調(diào)用這些API就能輕松地實(shí)現(xiàn)基于這些協(xié)議的數(shù)據(jù)通信。本文將簡(jiǎn)單地講述一下使用libcurl實(shí)現(xiàn)郵件發(fā)送的相關(guān)細(xì)節(jié)。
1、為啥要選擇libcurl庫(kù)去實(shí)現(xiàn)郵件的發(fā)送

如果我們自己去使用socket套接字去編碼,實(shí)現(xiàn)連接smtp郵件服務(wù)器,并完成和服務(wù)器的smtp協(xié)議的交互,整個(gè)過(guò)程走下來(lái)會(huì)非常地復(fù)雜,特別是要處理網(wǎng)絡(luò)通信過(guò)程中的多種異常,整個(gè)流程的穩(wěn)定性和健壯性沒(méi)有保證。
而libcurl中已經(jīng)實(shí)現(xiàn)了smtp協(xié)議的所有流程,我們不需要去關(guān)注協(xié)議的具體實(shí)現(xiàn)細(xì)節(jié),我們只要去調(diào)用libcurl的API接口就能實(shí)現(xiàn)發(fā)送郵件的功能。libcurl庫(kù)的穩(wěn)定性是毋庸置疑的。
我們可以到官網(wǎng)上下載libcurl開(kāi)源庫(kù)最新的源碼,直接使用Visual Studio編譯出要用的dll庫(kù),至于使用Visual Studio如何編譯libcurl代碼,后面我會(huì)寫一篇文章去詳細(xì)介紹。
2、調(diào)用libcurl庫(kù)的API接口實(shí)現(xiàn)郵件發(fā)送
先調(diào)用curl_easy_init接口初始化libcurl庫(kù),然后調(diào)用curl_easy_setopt(使用CURLOPT_URL選項(xiàng))設(shè)置url請(qǐng)求地址,正是通過(guò)該url的前綴確定具體使用哪種協(xié)議。比如本例中發(fā)送郵件時(shí)需要使用smtp協(xié)議:
char urlBuf[256] = { 0 };
sprintf( urlBuf, "smtp://%s:%s", m_strServerName.c_str(), m_strPort.c_str() );
curl_easy_setopt(curl, CURLOPT_URL, urlBuf);
設(shè)置url時(shí)使用的就是smtp前綴,然后帶上目標(biāo)服務(wù)器的IP和端口。
在使用相關(guān)協(xié)議完成數(shù)據(jù)交互時(shí),可能還要設(shè)置一些其他的信息,比如用戶名和密碼等,都是通過(guò)調(diào)用curl_easy_setopt設(shè)置的:
curl_easy_setopt(curl, CURLOPT_USERNAME, m_strUserName.c_str()); curl_easy_setopt(curl, CURLOPT_PASSWORD, m_strPassword.c_str());
要發(fā)送的數(shù)據(jù),則通過(guò)CURLOPT_READDATA選項(xiàng)去設(shè)置:
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);
最后調(diào)用curl_easy_perform或者curl_multi_perform接口發(fā)起請(qǐng)求,該接口內(nèi)部將去連接url中指定的服務(wù)器,并完成指定的協(xié)議協(xié)商與交互,并最終完成與服務(wù)器之間的數(shù)據(jù)通信。
調(diào)用libcurl庫(kù)發(fā)送郵件的完整代碼如下所示:
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. */
// 注意回調(diào)函數(shù)必須設(shè)置為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、構(gòu)造待發(fā)送的郵件內(nèi)容
libcurl負(fù)責(zé)和smtp郵件服務(wù)器建鏈,完成smtp簡(jiǎn)單郵件協(xié)議的協(xié)商與交互,但要發(fā)送的郵件內(nèi)容則需要我們自己去根據(jù)協(xié)議的規(guī)范去構(gòu)建。那郵件發(fā)送的內(nèi)容的數(shù)據(jù)格式到底是什么樣子的呢?其實(shí)很簡(jiǎn)單,找一個(gè)支持發(fā)送郵件的軟件,發(fā)送郵件時(shí)抓一下包,就能抓出對(duì)應(yīng)的格式,比如:

按照上面的格式構(gòu)建就可以了,相關(guān)代碼如下:
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 << "打開(kāi)文件失敗: " << 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、開(kāi)通163發(fā)送郵件賬號(hào)的SMTP服務(wù)
上述代碼處理好后,運(yùn)行如下的測(cè)試程序:

在上述界面中輸入163的smtp服務(wù)器地址,使用默認(rèn)的25端口,并填寫發(fā)送郵件地址和發(fā)送郵件的密碼,點(diǎn)擊“發(fā)送測(cè)試郵件”按鈕,結(jié)果郵件并沒(méi)有發(fā)送成功。
在代碼中添加斷點(diǎn)調(diào)試,發(fā)現(xiàn)curl_easy_perform接口返回的錯(cuò)誤碼為CURLE_LOGIN_DENIED,如下所示:

于是通過(guò)CURLE_OK go到錯(cuò)誤碼定義的頭文件中,去查看CURLE_LOGIN_DENIED錯(cuò)誤碼的含義:

注釋中提示可能是發(fā)送郵件的用戶名或密碼錯(cuò)誤引起的。用戶名和密碼填寫的應(yīng)該沒(méi)問(wèn)題???于是賬號(hào)到網(wǎng)頁(yè)上登陸一下163郵箱,可以成功登陸的,說(shuō)明賬號(hào)和密碼是沒(méi)問(wèn)題的。那到底是咋回事呢?
后來(lái)想到,是不是要到發(fā)送郵件賬號(hào)中去開(kāi)啟一下smtp服務(wù)才可以登陸到163的smtp服務(wù)器上?于是到網(wǎng)頁(yè)上登陸,按下列的操作步驟找到開(kāi)啟當(dāng)前賬號(hào)的smtp服務(wù)入口:



點(diǎn)擊開(kāi)啟按鈕,會(huì)彈出如下的提示框:

點(diǎn)擊繼續(xù)開(kāi)啟,進(jìn)入下面的頁(yè)面:

提示需要掃碼發(fā)送短信進(jìn)行驗(yàn)證。于是使用網(wǎng)易郵件大師APP掃描了一下,自動(dòng)跳轉(zhuǎn)到發(fā)送短信的頁(yè)面,發(fā)送驗(yàn)證短信即可。最后彈出如下的授權(quán)密碼頁(yè)面:

要將這個(gè)授權(quán)密碼記錄下來(lái),登陸smtp服務(wù)器時(shí)需要使用這個(gè)授權(quán)密碼,而不是賬號(hào)的密碼!
于是在測(cè)試頁(yè)面中輸入授權(quán)碼,郵件就能發(fā)送成功了。
5、排查接收的郵件內(nèi)容為空的問(wèn)題
郵件是能正常發(fā)送出去了,郵件也能正常接收到,但接收到的郵件內(nèi)容是空的:

這是啥情況?明明發(fā)送郵件時(shí)有設(shè)置郵件內(nèi)容的,為啥收到的郵件內(nèi)容是空的呢?
上述代碼在幾年前測(cè)試過(guò),好像沒(méi)問(wèn)題的,難道163郵箱系統(tǒng)升級(jí)了,不再兼容老的數(shù)據(jù)格式了?于是想到了海康的視頻監(jiān)控客戶端,該客戶端可以到海康官網(wǎng)上下載,免費(fèi)使用,其中系統(tǒng)設(shè)置中有個(gè)發(fā)送郵件的功能:

??档纳鲜鼋缑嬷邪l(fā)送測(cè)試郵件是沒(méi)問(wèn)題的,接收到的郵件也是有內(nèi)容的。于是趕緊抓一下海康發(fā)送郵件的數(shù)據(jù)包,以tcp.port==25過(guò)濾了一下,抓出??蛋l(fā)出去的郵件內(nèi)容:

又抓取了一下我們軟件發(fā)出去的郵件內(nèi)容如下:

于是詳細(xì)地對(duì)比了??蹬c我們發(fā)出去的數(shù)據(jù)內(nèi)容,多次嘗試修改我們構(gòu)建郵件數(shù)據(jù)的代碼,比如charset編碼格式、boundry類型等,甚至是否會(huì)空行。最后經(jīng)過(guò)多次嘗試找到了原因,是在具體的郵件內(nèi)容上面需要人為加上一個(gè)空行,我們代碼在構(gòu)造郵件數(shù)據(jù)時(shí)沒(méi)有加空行,導(dǎo)致接收到的郵件內(nèi)容是空的!
以上就是C++調(diào)用libcurl開(kāi)源庫(kù)實(shí)現(xiàn)郵件的發(fā)送功能流程詳解的詳細(xì)內(nèi)容,更多關(guān)于C++ 郵件發(fā)送的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語(yǔ)言 structural body結(jié)構(gòu)體詳解用法
C 數(shù)組允許定義可存儲(chǔ)相同類型數(shù)據(jù)項(xiàng)的變量,結(jié)構(gòu)是 C 編程中另一種用戶自定義的可用的數(shù)據(jù)類型,它允許您存儲(chǔ)不同類型的數(shù)據(jù)項(xiàng),結(jié)構(gòu)用于表示一條記錄,假設(shè)您想要跟蹤圖書(shū)館中書(shū)本的動(dòng)態(tài),您可能需要跟蹤每本書(shū)的下列屬性2021-10-10
C語(yǔ)言實(shí)現(xiàn)學(xué)生學(xué)籍管理系統(tǒng)程序設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)學(xué)生學(xué)籍管理系統(tǒng)程序設(shè)計(jì),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
C++ OpenCV實(shí)現(xiàn)像素畫(huà)的示例代碼
這篇文章主要介紹了通過(guò)OpenCV進(jìn)行圖片像素的變化,從而形成像素畫(huà)效果的功能。文中的示例代碼講解詳細(xì),感興趣的小伙伴可以動(dòng)手試一試2022-01-01
解析為何要關(guān)閉數(shù)據(jù)庫(kù)連接,可不可以不關(guān)閉的問(wèn)題詳解
本篇文章是對(duì)為何要關(guān)閉數(shù)據(jù)庫(kù)連接,可不可以不關(guān)閉的問(wèn)題進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
C語(yǔ)言數(shù)組應(yīng)用實(shí)現(xiàn)掃雷游戲
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言數(shù)組應(yīng)用實(shí)現(xiàn)掃雷游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
用C++實(shí)現(xiàn)單向循環(huán)鏈表的解決方法
本篇文章是對(duì)用C++實(shí)現(xiàn)單向循環(huán)鏈表的解決方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05

