從零實現一個郵件收發客戶端

2021-12-22 高性能伺服器開發

與郵件收發有關的協議有 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)
#endif

Platform.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 郵箱在收到的郵件內部插入自定義英語廣告)。


本文中的郵件示例代碼我利用業餘時間前前後後大概寫了差不多兩周時間,歡迎讀者留言留下寶貴建議。

相關焦點

  • 可以正常收發 Gmail 郵件的方法和郵箱客戶端軟體應用
    因此,在國內怎樣能正常收發 Gmail 郵件成為很多人希望能解決的問題……Gmail 已不能打開也不能正常收發郵件目前 Gmail 網頁端徹底死翹翹了,POP、IMAP、SMTP 等協議間歇性通訊障礙,因此想要在電腦、手機上繼續訪問 Gmail 網頁版、使用官方 Gmail 客戶端、Google Inbox,或是用手機自帶的郵件客戶端收發 Gmail
  • 139郵箱,收發郵件更簡單
    小王的煩惱篇  搶註一個好記又個性的郵件名是大家都要做的事,然而很多人又或多或少經歷過因郵件名過長或繁瑣造成的不便。白領小王就經常為E-mail頭疼。小王每天都要收發數十封E-mail,公司的各種內部文件傳閱、辦公流程、各種報表收發全部通過郵件進行,和客戶公司的各種文件傳閱執行也都通過E-mail傳達,沒有電腦就無法開展工作。
  • YoMail:完美支持Gmail的電子郵件客戶端
    然而網友發現,網頁版Gmail和許多第三方應用都無法訪正常收發郵件,這個問題讓很多人困擾不已。雖然有一些技術手段可以解決「Gmail失聯」問題,然而這畢竟只是少數派,今天我們將介紹一款簡單的郵件應用——YoMail (www.yomail.com),輕鬆解決「Gmail失聯」問題。
  • IBC,真正實現安全郵件 您的郵件加密了嗎?請找奧聯科技!
    一般郵件洩密的途徑有:1、傳輸過程洩密:普通郵件傳輸的過程是明文的,可被很多技術手段攔截郵件的內容,如部署了上網行為管理網絡監控設備就可以攔截內部收發的郵件;基於SSL方式:此方式通過web收發郵件時候使用https協議;通過客戶端收發時使用SMTPS/POPS/IMAPS等協議。這雖然進行了一定保護,但仍然存在安全問題:1)可能被攻擊:由於兼顧用戶習慣,SSL收發基本都不驗證客戶端身份。
  • 網易郵箱大師客戶端代收Gmail郵件
    在眾多郵件客戶端中,不乏有設計精美,交互良好的軟體,而網易郵箱大師推出的Mac客戶端,則針對國內用戶,讓郵件客戶端更接地氣。
  • 微軟Hotmail支持任何郵箱帳號收發郵件
    微軟近日對其在線電子郵件服務Hotmail進行了又一次大規模升級,支持收發其他任何郵箱帳號的郵件,就像桌面郵件客戶端Outlook。微軟表示,這一功能的實現得益於Hotmail郵件架構的POP聚合支持,以及對郵件地址的存儲和查詢方式的相應調整。從現在起,任何郵箱地址經過註冊、認證後都可以在Hotmail郵箱內接收和發送相應的郵件。
  • 如何將客戶端郵件同步到webmail郵箱
    最近,很多朋友遇到這樣的情況,用郵件客戶端在本地郵件伺服器接收郵件後,未勾選副本保存,或是出差更換電腦後,相當部分人需要使用webmail來應對隨時隨地的郵件收發。而這時問題就出來了,webmail與郵件客戶端中的歷史郵件不能同步,找不到需要的郵箱,工作無法開展。我們如何將客戶端上的歷史郵件同步導入到webmail郵箱中呢?
  • 《明日之後》收發郵件方法攻略 怎麼發郵件
    導 讀 大家是不是都很疑惑明日之後怎麼收發郵件呢,遊戲中在哪裡可以看到自己收到的郵件呢,不清楚的小夥伴就來這裡了解一下吧
  • 電子郵箱客戶端如何綁定?郵箱客戶端如何設置?
    幾個同事用的TOM vip郵箱,本人也註冊了一個,由於平時項目比較多,用客戶端的時候比較多,之前不懂老覺得設置客戶端流程很繁瑣,今天給大家具體介紹下電子郵箱客戶端的設置流程,沒有操作過的做個了解吧~
  • 四個開源的 Android 郵件客戶端 | Linux 中國
    現在一些年輕人正將郵件稱之為「老年人的交流方式」,然而事實卻是郵件絕對還沒有消亡。雖然協作工具[1]、社交媒體,和簡訊很常用,但是它們還沒做好取代郵件這種必要的商業(和社交)通信工具的準備。考慮到郵件還沒有消失,並且(很多研究表明)人們都是在行動裝置上閱讀郵件,擁有一個好的移動郵件客戶端就變得很關鍵。
  • 新生有郵箱了,阿D教你一鍵收發所有郵件!
    有關事項通知如下:一、郵箱登錄方式1.學校主頁右上方提供「郵件系統」連結,登錄時記得選擇「@m.gduf.edu.cn」。2. 騰訊企業郵箱登錄地址 http://exmail.qq.com/login,登錄時請輸入完整的郵箱帳號(包含@後面的部分)。
  • 使用企業郵箱如何綁定客戶端?
    首選介紹一下什麼是企業郵箱客戶端,這個客戶端和大家平時接觸的APP等類似,就是一個綁定企業郵箱的軟體,分為電腦版客戶端和手機版客戶端。它的主要作用就是通過這個軟體可以把企業郵箱綁定上去,通過這個客戶端實現郵件的接收送達等功能。
  • 怎樣設置OUTLOOK來收發郵件?
    設置Outlook Express軟體來收發郵件以中文版Outlook Express 6為例打開Outlook Express後,單擊窗口中的「工具」菜單,選擇「帳戶點擊「郵件」標籤,點擊右側的「添加」按鈕,在彈出的菜單中選擇「郵件」;彈出的對話框中
  • 常用郵箱客戶端設置指南
    大部分電子郵件供應商都提供了網頁端登錄界面,或者和自有的手機端app結合的功能,但在通用的郵件客戶端如Outlook或者手機端自帶的及第三方郵件終端中配置郵件,仍然是一項非常重要的工作,可以免去每次收發郵件都要打開網頁的煩惱。由於「配置郵件客戶端」越來越像是一門失傳已久的「絕學」,郵件服務商的文檔往往不夠完整,因此,有必要整理一下常用郵箱的客戶端設置方式:1.
  • 網易郵箱如何收發加密郵件
    郵件加密可以有助於保護信件的安全,網易郵箱是如何實現收發加密郵件的呢?下面給大家介紹。打開郵箱登錄界面,輸入用戶名和密碼,單擊「登錄」。進入郵箱後,單擊「寫信」。正常寫信完成後,單擊「更多選項」(如下圖所示)。
  • 拒絕海外郵件收發不暢 郵件專家教你選購企業郵箱
    可是如果在展會上好不容易談下意向訂單,卻因為電子郵件海外收發不暢而丟失客戶,就得不償失了。「海外客戶老說沒收到我的郵件」,一位從事外貿業務的網友飛飛抱怨公司郵箱不穩定,以致她偶爾就丟單。  電子郵件一直是外貿企業與海外客戶使用頻率最高的通信工具之一,然而一些國內企業郵箱發往海外的郵件經常會出現退信、丟信的情況,很讓企業抓狂。
  • 中文電子郵件地址啟動 將可用中文地址收發郵件
    中文電子郵件地址啟動 將可用中文地址收發郵件 P迪 | 2008-09-26 8:52:33 | 科技資訊
  • 企業郵箱能綁定微信收發郵件?
    下面聊聊企業郵箱如何綁定微信收發郵件吧?首先,得申請個能在微信收發郵件的企業郵箱,除了騰訊自帶產品,當屬TOM企業郵箱啦!就拿它來描述綁定流程吧~首先也明確下,微信收發郵箱和手機客戶端還是不一樣的,至少不需要設置複雜的綁定流程,而且新郵件實時推送到微信上,平時看微信就能順便看看,有沒有大客戶的郵件來了,而且使用時非常節省流量,增加了更多很實用的小功能。
  • 在 Linux 命令行中收發 Gmail 郵件 | Linux 中國
    我喜歡在 Linux 終端上讀寫電子郵件的便捷,因此我是 Mutt 這個輕量簡潔的電子郵件客戶端的忠實用戶。對於電子郵件服務來說,不同的系統配置和網絡接入並不會造成什麼影響。這個客戶端通常隱藏在我 Linux 終端的 某個標籤頁或者某個終端復用器的面板 上,需要用的時候隨時可以調出來,不需要使用的時候放到後臺,就不需要在桌面上一直放置一個電子郵件客戶端的應用程式。
  • 收發200封電子郵件,等於種下一棵樹
    製圖/楊濱瑞  黃菲菲 李傳新  8月4日上午,在媒體工作的網友ZJOLORG,一上班打開郵箱,發現網易郵件中心發來了一封特殊的郵件,主題是「原來你為環保做了這麼多他馬上點開郵件:「截至本月您使用網易郵箱已經8年了,或許8年來通過電子郵箱收發信件已經成為一種習慣,但您不知道,您的每一次收發,都在為我們的生活新增一抹綠:收發200封郵件≈親手種下一棵樹。」  「2009年至今您共收到1359封郵件,發出144封郵件,≈種下了8棵樹。」  網友ZJOLORG感慨:「沒想到我收發郵件還為環保作了貢獻啊!」