目錄
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庫去實作郵件的發送

如果我們自己去使用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簡單郵件協議的協商與互動,但要發送的郵件內容則需要我們自己去根據協議的規范去構建,那郵件發送的內容的資料格式到底是什么樣子的呢?其實很簡單,找一個支持發送郵件的軟體,發送郵件時抓一下包,就能抓出對應的格式,比如:

按照上面的格式構建就可以了,相關代碼如下:
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服務
上述代碼處理好后,運行如下的測驗程式:

在上述界面中輸入163的smtp服務器地址,使用默認的25埠,并填寫發送郵件地址和發送郵件的密碼,點擊“發送測驗郵件”按鈕,結果郵件并沒有發送成功,
在代碼中添加斷點除錯,發現curl_easy_perform介面回傳的錯誤碼為CURLE_LOGIN_DENIED,如下所示:

于是通過CURLE_OK go到錯誤碼定義的頭檔案中,去查看CURLE_LOGIN_DENIED錯誤碼的含義:

注釋中提示可能是發送郵件的用戶名或密碼錯誤引起的,用戶名和密碼填寫的應該沒問題啊?于是賬號到網頁上登陸一下163郵箱,可以成功登陸的,說明賬號和密碼是沒問題的,那到底是咋回事呢?
后來想到,是不是要到發送郵件賬號中去開啟一下smtp服務才可以登陸到163的smtp服務器上?于是到網頁上登陸,按下列的操作步驟找到開啟當前賬號的smtp服務入口:



點擊開啟按鈕,會彈出如下的提示框:

點擊繼續開啟,進入下面的頁面:

提示需要掃碼發送短信進行驗證,于是使用網易郵件大師APP掃描了一下,自動跳轉到發送短信的頁面,發送驗證短信即可,最后彈出如下的授權密碼頁面:

要將這個授權密碼記錄下來,登陸smtp服務器時需要使用這個授權密碼,而不是賬號的密碼!
于是在測驗頁面中輸入授權碼,郵件就能發送成功了,
5、排查接收的郵件內容為空的問題
郵件是能正常發送出去了,郵件也能正常接收到,但接收到的郵件內容是空的:

這是啥情況?明明發送郵件時有設定郵件內容的,為啥收到的郵件內容是空的呢?
上述代碼在幾年前測驗過,好像沒問題的,難道163郵箱系統升級了,不再兼容老的資料格式了?于是想到了海康的視頻監控客戶端,該客戶端可以到海康官網上下載,免費使用,其中系統設定中有個發送郵件的功能:

海康的上述界面中發送測驗郵件是沒問題的,接收到的郵件也是有內容的,于是趕緊抓一下海康發送郵件的資料包,以tcp.port==25過濾了一下,抓出海康發出去的郵件內容:

又抓取了一下我們軟體發出去的郵件內容如下:

于是詳細地對比了海康與我們發出去的資料內容,多次嘗試修改我們構建郵件資料的代碼,比如charset編碼格式、boundry型別等,甚至是否會空行,最后經過多次嘗試找到了原因,是在具體的郵件內容上面需要人為加上一個空行,我們代碼在構造郵件資料時沒有加空行,導致接收到的郵件內容是空的!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/357110.html
標籤:其他
上一篇:行程與執行緒系列系列講解(一)(超詳細榨干博主系列~)
下一篇:資料結構(C語言版)之佇列
