6.12 SMTP、POP3 协议与邮件客户端
# 6.12.1 邮件协议概况
与邮件收发有关的协议有 POP3、SMPT 和 IMAP 等。
POP3 全称是 Post Office Protocol 3 ,即邮局协议的第 3 个版本,它规定怎样将个人计算机连接到 Internet 的邮件服务器和下载电子邮件的电子协议,它是因特网电子邮件的第一个离线协议标准,POP3 允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时删除保存在邮件服务器上的邮件,而 POP3 服务器则是遵循 POP3 协议的接收邮件服务器,用来接收电子邮件的。
SMTP
SMTP 的全称是 Simple Mail Transfer Protocol,即简单邮件传输协议。它是一组用于从源地址到目的地址传输邮件的规范,它帮助每台计算机在发送或中转邮件时找到下一个目的地。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免费邮箱相关服务器信息:
POP3、SMTP、IMAP 协议是我们前面介绍的、使用指定字符(串)作为包结束标志的典型例子。我们来以 SMTP 协议和 POP3 协议为例来讲解一下。
# 6.12.2 SMTP 协议
先来介绍 SMTP 协议吧,SMTP 全称是 Simple Mail Transfer Protocol,即简单邮件传输协议,该协议用于发送邮件。
SMTP 协议的格式:
关键字 自定义内容\r\n
“自定义内容”根据“关键字”的类型选择是否设置,对于使用 SMTP 作为客户端的一方常用的“关键字“如下所示:
//连接上邮件服务器之后登录服务器之前向服务器发送的问候信息
HELO 自定义问候语\r\n
//请求登录邮件服务器
AUTH LOGIN\r\n
base64形式的用户名\r\n
base64形式的密码\r\n
//设置发件人邮箱地址
MAIL FROM:发件人地址\r\n
//设置收件人地址,每次发送可设置一个收件人地址,如果有多个收件地址,要分别设置对应次数
rcpt to:收件人地址\r\n
//发送邮件正文开始标志
DATA\r\n
//发送邮件正文,注意邮件正文以.\r\n结束
邮件正文\r\n.\r\n
//登出服务器
QUIT\r\n
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用 SMTP 作为邮件服务器的一方常用的“关键字“是定义的各种应答码,应答码后面可以带上自己的信息,然后以\r\n作为结束,格式如下:
应答码 自定义消息\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 传输失败
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
更多的 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;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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);
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);
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*/)
{
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
然后我们使用另外一个类 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; //运行标志
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
52
53
54
55
56
57
58
59
60
61
62
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;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
程序中另外用到的几个辅助文件Base64Util.h、Base64Util.cpp、Platform.h、Platform.cpp读者可通过随书代码资源获取。
我们在 main 函数模拟产生一条新的报警邮件:
main.cpp
/**
* 邮件报警demo
* zhangyl 2020.04.09
**/
#include <iostream>
#include <stdlib.h>
#include "Platform.h"
#include "MailMonitor.h"
//Winsock网络库初始化
#ifdef WIN32
NetworkInitializer windowsNetworkInitializer;
#endif
#ifndef WIN32
void 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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
52
53
54
55
56
57
58
上述代码使用了 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;
}
2
3
4
5
6
7
8
产生第二封邮件则是我们在 main 函数中主动调用产生报警邮件的函数:
const std::string subject = "Alert Mail";
const std::string content = "This is an alert mail from " + mailuser;
MailMonitor::getInstance().alert(subject, content);
2
3
我们以第一封邮件为例来说明整个邮件发送过程中,我们的程序(客户端)与 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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这个函数先创建 socket,再使用指定的地址和端口号去连接服务器(smtpSocket.connect() 函数内部额外做了一步将域名解析成 ip 地址的工作),连接成功后开始和服务器端进行数据交换:
client: 尝试连接服务器
client: 连接成功
server: 220\r\n
client: helo 自定义问候语\r\n
server: 250\r\n
client: AUTH LOGIN\r\n
server: 334\r\n
client: base64编码后的用户名\r\n
server: 334\r\n
client: base64编码后的密码\r\n
server: 235\r\n
client: MAIL FROM:<发件人地址>\r\n
server: 250\r\n
client: rcpt to:<收件人地址1>\r\n
server: 250\r\n
client: rcpt to:<收件人地址2>\r\n
server: 250\r\n
client: DATA\r\n
server: 354\r\n
client: 邮件正文\r\n.\r\n
server: 250\r\n
client:QUIT\r\n
server:221\r\n
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
我们将上述过程绘制成如下示意图:
最终邮件就发出去了,这里我们模拟了客户端使用 smtp 协议给服务器端发送邮件,我们自己实现服务器端接收客户端发送的邮件请求也是一样的道理。这就是 SMTP 协议的格式,SMTP 协议是以特定标记作为分隔符的协议格式典型。
读者可以在 Windows 上或者 Linux 主机上测试上述程序,如果读者在阿里云这样的云主机上测试,阿里云等云主机为了避免在网络上产生大量垃圾邮件默认是禁止发往其他服务器的 25 号端口的数据的,读者需要申请解除该端口限制,或者将邮件服务器的 25 端口改成其他端口(一般改成 465 端口)。
上文我们介绍了 SMTP 协议常用的协议命令,SMTP 协议支持的完整命令列表读者可以参考 rfc5321 文档。
# 6.12.3 POP3 协议
我们再来看下 POP3 协议
client:尝试连接邮箱pop服务器,连接成功
server:+OK Welcome to coremail Mail Pop3 Server (163coms[10774b260cc7a37d26d71b52404dcf5cs])\r\n
client:USER 用户名\r\n
server:+OK core mail
client:PASS 密码\r\n
server:+OK 202 message(s) [3441786 byte(s)]\r\n
client:LIST\r\n
server:+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\n
server:
+OK 4108 octets\r\n
Received: 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\n
DKIM-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\n
b=OrAQTs0GJnA
...省略部分内容...
6Mzu2lmr07WwMCE7wgqwOSWRnYNCz2rWcLmXA_TVDtdJ85
bHZ79FY6Vs5pGJjp.7YgDnVqysBp95w--\r\n
Received: 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\n
Date: Tue, 3 Mar 2020 17:56:49 +0000 (UTC)\r\n
From: Peter Edward Copley <noodlelife@yahoo.com>\r\n
Reply-To: Peter Edward Copley <pshun3592@gmail.com>\r\n
Message-ID: <729348196.5391236.1583258209467@mail.yahoo.com>\r\n
Subject: Re:Hello\r\n
MIME-Version: 1.0\r\n
Content-Type: multipart/alternative; \r\n
boundary="----=_Part_5391235_1821490954.1583258209466"\r\n
References: <729348196.5391236.1583258209467.ref@mail.yahoo.com>\r\n
X-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\n
X-CM-TRANSID:T8CowABHlztmml5erAoHAQ--.23443S3\r\n
Authentication-Results: mx29; spf=pass smtp.mail=noodlelife@yahoo.com;\r\n
dkim=pass header.i=@yahoo.com\r\n
X-Coremail-Antispam: 1Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73
VFW2AGmfu7bjvjm3AaLaJ3UbIYCTnIWIevJa73UjIFyTuYvjxU-NtIUUUUU\r\n
\r\n
------=_Part_5391235_1821490954.1583258209466\r\n
.\r\n
client:QUIT\r\n
server:+OK core mail\r\n
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
上述过程如下示意图所示:
当我们收取邮件正文之后,我们就可以根据邮件正文中的各种 tag 来解析邮件内容得到邮件的 MessageID、收件人、发件人、邮件主题、正文和附件,注意附件内容也会被拆成特定的编码格式放在邮件中。邮件正文里面按所谓的 boundary 来分成多个块,例如上文中的 boundary="----=_Part_5391235_1821490954.1583258209466"
。
我们来看一个具体的例子吧。
上述邮件的主题是“测试邮件”,内容是纯文本“这是一封测试邮件,含有两个附件。”,还有两个附件,一张名为 self.jpg 的图片,一个名为 test.docx 的文档。我们将邮件下载下来后得到邮件原文是:
+OK 93763 octets
Received: 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: 00010000000000F000000000000000Z
X-HAS-ATTACH: no
X-QQ-BUSINESS-ORIGIN: 2
X-Originating-IP: 255.21.142.175
X-QQ-STYLE:
X-QQ-mid: webmail504t1587819163t7387219
From: "=?gb18030?B?1/PRp7fG?=" <balloonwj@qq.com>
To: "=?gb18030?B?dGVzdGZvcm15Ym9vaw==?=" <testformybook@163.com>
Subject: =?gb18030?B?suLK1NPKvP4=?=
Mime-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_NextPart_5EA4329B_0FBAC2B8_51634C9D"
Content-Transfer-Encoding: 8Bit
Date: Sat, 25 Apr 2020 20:52:43 +0800
X-Priority: 3
Message-ID: <tencent_855A7727508F28D762951979338305E06B08@qq.com>
X-QQ-MIME: TCMime 1.0 by Tencent
X-Mailer: QQMail 2.x
X-QQ-Mailer: QQMail 2.x
X-QQ-SENDSIZE: 520
Received: 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:bgweb16
X-CM-TRANSID:TcCowABHJo+dMqReI+72Bg--.18000S3
Authentication-Results: mx27; spf=pass smtp.mail=balloonwj@qq.com; dki
m=pass header.i=@qq.com
X-Coremail-Antispam: 1Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73
VFW2AGmfu7bjvjm3AaLaJ3UbIYCTnIWIevJa73UjIFyTuYvjxU-LIDUUUUU
This is a multi-part message in MIME format.
------=_NextPart_5EA4329B_0FBAC2B8_51634C9D
Content-Type: multipart/alternative;
boundary="----=_NextPart_5EA4329B_0FBAC2B8_71508FA9";
------=_NextPart_5EA4329B_0FBAC2B8_71508FA9
Content-Type: text/plain;
charset="gb18030"
Content-Transfer-Encoding: base64
1eLKx9K7t+Ky4srU08q8/qOsuqzT0MG9uPa4vbz+oaM=
------=_NextPart_5EA4329B_0FBAC2B8_71508FA9
Content-Type: text/html;
charset="gb18030"
Content-Transfer-Encoding: base64
PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNo
YXJzZXQ9R0IxODAzMCI+PGRpdj7V4srH0ru34rLiytTTyrz+o6y6rNPQwb249ri9vP6hozwv
ZGl2Pg==
------=_NextPart_5EA4329B_0FBAC2B8_71508FA9--
------=_NextPart_5EA4329B_0FBAC2B8_51634C9D
Content-Type: application/octet-stream;
charset="gb18030";
name="self.jpg"
Content-Disposition: attachment; filename="self.jpg"
Content-Transfer-Encoding: base64
/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRof
限于篇幅,这里省去部分内容
RzNNWJfvT1Kz+LvDctsBHqW0p0Bik/8AiKrr4v0HH/H+P+/Mn+FFFZz3Gf/Z
------=_NextPart_5EA4329B_0FBAC2B8_51634C9D
Content-Type: application/octet-stream;
charset="gb18030";
name="test.docx"
Content-Disposition: attachment; filename="test.docx"
Content-Transfer-Encoding: base64
UEsDBBQABgAIAAAAIQCshlBXjgEAAMAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIo
(限于篇幅,省去部分内容...)
AQAAxQIAABAAAAAAAAAAAAAAAAAAcb8AAGRvY1Byb3BzL2FwcC54bWxQSwUGAAAAAA0ADQBM
AwAAF8IAAAAA
------=_NextPart_5EA4329B_0FBAC2B8_51634C9D--
.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
我们如何去解析这样的邮件格式呢?
这封邮件内容主要由两部分组成,第一部分是“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 +0800
Message-ID: <tencent_855A7727508F28D762951979338305E06B08@qq.com>
2
3
4
5
其中像邮件的收发人姓名(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=
2
3
4
5
这段内容为纯文本格式(text/plain),使用 base64 编码,字符集格式为 gb18030,解码之后得到正文即:
正文段二
Content-Type: text/html;
charset="gb18030"
Content-Transfer-Encoding: base64
PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNo
YXJzZXQ9R0IxODAzMCI+PGRpdj7V4srH0ru34rLiytTTyrz+o6y6rNPQwb249ri9vP6hozwv
ZGl2Pg==
2
3
4
5
6
7
这段内容为富文本格式(text/html),使用 base64 编码,字符集格式为 gb18030,解码之后得到正文即邮件中的那个带超级链接的英语广告,这是我使用的 163 邮件服务器自动插入到邮件正文中的:
接下来就是两个附件的内容了,使用的编码格式也是 base64,我们使用 base64 解码还原成 ASCII 字节流后作为文件的内容,再取 tag 中附件文件名生成对应的文件即可还原成附件内容。
上文我们介绍了 POP3 协议常用的命令,POP3 完整的命令读者可以参考 rfc1939 文档。
# 6.12.4 邮件客户端
上面我们介绍了 POP3 和 SMTP 协议,IMAP 与此类似这里就不再介绍了,读者可以参考 rfc3501文档。
除了上面说的三种协议,邮件还有使用 Exchange 协议的,读者可以参考这里 Microsoft 官方关于 Exchange 协议的说明文档。
在理解了上述邮件协议之后,我们就可以编写自己的邮件客户端了,且可以自由定制邮件展示功能(如上文中 163 邮箱在收到的邮件内部插入自定义英语广告)。