與郵件收發有關的協議有 POP3、SMPT 和 IMAP 等。
POP3
POP3全稱是 Post Office Protocol 3 ,即郵局協議的第 3 個版本,它規定怎樣將個人計算機連接到 Internet 的郵件伺服器和下載電子郵件的電子協議,它是網際網路電子郵件的第一個離線協議標準,POP3 允許用戶從伺服器上把郵件存儲到本地主機(即自己的計算機)上,同時刪除保存在郵件伺服器上的郵件,而 POP3 伺服器則是遵循 POP3 協議的接收郵件伺服器,用來接收電子郵件的。
SMTP
SMTP 的全稱是 Simple Mail Transfer Protocol,即簡單郵件傳輸協議。它是一組用於從源地址到目的地址傳輸郵件的規範,它幫助每臺計算機在發送或中轉郵件時找到下一個目的地。SMTP 伺服器就是遵循 SMTP 協議的發送郵件伺服器。SMTP 需要認證,簡單地說就是要求必須在提供了帳戶名和密碼之後才可以登錄 SMTP 伺服器,這就使得那些垃圾郵件的散播者無可乘之機,使用戶避免受到垃圾郵件的侵擾。
IMAP
IMAP全稱是 Internet Mail Access Protocol,即交互式郵件存取協議,它是跟 POP3 類似郵件訪問標準協議之一。不同的是,開啟了 IMAP 後,在電子郵件客戶端收取的郵件仍然保留在伺服器上,同時在客戶端上的操作都會反饋到伺服器上,如:刪除郵件,標記已讀等,伺服器上的郵件也會做相應的動作。所以無論從瀏覽器登錄郵箱或者客戶端軟體登錄郵箱,看到的郵件以及狀態都是一致的。而 POP3 對郵件的操作只會在本地郵件客戶端起作用。
讀者如果需要自己編寫相關的郵件收發客戶端,需要登錄對應的郵件伺服器開啟相應的 POP3/SMTP/IMAP 服務。以 163 郵箱為例:
請登錄 163 郵箱(http://mail.163.com/),點擊頁面正上方的「設置」,再點擊左側上「POP3/SMTP/IMAP」,其中「開啟 SMTP 服務」是系統默認勾選開啟的。讀者可勾選圖中另兩個選項,點擊確定,即可開啟成功。不勾選圖中兩個選項,點擊確定,可關閉成功。
網易163免費郵箱相關伺服器信息:
163免費郵客戶端設置的POP3、SMTP、IMAP地址POP3、SMTP、IMAP 協議就是我們前面介紹的以指定字符(串)為包的結束標誌的協議典型例子。我們來以 SMTP 協議和 POP3 協議為例來講解一下。
SMTP 協議先來介紹 SMTP 協議吧,SMTP 全稱是 Simple Mail Transfer Protocol,即簡單郵件傳輸協議,該協議用於發送郵件。
SMTP 協議的格式:
「自定義內容」根據「關鍵字」的類型是否設置,對於使用 SMTP 作為客戶端的一方常用的「關鍵字「如下所示:
//連接上郵件伺服器之後登錄伺服器之前向伺服器發送的問候信息HELO 自定義問候語\r\n
//請求登錄郵件伺服器AUTH LOGIN\r\nbase64形式的用戶名\r\nbase64形式的密碼\r\n
//設置發件人郵箱地址MAIL FROM:發件人地址\r\n
//設置收件人地址,每次發送可設置一個收件人地址,如果有多個收件地址,要分別設置對應次數rcpt to:收件人地址\r\n
//發送郵件正文開始標誌DATA\r\n//發送郵件正文,注意郵件正文以.\r\n結束郵件正文\r\n.\r\n
//登出伺服器QUIT\r\n使用 SMTP 作為郵件伺服器的一方常用的「關鍵字「是定義的各種應答碼,應答碼後面可以帶上自己的信息,然後以\r\n作為結束,格式如下:
常用的應答碼含義如下所示:
211 幫助返回系統狀態214 幫助信息220 服務準備就緒221 關閉連接235 用戶驗證成功250 請求操作就緒251 用戶不在本地,轉寄到其他路徑334 等待用戶輸入驗證信息354 開始郵件輸入421 服務不可用450 操作未執行,郵箱忙451 操作中止,本地錯誤452 操作未執行,存儲空間不足500 命令不可識別或語言錯誤501 參數語法錯誤502 命令不支技503 命令順序錯誤504 命令參數不支持550 操作未執行,郵箱不可用551 非本地用戶552 中止存儲空間不足553 操作未執行,郵箱名不正確554 傳輸失敗更多的 SMTP 協議的細節可以參考相應的 RFC 文檔。
下面我們來看一個具體的使用 SMTP 發送郵件的代碼示例,假設我們現在要實現一個郵件報警系統,根據上文的介紹,我們實現一個 SmtpSocket 類來綜合常用郵件的功能:
SmtpSocket.h
/** * 發送郵件類,SmtpSocket.h * zhangyl 2019.05.11 */
#pragma once
#include <string>#include <vector>#include "Platform.h"
class SmtpSocket final{public: static bool sendMail(const std::string& server, short port, const std::string& from, const std::string& fromPassword, const std::vector<std::string>& to, const std::string& subject, const std::string& mailData);
public: SmtpSocket(void); ~SmtpSocket(void); bool isConnected() const { return m_hSocket; } bool connect(const char* pszUrl, short port = 25); bool logon(const char* pszUser, const char* pszPassword); bool setMailFrom(const char* pszFrom); bool setMailTo(const std::vector<std::string>& sendTo); bool send(const std::string& subject, const std::string& mailData);
void closeConnection(); void quit(); //退出
private: /** * 驗證從伺服器返回的前三位代碼和傳遞進來的參數是否一樣 */ bool checkResponse(const char* recvCode);
private: bool m_bConnected; SOCKET m_hSocket; std::string m_strUser; std::string m_strPassword; std::string m_strFrom; std::vector<std::string> m_strTo;;};SmtpSocket.cpp
#include "SmtpSocket.h"#include <sstream>#include <time.h>#include <string.h>#include "Base64Util.h"#include "Platform.h"
bool SmtpSocket::sendMail(const std::string& server, short port, const std::string& from, const std::string& fromPassword, const std::vector<std::string>& to, const std::string& subject, const std::string& mailData){ size_t atSymbolPos = from.find_first_of("@"); if (atSymbolPos == std::string::npos) return false;
std::string strUser = from.substr(0, atSymbolPos); SmtpSocket smtpSocket; //smtp.163.com 25 if (!smtpSocket.connect(server.c_str(), port)) return false;
//testformybook 2019hhxxttxs if (!smtpSocket.logon(strUser.c_str(), fromPassword.c_str())) return false;
//testformybook@163.com if (!smtpSocket.setMailFrom(from.c_str())) return false;
if (!smtpSocket.setMailTo(to)) return false;
if (!smtpSocket.send(subject, mailData)) return false;
return true;}
SmtpSocket::SmtpSocket() : m_bConnected(false), m_hSocket(-1){
}
SmtpSocket::~SmtpSocket(){ quit();}
bool SmtpSocket::checkResponse(const char* recvCode){ char recvBuffer[1024] = { 0 }; long lResult = 0; lResult = recv(m_hSocket, recvBuffer, 1024, 0); if (lResult == SOCKET_ERROR || lResult < 3) return false;
return recvCode[0] == recvBuffer[0] && \ recvCode[1] == recvBuffer[1] && \ recvCode[2] == recvBuffer[2] ? true : false;}
void SmtpSocket::quit(){ if (m_hSocket < 0) return;
//退出 if (::send(m_hSocket, "QUIT\r\n", strlen("QUIT\r\n"), 0) == SOCKET_ERROR) { closeConnection(); return; }
if (!checkResponse("221")) return;}
bool SmtpSocket::logon(const char* pszUser, const char* pszPassword){ if (m_hSocket < 0) return false;
//發送"AUTH LOGIN" if (::send(m_hSocket, "AUTH LOGIN\r\n", strlen("AUTH LOGIN\r\n"), 0) == SOCKET_ERROR) return false;
if (!checkResponse("334")) return false;
//發送經base64編碼的用戶名 char szUserEncoded[64] = { 0 }; Base64Util::encode(szUserEncoded, pszUser, strlen(pszUser), '=', 64); strncat(szUserEncoded, "\r\n", 64); //MailLogNormalA("[SmtpSocket::Logon] Logon [User:%s].", lpUser); if (::send(m_hSocket, szUserEncoded, strlen(szUserEncoded), 0) == SOCKET_ERROR) return false;
if (!checkResponse("334")) return false;
//發送經base64編碼的密碼 //驗證密碼 char szPwdEncoded[64] = { 0 }; Base64Util::encode(szPwdEncoded, pszPassword, strlen(pszPassword), '=', 64); strncat(szPwdEncoded, "\r\n", 64); //MailLogNormalA("[SmtpSocket::Logon] Logon [User:%s] [Pass:*****].", lpUser); if (::send(m_hSocket, szPwdEncoded, strlen(szPwdEncoded), 0) == SOCKET_ERROR) return false;
if (!checkResponse("235")) return false;
m_strUser = pszUser; m_strPassword = pszPassword;
return true;}
void SmtpSocket::closeConnection(){ if (m_hSocket >= 0) { closesocket(m_hSocket); m_hSocket = -1; m_bConnected = false; }}
bool SmtpSocket::connect(const char* pszUrl, short port/* = 25*/){ //MailLogNormalA("[SmtpSocket::Connect] Start connect [%s:%d].", lpUrl, lPort); struct sockaddr_in server = { 0 }; struct hostent* pHostent = NULL; unsigned int addr = 0;
closeConnection(); m_hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_hSocket < 0) return false;
long tmSend(15 * 1000L), tmRecv(15 * 1000L), noDelay(1); setsockopt(m_hSocket, IPPROTO_TCP, TCP_NODELAY, (char*)& noDelay, sizeof(long)); setsockopt(m_hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)& tmSend, sizeof(long)); setsockopt(m_hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)& tmRecv, sizeof(long));
if (inet_addr(pszUrl) == INADDR_NONE) { pHostent = gethostbyname(pszUrl); } else { addr = inet_addr(pszUrl); pHostent = gethostbyaddr((char*)& addr, sizeof(addr), AF_INET); }
if (!pHostent) return false;
server.sin_family = AF_INET; server.sin_port = htons((u_short)port); server.sin_addr.s_addr = *((unsigned long*)pHostent->h_addr); if (::connect(m_hSocket, (struct sockaddr*) & server, sizeof(server)) == SOCKET_ERROR) return false;
if (!checkResponse("220")) return false;
//向伺服器發送"HELO "+伺服器名 //string strTmp="HELO "+SmtpAddr+"\r\n"; char szSend[256] = { 0 }; snprintf(szSend, sizeof(szSend), "HELO %s\r\n", pszUrl); if (::send(m_hSocket, szSend, strlen(szSend), 0) == SOCKET_ERROR) return false;
if (!checkResponse("250")) return false;
m_bConnected = true;
return true;}
bool SmtpSocket::setMailFrom(const char* pszFrom){ if (m_hSocket < 0) return false;
char szSend[256] = { 0 }; snprintf(szSend, sizeof(szSend), "MAIL FROM:<%s>\r\n", pszFrom); if (::send(m_hSocket, szSend, strlen(szSend), 0) == SOCKET_ERROR) return false;
if (!checkResponse("250")) return false;
m_strFrom = pszFrom;
return true;}
bool SmtpSocket::setMailTo(const std::vector<std::string>& sendTo){ if (m_hSocket < 0) return false;
char szSend[256] = { 0 };
for (const auto& iter : sendTo) { snprintf(szSend, sizeof(szSend), "rcpt to: <%s>\r\n", iter.c_str()); if (::send(m_hSocket, szSend, strlen(szSend), 0) == SOCKET_ERROR) return false;
if (!checkResponse("250")) return false; }
m_strTo = sendTo;
return true;}
bool SmtpSocket::send(const std::string& subject, const std::string& mailData){ if (m_hSocket < 0) return false;
std::ostringstream osContent;
//注意:郵件正文內容與其他附屬字樣之間一定要空一行 osContent << "Date: " << time(nullptr) << "\r\n"; osContent << "from: " << m_strFrom << "\r\n"; osContent << "to: "; for (const auto& iter : m_strTo) { osContent << iter << ";"; } osContent << "\r\n"; osContent << "subject: " << subject << "\r\n"; osContent << "Content-Type: text/plain; charset=UTF-8\r\n"; osContent << "Content-Transfer-Encoding: quoted-printable\r\n\r\n"; osContent << mailData << "\r\n.\r\n";
std::string data = osContent.str(); const char* lpSendBuffer = data.c_str();
//發送"DATA\r\n" if (::send(m_hSocket, "DATA\r\n", strlen("DATA\r\n"), 0) == SOCKET_ERROR) return false;
if (!checkResponse("354")) return false;
long dwSend = 0; long dwOffset = 0; long lTotal = data.length(); long lResult = 0; const long SEND_MAX_SIZE = 1024 * 100000; while ((long)dwOffset < lTotal) { if (lTotal - dwOffset > SEND_MAX_SIZE) dwSend = SEND_MAX_SIZE; else dwSend = lTotal - dwOffset;
lResult = ::send(m_hSocket, lpSendBuffer + dwOffset, dwSend, 0); if (lResult == SOCKET_ERROR) return false;
dwOffset += lResult; }
if (!checkResponse("250")) return false;
return true;}然後我們使用另外一個類 MailMonitor 對 SmtpSocket 對象的功能進行高層抽象:
MailMonitor.h
/** * 郵件監控線程, MailMonitor.h * zhangyl 2019.05.11 */
#pragma once
#include <string>#include <vector>#include <list>#include <memory>#include <mutex>#include <condition_variable>#include <thread>
struct MailItem{ std::string subject; std::string content;};
class MailMonitor final{public: static MailMonitor& getInstance();
private: MailMonitor() = default; ~MailMonitor() = default; MailMonitor(const MailMonitor & rhs) = delete; MailMonitor& operator=(const MailMonitor & rhs) = delete;
public: bool initMonitorMailInfo(const std::string& servername, const std::string& mailserver, short mailport, const std::string& mailfrom, const std::string& mailfromPassword, const std::string& mailto); void uninit(); void wait();
void run();
bool alert(const std::string& subject, const std::string& content);
private: void alertThread();
void split(const std::string& str, std::vector<std::string>& v, const char* delimiter = "|"); private: std::string m_strMailName; //用於標識是哪一臺伺服器發送的郵件 std::string m_strMailServer; short m_nMailPort; std::string m_strFrom; std::string m_strFromPassword; std::vector<std::string> m_strMailTo;
std::list<MailItem> m_listMailItemsToSend; //待寫入的日誌 std::shared_ptr<std::thread> m_spMailAlertThread; std::mutex m_mutexAlert; std::condition_variable m_cvAlert; bool m_bExit; //退出標誌 bool m_bRunning; //運行標誌};MailMonitor.cpp
#include "MailMonitor.h"#include <functional>#include <sstream>#include <iostream>#include <string.h>#include "SmtpSocket.h"
MailMonitor& MailMonitor::getInstance(){ static MailMonitor instance; return instance;}
bool MailMonitor::initMonitorMailInfo(const std::string& servername, const std::string& mailserver, short mailport, const std::string& mailfrom, const std::string& mailfromPassword, const std::string& mailto){ if (servername.empty() || mailserver.empty() || mailport < 0 || mailfrom.empty() || mailfromPassword.empty() || mailto.empty()) { std::cout << "Mail account info is not config, not start MailAlert" << std::endl; return false; }
m_strMailName = servername;
m_strMailServer = mailserver; m_nMailPort = mailport; m_strFrom = mailfrom; m_strFromPassword = mailfromPassword;
split(mailto, m_strMailTo, ";");
std::ostringstream osSubject; osSubject << "[" << m_strMailName << "]";
SmtpSocket::sendMail(m_strMailServer, m_nMailPort, m_strFrom, m_strFromPassword, m_strMailTo, osSubject.str(), "You have started Mail Alert System.");
return true;}
void MailMonitor::uninit(){ m_bExit = true;
m_cvAlert.notify_one();
if (m_spMailAlertThread->joinable()) m_spMailAlertThread->join();}
void MailMonitor::wait(){ if (m_spMailAlertThread->joinable()) m_spMailAlertThread->join();}
void MailMonitor::run(){ m_spMailAlertThread.reset(new std::thread(std::bind(&MailMonitor::alertThread, this)));}
void MailMonitor::alertThread(){ m_bRunning = true;
while (true) { MailItem mailItem; { std::unique_lock<std::mutex> guard(m_mutexAlert); while (m_listMailItemsToSend.empty()) { if (m_bExit) return;
m_cvAlert.wait(guard); }
mailItem = m_listMailItemsToSend.front(); m_listMailItemsToSend.pop_front(); }
std::ostringstream osSubject; osSubject << "[" << m_strMailName << "]" << mailItem.subject; SmtpSocket::sendMail(m_strMailServer, m_nMailPort, m_strFrom, m_strFromPassword, m_strMailTo, osSubject.str(), mailItem.content); }// end outer-while-loop
m_bRunning = false;}
bool MailMonitor::alert(const std::string& subject, const std::string& content){ if (m_strMailServer.empty() || m_nMailPort < 0 || m_strFrom.empty() || m_strFromPassword.empty() || m_strMailTo.empty()) return false;
MailItem mailItem; mailItem.subject = subject; mailItem.content = content;
{ std::lock_guard<std::mutex> lock_guard(m_mutexAlert); m_listMailItemsToSend.push_back(mailItem); m_cvAlert.notify_one(); } return true;}
void MailMonitor::split(const std::string& str, std::vector<std::string>& v, const char* delimiter/* = "|"*/){ if (delimiter == NULL || str.empty()) return;
std::string buf(str); size_t pos = std::string::npos; std::string substr; int delimiterlength = strlen(delimiter); while (true) { pos = buf.find(delimiter); if (pos != std::string::npos) { substr = buf.substr(0, pos); if (!substr.empty()) v.push_back(substr);
buf = buf.substr(pos + delimiterlength); } else { if (!buf.empty()) v.push_back(buf); break; } }}程序中另外用到的兩個輔助類文件如下:
Base64Util.h
#pragma once
class Base64Util final{private: Base64Util() = delete; ~Base64Util() = delete; Base64Util(const Base64Util& rhs) = delete; Base64Util& operator=(const Base64Util& rhs) = delete;
public: static int encode(char* pDest, const char* pSource, int lenSource, char chMask, int maxDest); static int decode(char* pDest, const char* pSource, int lenSource, char chMask, int maxDest); static bool check(char* lpString);};Base64Util.cpp
#include "Base64Util.h"/////////////////////////////////////////////////////////////////////////////////////////////////static const char __DeBase64Tab__[] ={ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, // '+' 0, 0, 0, 63, // '/' 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // '0'-'9' 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // 'A'-'Z' 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // 'a'-'z'};static const char __EnBase64Tab__[] = { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" };
int Base64Util::encode(char* pDest, const char* pSource, int lenSource, char chMask, int maxDest){ char c1, c2, c3; int i = 0, lenDest(0), lDiv(lenSource / 3), lMod(lenSource % 3); for (; i < lDiv; ++i, lenDest += 4) { if (lenDest + 4 >= maxDest) return 0; c1 = *pSource++; c2 = *pSource++; c3 = *pSource++; *pDest++ = __EnBase64Tab__[c1 >> 2]; *pDest++ = __EnBase64Tab__[((c1 << 4) | (c2 >> 4)) & 0X3F]; *pDest++ = __EnBase64Tab__[((c2 << 2) | (c3 >> 6)) & 0X3F]; *pDest++ = __EnBase64Tab__[c3 & 0X3F]; } if (lMod == 1) { if (lenDest + 4 >= maxDest) return(0); c1 = *pSource++; *pDest++ = __EnBase64Tab__[(c1 & 0XFC) >> 2]; *pDest++ = __EnBase64Tab__[((c1 & 0X03) << 4)]; *pDest++ = chMask; *pDest++ = chMask; lenDest += 4; } else if (lMod == 2) { if (lenDest + 4 >= maxDest) return(0); c1 = *pSource++; c2 = *pSource++; *pDest++ = __EnBase64Tab__[(c1 & 0XFC) >> 2]; *pDest++ = __EnBase64Tab__[((c1 & 0X03) << 4) | ((c2 & 0XF0) >> 4)]; *pDest++ = __EnBase64Tab__[((c2 & 0X0F) << 2)]; *pDest++ = chMask; lenDest += 4; } *pDest = 0; return(lenDest);}
int Base64Util::decode(char* pDest, const char* pSource, int lenSource, char chMask, int maxDest){ int lenDest = 0, nValue = 0, i = 0; for (; i < lenSource; i += 4) { nValue = __DeBase64Tab__[(int)(*pSource)] << 18; pSource++; nValue += __DeBase64Tab__[(int)*pSource] << 12; pSource++; if (++lenDest >= maxDest) break;
*pDest++ = char((nValue & 0X00FF0000) >> 16);
if (*pSource != chMask) { nValue += __DeBase64Tab__[(int)*pSource] << 6; pSource++; if (++lenDest >= maxDest) break; *pDest++ = (nValue & 0X0000FF00) >> 8;
if (*pSource != chMask) { nValue += __DeBase64Tab__[(int)*pSource]; pSource++; if (++lenDest >= maxDest) break; *pDest++ = nValue & 0X000000FF; } } } *pDest = 0; return(lenDest);}
bool Base64Util::check(char* lpString){ for (; *lpString; ++lpString) { switch (*lpString) { case '+': *lpString = '@'; break; case '@': *lpString = '+'; break; case '=': *lpString = '$'; break; case '$': *lpString = '='; break; case '/': *lpString = '#'; break; case '#': *lpString = '/'; break; default: if (*lpString >= 'A' && *lpString <= 'Z') * lpString = *lpString - 'A' + 'a'; else if (*lpString >= 'a' && *lpString <= 'z') * lpString = *lpString - 'a' + 'A'; else if (*lpString >= '0' && *lpString <= '4') * lpString = *lpString - '0' + '5'; else if (*lpString >= '5' && *lpString <= '9') * lpString = *lpString - '5' + '0'; else return false; } } return true;}Platform.h
#pragma once
#include <stdint.h>
#if defined(__GNUC__)#pragma GCC diagnostic push#pragma GCC diagnostic ignored "-Wdeprecated-declarations"#elif defined(_MSC_VER)#pragma warning(disable : 4996)#endif
#ifdef WIN32
#pragma comment(lib, "Ws2_32.lib")#pragma comment(lib, "Shlwapi.lib")
//remove warning C4996 on Windows//#define _CRT_SECURE_NO_WARNINGS
typedef int socklen_t;//typedef uint64_t ssize_t;typedef unsigned int in_addr_t;
//Windows 上沒有這些結構的定義,為了移植方便,手動定義這些結構#define XPOLLIN 1#define XPOLLPRI 2#define XPOLLOUT 4#define XPOLLERR 8 #define XPOLLHUP 16#define XPOLLNVAL 32#define XPOLLRDHUP 8192
#define XEPOLL_CTL_ADD 1#define XEPOLL_CTL_DEL 2#define XEPOLL_CTL_MOD 3
#pragma pack(push, 1)typedef union epoll_data { void* ptr; int fd; uint32_t u32; uint64_t u64;} epoll_data_t;
struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};#pragma pack(pop)
#include <winsock2.h>#include <ws2tcpip.h>#include <Windows.h>#include <Ws2ipdef.h>#include <io.h> //_pipe#include <fcntl.h> //for O_BINARY#include <shlwapi.h>
class NetworkInitializer{public: NetworkInitializer(); ~NetworkInitializer();};
#else
typedef int SOCKET;
#define SOCKET_ERROR -1
#define closesocket(s) close(s)
#include <arpa/inet.h>
#include <netinet/in.h>#include <netinet/tcp.h>#include <netinet/in.h>#include <netdb.h>
#include <unistd.h>#include <stdint.h>#include <endian.h>#include <poll.h>#include <fcntl.h>#include <signal.h>#include <inttypes.h>#include <errno.h>#include <dirent.h>
#include <sys/socket.h>#include <sys/select.h>#include <sys/time.h>#include <sys/types.h>#include <sys/eventfd.h>#include <sys/stat.h>#include <sys/uio.h>#include <sys/epoll.h>#include <sys/syscall.h>
//for ubuntu readv not found#ifdef __UBUNTU#include <sys/uio.h>#endif
#define XPOLLIN POLLIN#define XPOLLPRI POLLPRI#define XPOLLOUT POLLOUT#define XPOLLERR POLLERR #define XPOLLHUP POLLHUP#define XPOLLNVAL POLLNVAL#define XPOLLRDHUP POLLRDHUP
#define XEPOLL_CTL_ADD EPOLL_CTL_ADD#define XEPOLL_CTL_DEL EPOLL_CTL_DEL#define XEPOLL_CTL_MOD EPOLL_CTL_MOD
//Linux下沒有這兩個函數,定義之#define ntohll(x) be64toh(x)#define htonll(x) htobe64(x)
#endifPlatform.cpp
#include "Platform.h"
#ifdef WIN32
NetworkInitializer::NetworkInitializer(){ WORD wVersionRequested = MAKEWORD(2, 2); WSADATA wsaData; ::WSAStartup(wVersionRequested, &wsaData); }
NetworkInitializer::~NetworkInitializer(){ ::WSACleanup();}
#endif我們在 main 函數模擬產生一條新的報警郵件:
main.cpp
/** * 郵件報警demo * zhangyl 2020.04.09 **/
#include <iostream>#include <stdlib.h>#include "Platform.h"#include "MailMonitor.h"
//Winsock網絡庫初始化#ifdef WIN32NetworkInitializer windowsNetworkInitializer;#endif
#ifndef WIN32void prog_exit(int signo){ std::cout << "program recv signal [" << signo << "] to exit." << std::endl;
//停止郵件發送服務 MailMonitor::getInstance().uninit();}
#endif
const std::string servername = "MailAlertSysem";const std::string mailserver = "smtp.163.com";const short mailport = 25;const std::string mailuser = "testformybook@163.com";const std::string mailpassword = "2019hhxxttxs";const std::string mailto = "balloonwj@qq.com;analogous_love@qq.com";
int main(int argc, char* argv[]){#ifndef WIN32 //設置信號處理 signal(SIGCHLD, SIG_DFL); signal(SIGPIPE, SIG_IGN); signal(SIGINT, prog_exit); signal(SIGTERM, prog_exit);#endif bool bInitSuccess = MailMonitor::getInstance().initMonitorMailInfo(servername, mailserver, mailport, mailuser, mailpassword, mailto); if (bInitSuccess) MailMonitor::getInstance().run();
const std::string subject = "Alert Mail"; const std::string content = "This is an alert mail from " + mailuser; MailMonitor::getInstance().alert(subject, content);
//等待報警郵件線程退出 MailMonitor::getInstance().wait();
return 0;}上述代碼使用了 163 郵箱帳號 testformybook@163.com 給 QQ 郵箱帳戶 balloonwj@qq.com 和 analogous_love@qq.com 分別發送郵件,發送給郵件的函數是 MailMonitor::alert() 函數,實際發送郵件的函數是 SmtpSocket::send() 函數。
無論在 Windows 或者 Linux 上編譯運行程序,我們的兩個郵箱都會分別收到兩封郵件,如下圖所示:
產生第一封郵件的原因是我們在 main 函數中調用 MailMonitor::getInstance().initMonitorMailInfo() 初始化郵箱伺服器名、地址、埠號、用戶名和密碼時,MailMonitor::initMonitorMailInfo() 函數內部會調用 SmtpSocket::sendMail() 函數發送一封郵件通知指定聯繫人郵件報警系統已經啟動:
bool MailMonitor::initMonitorMailInfo(const std::string& servername, const std::string& mailserver, short mailport, const std::string& mailfrom, const std::string& mailfromPassword, const std::string& mailto){ //...無關代碼省略...
SmtpSocket::sendMail(m_strMailServer, m_nMailPort, m_strFrom, m_strFromPassword, m_strMailTo, osSubject.str(), "You have started Mail Alert System.");
return true;}產生第二封郵件則是我們在 main 函數中主動調用產生報警郵件的函數:
const std::string subject = "Alert Mail";const std::string content = "This is an alert mail from " + mailuser;MailMonitor::getInstance().alert(subject, content);我們以第一封郵件為例來說明整個郵件發送過程中,我們的程序(客戶端)與 163 郵件伺服器之間的協議數據的交換內容,核心的郵件發送功能在 SmtpSocket::sendMail() 函數中:
bool SmtpSocket::sendMail(const std::string& server, short port, const std::string& from, const std::string& fromPassword, const std::vector<std::string>& to, const std::string& subject, const std::string& mailData){ size_t atSymbolPos = from.find_first_of("@"); if (atSymbolPos == std::string::npos) return false;
std::string strUser = from.substr(0, atSymbolPos); SmtpSocket smtpSocket; //smtp.163.com 25 if (!smtpSocket.connect(server.c_str(), port)) return false;
//testformybook 2019hhxxttxs if (!smtpSocket.logon(strUser.c_str(), fromPassword.c_str())) return false;
//testformybook@163.com if (!smtpSocket.setMailFrom(from.c_str())) return false;
if (!smtpSocket.setMailTo(to)) return false;
if (!smtpSocket.send(subject, mailData)) return false;
return true;}這個函數先創建 socket,再使用郵箱的地址和埠後去連接伺服器(smtpSocket.connect() 函數內部額外做了一步將域名解析成 ip 地址的工作),連接成功後開始和伺服器端進行數據交換:
client: 嘗試連接伺服器client: 連接成功server: 220\r\n
client: helo自定義問候語\r\nserver: 250\r\n
client: AUTH LOGIN\r\nserver: 334\r\n
client: base64編碼後的用戶名\r\nserver: 334\r\n
client: base64編碼後的密碼\r\nserver: 235\r\n
client: MAIL FROM:<發件人地址>\r\nserver: 250\r\n
client: rcpt to:<收件人地址1>\r\nserver: 250\r\n
client: rcpt to:<收件人地址2>\r\nserver: 250\r\n
client: DATA\r\nserver: 354\r\n
client: 郵件正文\r\n.\r\nserver: 250\r\n
client:QUIT\r\nserver:221\r\n我們將上述過程繪製成如下示意圖:
最終郵件就發出去了,這裡我們模擬了客戶端使用 smtp 協議給伺服器端發送郵件,我們自己實現伺服器端接收客戶端發送的郵件請求也是一樣的道理。這就是 SMTP 協議的格式,SMTP 協議是以特定標記作為分隔符的協議格式典型。
讀者可以在 Windows 上或者 Linux 主機上測試上述程序,如果讀者在阿里雲這樣的雲主機上測試,阿里雲等雲主機為了避免在網絡上產生大量垃圾郵件默認是禁止發往其他伺服器的 25 號埠的數據的,讀者需要申請解除該埠限制,或者將郵件伺服器的 25 埠改成其他埠(一般改成 465 埠)。
完整的發送郵件編解碼 SMTP 協議示例代碼請在【高性能伺服器開發】公眾號後臺回復關鍵字「SMTP」。
上文我們介紹了 SMTP 協議常用的協議命令,SMTP 協議支持的完整命令列表讀者可以參考 rfc5321 文檔:https://tools.ietf.org/html/rfc5321 。
POP3 協議我們再來看下 POP3 協議
client:連接郵箱pop伺服器,連接成功server:+OK Welcome to coremail Mail Pop3 Server (163coms[10774b260cc7a37d26d71b52404dcf5cs])\r\n
client:USER 用戶名\r\nserver:+OK core mail
client:PASS 密碼\r\nserver:+OK 202 message(s) [3441786 byte(s)]\r\n
client:LIST\r\nserver:+OK 5 30284\r\n1 8284\r\n2 11032\r\n3 2989\r\n4 3871\r\n5 4108\r\n.\r\n
client:RETR 100\r\nserver:+OK 4108 octets\r\nReceived: from sonic310-21.consmr.mail.gq1.yahoo.com (unknown [98.137.69.147])by mx29 (Coremail) with SMTP id T8CowABHlztmml5erAoHAQ--.23443S3;Wed, 04 Mar 2020 01:56:57 +0800 (CST)\r\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo.com; s=s2048; t=1583258213; bh=ABL3sF+YL/syl+mwknwxiAlvKPRNYq4AYTujNrPA86g=; h=Date:From:Reply-To:Subject:References:From:Subject;\r\nb=OrAQTs0GJnA9yVA08gRolpsoe9E2PQhc3BLvK0msqlZkIYPYVLD1SHAHc7eI3imH4b+hggrFA6wUeiSqqq2du3tOokCU8ckq3LrbdI82EZ013M3KL6o2y+/wdPIj9Mo1TeGbmqtthYBOpGvEgwzsQMNnydkJdy5tDaW6IBT2Ux+IaP0K+jp71eYXcWjdR0mSyu3aMhLqc0z4l5HlZYpZRQG1hjxZOaCH/UjgBAdr98JecVvuRp4s5iGe6OIxc0p3xzRZBxTlLdgdHjmTKHQ00eTNCfFYai2rMxf4830lMYTwKI6O/iu3jUbTA2yjxx0LrYBFTiWzFetwAQupKLw3Qg==\r\nX-YMail-OSG: 7HG016cVM1nEI.fdz8BF9PN3tO6MvrppAOwu_jpQ09s4eVdYvLXavghvjDvWrRWB7PF6pZKuhiLjV7yCErxEmbWUKPLzX.WL4RJ9u4tnPC4NyVp30cLaoGZVIapWeFtqRpKlh31orVYWTsWE9FcDuHts5p2MPAd7Si52EZfyPuoffEIWrd481hx1IdSsRQN_V7mpfxihvReOIoQ5rCWuMzdoK5kXOho8iOwXlEVPzdTs33RD5rQmwbycPtLS7.FARNxWl9yO9Lrd25gDYa1hXvgG4aQptJQK5aDcHQpZUYqdiaNUaEoGoIDQR_HVndus53gTyzUzmJONpDo6wQM39O.pih7VGCrgLqB2_hHeJdPEUIkjCcwkqNn0cfDyc4QwBdQ65jcgm2cJDPFgoODhxDIqTqeFVbXFr2cXLand8vAqARi3tlnmsOUA7ZIDiSvhSx8eYGd4_frX1LfP.TpctO9Uuc3ZP6iP_K24F9HE9HNN9_swBUEPlBOB3jjSPSOdmiEMteFNWj1qOJ8i47BwMBILtx0dZheRRSxvfzSA.JmnUghFo80EgaGRgXYIEAzt8hpvxdZbtwrg0k0WPeFY4LC2my.A9XcsnF58558bweJDaDHCJyLGFnE8__ZQI163vMPqY6QbU3OP0EJz2OE1rPBOrq9PUolTZjOEu6ghV1PG2HhX0Ydc.vvq5mloqbKusdzV5EgtpFZjLdp1_RQWuI1LG865Ig756HBaozMU6RG0FUMn86pvXRBbNMPD6ADwcw4rdw.Xqk5TRZkqJpSp6KX82OjAgFu0xxMiZnQ7LNemrsJ2UQK9Y2_nm8nrwOIX03Ol6Z2KspWUcPNkqPIZ6vGAr9FO9qqE_elB3K4hh04lq_KS5Tv_XoI3deD4r3J6RTbO9xp5O6cbe0Svy7FS7DosvJfK958_57Kk_6vk6wxxc3D8cx_k6P.yPbphTCLYFdfnbV5sRKNvUKT.apHpO8d0GUf29QtSc3dwBDrLEcRSpguJ3tMKBc2GZPCwUMOgf2b24zFZ49.D7MRQbZifaHsk4dJL9jxS2qdN5pSjhZUjbLCUQ2YGcYgmNnTbfjAIaxqUWNSgpypIYNmi.lgG4bM_gW7sXH_Y3TULcsC.1GXTSjZUdUvvkr7BDnzUy3FGqv9Eyfb7GOwPzTXLzdurcd6eHx0ejCmC6gVJIwoIh9S0YSK659aK2usThSAyogrxqQ664fZr70CrLJehr5OZNLstPt3fiJhyUR1DXrlm6myQ9uSQ5vPTl0p2.DemDaYk84mtcZO0EEjKIzqeSvZ505Fex0u.66Mzu2lmr07WwMCE7wgqwOSWRnYNCz2rWcLmXA_TVDtdJ85bHZ79FY6Vs5pGJjp.7YgDnVqysBp95w--\r\nReceived: from sonic.gate.mail.ne1.yahoo.com by sonic310.consmr.mail.gq1.yahoo.com with HTTP; Tue, 3 Mar 2020 17:56:53 +0000\r\nDate: Tue, 3 Mar 2020 17:56:49 +0000 (UTC)\r\nFrom: Peter Edward Copley <noodlelife@yahoo.com>\r\nReply-To: Peter Edward Copley <pshun3592@gmail.com>\r\nMessage-ID: <729348196.5391236.1583258209467@mail.yahoo.com>\r\nSubject: Re:Hello\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; \r\nboundary="----=_Part_5391235_1821490954.1583258209466"\r\nReferences: <729348196.5391236.1583258209467.ref@mail.yahoo.com>\r\nX-Mailer: WebService/1.1.15302 YMailNorrin Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36\r\nX-CM-TRANSID:T8CowABHlztmml5erAoHAQ--.23443S3\r\nAuthentication-Results: mx29; spf=pass smtp.mail=noodlelife@yahoo.com;\r\ndkim=pass header.i=@yahoo.com\r\nX-Coremail-Antispam: 1Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73VFW2AGmfu7bjvjm3AaLaJ3UbIYCTnIWIevJa73UjIFyTuYvjxU-NtIUUUUU\r\n\r\n-=_Part_5391235_1821490954.1583258209466\r\n.\r\n
client:QUIT\r\nserver:+OK core mail\r\n上述過程如下示意圖所示:
當我們收取郵件正文之後,我們就可以根據郵件正文中的各種 tag 來解析郵件內容得到郵件的 MessageID、收件人、發件人、郵件主題、正文和附件,注意附件內容也會被拆成特定的編碼格式放在郵件中。郵件正文裡面按所謂的 boundary 來分成多個塊,例如上文中的 boundary="----=_Part_5391235_1821490954.1583258209466"。
我們來看一個具體的例子吧。
上述郵件的主題是「測試郵件」,內容是純文本「這是一封測試郵件,含有兩個附件。」,還有兩個附件,一張名為 self.jpg 的圖片,一個名為 test.docx 的文檔。我們將郵件下載下來後得到郵件原文是:
+OK 93763 octetsReceived: from qq.com (unknown [183.3.226.165]) by mx27 (Coremail) with SMTP id TcCowABHJo+dMqReI+72Bg--.18000S3; Sat, 25 Apr 2020 20:52:45 +0800 (CST)DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=qq.com; s=s201512; t=1587819165; bh=RLNDml5+GusG7KQTgkjeS/Mpn1m/LmqBUaz6Nmo6ukY=; h=From:To:Subject:Mime-Version:Date:Message-ID; b=K3sJK+aPQ9zHu1GUvKckofm3cfocpze10XBp9FufVVVYS423myQnFWMaREpGGbeaS vrCGdjawcfhXpkvGZnhOkJZrtut1er5zWZRkmsDnqvoekRURXKt3wWyOv5WUuSPHZI NzGjMQbtYmbWjFla7zs1Cg81UQKRtg1s5KxWwGVQ=X-QQ-FEAT: CPmoSFXLZ/TSSc3nxNJn8bUc57myjtkH8mxkmSC9/G9nP1mNDXcYVAAERmmiE 038rlXj8w6qkTmh1317bdJp9MqMMEUSgpJC5DulJn4k6WCURo4NEYDiuUQK/J+YfUQnpETt w4aQYpj6nKAIqKgorGGK0zy6oQWavfOgssyvSU15d6wqlw904x6aZhS3KAUAM4+eGitBRk9 fxUEABnV/opGuLtZ/fex+UsUAVgXFbTZPoYjhxoM4ZKJsDEJ38x/9QHR1FymBebmAvNzzbB JT45M4OYwynKE/mrFR1FPSeXA=X-QQ-SSF: 00010000000000F000000000000000ZX-HAS-ATTACH: noX-QQ-BUSINESS-ORIGIN: 2X-Originating-IP: 255.21.142.175X-QQ-STYLE: X-QQ-mid: webmail504t1587819163t7387219From: "=?gb18030?B?1/PRp7fG?=" <balloonwj@qq.com>To: "=?gb18030?B?dGVzdGZvcm15Ym9vaw==?=" <testformybook@163.com>Subject: =?gb18030?B?suLK1NPKvP4=?=Mime-Version: 1.0Content-Type: multipart/mixed; boundary="----=_NextPart_5EA4329B_0FBAC2B8_51634C9D"Content-Transfer-Encoding: 8BitDate: Sat, 25 Apr 2020 20:52:43 +0800X-Priority: 3Message-ID: <tencent_855A7727508F28D762951979338305E06B08@qq.com>X-QQ-MIME: TCMime 1.0 by TencentX-Mailer: QQMail 2.xX-QQ-Mailer: QQMail 2.xX-QQ-SENDSIZE: 520Received: from qq.com (unknown [127.0.0.1]) by smtp.qq.com (ESMTP) with SMTP id ; Sat, 25 Apr 2020 20:52:44 +0800 (CST)Feedback-ID: webmail:qq.com:bgweb:bgweb16X-CM-TRANSID:TcCowABHJo+dMqReI+72Bg--.18000S3Authentication-Results: mx27; spf=pass smtp.mail=balloonwj@qq.com; dki m=pass header.i=@qq.comX-Coremail-Antispam: 1Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73 VFW2AGmfu7bjvjm3AaLaJ3UbIYCTnIWIevJa73UjIFyTuYvjxU-LIDUUUUU
This is a multi-part message in MIME format.
-=_NextPart_5EA4329B_0FBAC2B8_51634C9DContent-Type: multipart/alternative; boundary="----=_NextPart_5EA4329B_0FBAC2B8_71508FA9";
-=_NextPart_5EA4329B_0FBAC2B8_71508FA9Content-Type: text/plain; charset="gb18030"Content-Transfer-Encoding: base64
1eLKx9K7t+Ky4srU08q8/qOsuqzT0MG9uPa4vbz+oaM=
-=_NextPart_5EA4329B_0FBAC2B8_71508FA9Content-Type: text/html; charset="gb18030"Content-Transfer-Encoding: base64
PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9R0IxODAzMCI+PGRpdj7V4srH0ru34rLiytTTyrz+o6y6rNPQwb249ri9vP6hozwvZGl2Pg==
-=_NextPart_5EA4329B_0FBAC2B8_71508FA9--
-=_NextPart_5EA4329B_0FBAC2B8_51634C9DContent-Type: application/octet-stream; charset="gb18030"; name="self.jpg"Content-Disposition: attachment; filename="self.jpg"Content-Transfer-Encoding: base64
/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEXAR8DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk3nOqryf+eEvH/jlX4PHfhIS(限於篇幅,省去部分內容...)F11n5mOMG3lwMf8AAKKKmOxDKeq+PPDtxPA0OuBIYz+8T7PL8/8A45Uj/ETwzDCWOqqV64EEv/xFFFO5q3eIknj7wy8AH9qrsccjyJeP/HKhsvGHhWyT93qwyflX9xLwP++KKKaqS5TFxVwvPiBoLRMbTUonkzgBoJQPr9ysXWvGGg30kVumpqoLh2byZP8A4iiikpNS0GtjpNM8c+FraHB1f/yBL/8AEVej+InhUf8AMWXGf+feX/4iiihslmVrPj3wzLGETVlPPeCX/wCIrMl8YeH5rdBDrCLInIzBL/8AEUUU7msNyRPGugi2/eaohbG5R5EnHt9ym3vjPw3JZAHUtp2hvlhk6/8AfFFFRzNNWJfvT1Kz+LvDctsBHqW0p0Bik/8AiKrr4v0HH/H+P+/Mn+FFFZz3Gf/Z
-=_NextPart_5EA4329B_0FBAC2B8_51634C9DContent-Type: application/octet-stream; charset="gb18030"; name="test.docx"Content-Disposition: attachment; filename="test.docx"Content-Transfer-Encoding: base64
UEsDBBQABgAIAAAAIQCshlBXjgEAAMAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA(限於篇幅,省去部分內容...)V5cAEAAOECAAARAAAAAAAAAAAAAAAAAHyyAABkb2NQcm9wcy9jb3JlLnhtbFBLAQItABQABgAIAAAAIQAKRdqw1gcAAGM8AAAPAAAAAAAAAAAAAAAAACO1AAB3b3JkL3N0eWxlcy54bWxQSwECLQAUAAYACAAAACEAISmu3xsCAADFBQAAEgAAAAAAAAAAAAAAAAAmvQAAd29yZC9mb250VGFibGUueG1sUEsBAi0AFAAGAAgAAAAhAMB9S4ZwAQAAxQIAABAAAAAAAAAAAAAAAAAAcb8AAGRvY1Byb3BzL2FwcC54bWxQSwUGAAAAAA0ADQBMAwAAF8IAAAAA
-=_NextPart_5EA4329B_0FBAC2B8_51634C9D--.我們如何去解析這樣的郵件格式呢?
這封郵件內容主要由兩部分組成,第一部分是「OK」關鍵字,第二部分是郵件內容,郵件內容以 點 + \r\n 結束。其中郵件內容中前面一部分是一個的 tag 和 tag 值,我們可以從這些 tag 中得到郵件的 MessageID、收件人姓名和地址、發件人姓名和地址、郵件主題,例如:
From: "=?gb18030?B?1/PRp7fG?=" <balloonwj@qq.com>To: "=?gb18030?B?dGVzdGZvcm15Ym9vaw==?=" <testformybook@163.com>Subject: =?gb18030?B?suLK1NPKvP4=?=Date: Sat, 25 Apr 2020 20:52:43 +0800Message-ID: <tencent_855A7727508F28D762951979338305E06B08@qq.com>其中像郵件的收發人姓名(From 和 To)使用了 base64 編碼,我們使用 base64 解碼即可還原其內容。
Content-Type: multipart/mixed; 說明郵件有多個部分組成。
我們先根據boundary="----=_NextPart_5EA4329B_0FBAC2B8_71508FA9";中指定的 ----=_NextPart_5EA4329B_0FBAC2B8_71508FA9 分隔符得到除了郵件附件內容外的郵件正文內容,一共有兩段:
正文段一
Content-Type: text/plain; charset="gb18030"Content-Transfer-Encoding: base64
1eLKx9K7t+Ky4srU08q8/qOsuqzT0MG9uPa4vbz+oaM=這段內容為純文本格式(text/plain),使用 base64 編碼,字符集格式為 gb18030,解碼之後得到正文即:
正文段二
Content-Type: text/html; charset="gb18030"Content-Transfer-Encoding: base64
PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9R0IxODAzMCI+PGRpdj7V4srH0ru34rLiytTTyrz+o6y6rNPQwb249ri9vP6hozwvZGl2Pg==這段內容為富文本格式(text/html),使用 base64 編碼,字符集格式為 gb18030,解碼之後得到正文即郵件中的那個帶超級連結的英語廣告,這是我使用的 163 郵件伺服器自動插入到郵件正文中的:
接下來就是兩個附件的內容了,使用的編碼格式也是 base64,我們使用 base64 解碼還原成 ASCII 字節流後作為文件的內容,再取 tag 中附件文件名生成對應的文件即可還原成附件內容。
完整的郵件解碼及 POP3 協議收發示例代碼請在【高性能伺服器開發】公眾號後臺回復關鍵字「POP3」。
上文我們介紹了 POP3 協議常用的命令,POP3 完整的命令讀者可以參考 rfc1939 文檔 https://tools.ietf.org/html/rfc1939。
郵件客戶端上面我們介紹了 POP3 和 SMTP 協議,IMAP 與此類似這裡就不再介紹了,讀者可以參考 rfc3501:https://tools.ietf.org/html/rfc3501 。
除了上面說的三種協議,郵件還有使用 Exchange 協議的,具體可以參考這裡:https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-asemail/f3d27369-e0f5-4164-aa5e-9b1abda16f5f。
在理解了上述郵件協議之後,我們就可以編寫自己的郵件客戶端了,且可以自由定製郵件展示功能(如上文中 163 郵箱在收到的郵件內部插入自定義英語廣告)。
本文中的郵件示例代碼我利用業餘時間前前後後大概寫了差不多兩周時間,歡迎讀者留言留下寶貴建議。