jwSMTP源码剖析

前段时间事情太多了,忙着写毕业论文,考试,然后又被抽到了盲审,不过好在有惊无险,最后也在学院提交三月中旬申请答辩成功,如果盲审顺利的话,4月份就可以毕业了。不过这段时间总算可以看代码、看书了,感觉自己操作系统方面有些不扎实,索性买了本孙钟秀的《操作系统教程》看,之后顺便阅读和分析了jwSMTP源码,这里写篇文章记录下。本文不想对代码细节作太多分析,因为代码比较好读,并且文章末尾我会放出我注释过的源码链接,所以此文多介绍下原理吧。

jwSMTP

jwSMTP是一个由C++编写的发送邮件的库,支持Linux、Windows平台。可使用HTML或纯文本方式发送邮件。也可添加附件,支持多个收件人。并且支持LOGIN和PLAIN两种服务器验证方式。

两种调用方式

第一种方式

1
2
3
4
5
6
7
8
mailer mail(“myfriend@friend.com”, // who the mail is too
“someone@somewhere.net”, // who the mail is from
“There is always room for FooBar”, // subject for the email
“Foo\nBar”, // content of the message
“ns.somewhere.net”); // the nameserver to contact
// to query for an MX record
mail.send( );

第二种方式

1
2
3
4
5
6
7
8
mailer mail(“myfriend@friend.com”, // who the mail is too
“someone@somewhere.net”, // who the mail is from
“There is always room for FooBar”, // subject for the email
vec, // content of the message
“mail.somewhere.net”, // the smtp server to mail to
mailer::SMTP_PORT, // default smtp port (25)
false); // do not query MX records,
// mail directly to mail.somewhere.net

主要区别是一个查询MX record,一个不查询MX record,直接发送给SMTP Server。

base64编码

Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,设计此种编码是为了使二进制数据可以通过非纯8bit的传输层传输,Base64编码可用于在HTTP环境下传递较长的标识信息,另一方面,采用Base64编码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。
电子邮件的主题,MIME都会用到base64编码。我们现在说下其原理:

Base64编码方法:

  • base64的编码都是按字符串长度,以每3个8bit的字符为一组,然后针对每组,首先获取每个字符的ASCII编码,然后将ASCII编码转换成8bit的二进制,得到一组3*8=24bit的字节,然后再将这24bit划分为4个6bit的字节,并在每个6bit的字节前面都填两个高位0,得到4个8bit的字节,然后将这4个8bit的字节转换成10进制,对照Base64编码表,得到对应编码后的字符。
  • 不是3的整数倍的,需要补齐而出现的0,转化成十进制的时候就不能按常规用base64编码表来对应,可以理解成为一种特殊的“异常”,编码应该对应“=”。
    代码base64.cpp/base64.h是对Base64编码的实现,更多原理请参考下参考链接关于base64编码的原理与实现一文。打开一封Email,查看其原始信息,一般为如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Date: Thu, 25 Dec 2014 06:33:07 +0800 
    From: A<A@yeah.net>
    To: "B"<b@126.com>
    Subject:
    X-mailer: Foxmail 5.0 beta2 [cn]
    Mime-Version: 1.0
    Content-Type: text/plain;
    charset="gb2312"
    Content-Transfer-Encoding: base64

    xOO6w6OsU25haVgNCg0KoaGhodXiysfSu7j2QmFzZTY0tcSy4srU08q8/qOhDQoNCkJlc3QgV2lz
    aGVzIQ0KIAkJCQkNCqGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaEgICAgICAgICAgICAgICBl
    U1g/IQ0KoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoSAgICAgICAgICAgICAgIHNuYWl4QHll
    YWgubmV0DQqhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhICAgICAgICAgMjAwMy0x
    Mi0yNQ0K

    程序流程

    程序一般为先设置发件人信息,之后设置收件人信息,对应的函数为setsender()和addrecipient()函数,此处没什么可说的。之后是setmessage/setmessageHTML函数,两者的主要区别是不是需要base64编码,方法前面已说,此处主要说下checkRFCcompat()函数,此函数主要功能是:将消息结尾改为CRLF(\r\n)形式,之后注意此处:
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
if(message.size() == 1) {
if(*(message.begin()) == '.')
message.push_back('.');
}
else if(message.size() == 2) {
if(*(message.begin()) == '.') {
it = message.begin();
it = message.insert(it, '.');
}
}
else {
if(*(message.begin()) == '.') {
it = message.begin();
it = message.insert(it, '.');
}
for(it = message.begin()+2; it != message.end(); ++it) {
// follow the rfc. Add '.' if the first character on a line is '.'
if(*it == '\n') {
if( ((it + 1) != message.end()) && (*(it +1) == '.') ) {
it = message.insert(it + 1, '.');
++it; // step past
}
}
}
}

此处是根据RFC2821(SMTP协议)的4.5.2 Transparency编写的,内容为下:

  1. Before sending a line of mail text, the SMTP client checks the first character of the line. If it is a period, one additional period is inserted at the beginning of the line.

  2. When a line of mail text is received by the SMTP server, it checks the line. If the line is composed of a single period, it is treated as the end of mail indicator. If the first character is a period and there are other characters on the line, the first character is deleted.

然后就是每一行消息不能超过1000个字符,见RFC2821的text line小节。

之后的一些setsubject、setserver、addrecipent等等函数,都不做解释了,都是用来添加/删除主机、设置服务器、增加/删除收件人列表相关的,很好明白。我们重点说下邮件发送函数send()里的operator()()函数,如果lookupMXRecord为真,就调用gethostaddresses()函数,用来查询MX记录,这涉及到DNS协议相关知识,请查DNS小节。如果为fasle,则直接连接SMTP server,operator()()和makesmtpmessage()函数主要是完成了如下流程(并不完全一致,仅参考):

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
26
27
28
29
30
31
32
33
34
C: telnet smtp.163.com 25 (连接到163的SMTP服务器,协议规定SMTP服务器的端口号为25
S: Trying 202.108.5.83...
Connected to smtp.163.split.netease.com.
Escape character is '^]'.
220 163.com Anti-spam GT for Coremail System (163com[071018]) (220 表示连接成功)
C: HELO smtp.163.com (协议规定的握手过程,格式为HELO + 服务器名称)
S: 250 OK (250 表示握手成功)
C: AUTH LOGIN (AUTH LOGIN 是用户登录命令)
S: 334 dXNlcm5hbWU6 (334表示服务器接受)
C: tommy_mail (输入明文用户名)
S: 535 Error: authentication failed (服务器拒绝,因为SMTP要求用户名和密码都通过64位编码后再发送)
C: AUTH LOGIN (重新要求SMTP登录)
S: 334 dXNlcm5hbWU6
C: dG9tb*****FpbA== (用编码后的内容发送)
S: 334 UGFzc3dvcmQ6 (334表示接受)
C: ********aXZldXA= (编码后的密码)
S: 235 Authentication successful (235 登录成功)
C: MAIL FROM:<A@163.com> (MAIL FROM:<>格式,用来记录发送者)
S: 250 Mail OK (250 系统常用确认信息)
C: RCPT TO:<B@126.com> (接收者邮箱,可多次使用以实现发送给多个人)
S: 250 Mail OK
C: DATA (DATA明令表示以下为邮件正文)
S: 354 End data with <CR><LF>.<CR><LF>
C: TO:11@11 (发送给谁,这里可自由撰写,也是伪造邮件的一个入口,欺骗一般人可以,但会读源码的人欺骗不了)
FROM:22@22 (发送者是谁,可串改)
SUBJECT:TEST MAIL SMTP (邮件主题)

hello world (空一行写邮件正文)
. (正文以.结束)
S: 250 Mail OK queued as smtp3,DdGowLBLAjqD6_JIg1hfBA==.63235S2 1223879684 (服务器接受)
C: noop (空操作,延迟退出时间)
S: 250 OK
C: quit (退出SMTP服务器连接)
S: 221 Bye

DNS协议

调用gethostaddresses()函数,用来查询MX记录,这涉及到DNS协议相关知识,本函数可以使用nslookup命令模拟,我本地模拟如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
imlinuxer@imlinuxer:~$ nslookup 
> set type=mx
> mail.qq.com
Server: 127.0.1.1
Address: 127.0.1.1#53

Non-authoritative answer:
*** Can't find mail.qq.com: No answer

Authoritative answers can be found from:
mail.qq.com
origin = qq.com
mail addr = webmaster.qq.com
serial = 1186990741
refresh = 300
retry = 600
expire = 86400
minimum = 86400

DNS查询的过程一般是:客户向DNS服务器的53端口发送UDP报文,DNS服务器收到后进行处理,并把结果记录仍以UDP报文的形式返回过来。除了报文头是固定的12字节外,其他每一部分的长度均为不定字节数。我们只关心的是报文头、问题、回答这三个部分
DNS的协议为rfc1035,但是枯燥难懂,可以查看参考链接的DNS消息格式,比较容易理解。

1
2
unsigned char dns[512] = {1,1,   1,0,      0,1,      0,0, 0,0, 0,0}; 

比如此处即为DNS Header消息头部信息。
之后几段代码是请求部分格式,代码里我已详细注释,之后发送请求,解析应答即可。

SMTP验证方式

比较简单,原理见此:SMTP验证方式种类(LOGIN、PLAIN、CRAM-MD5)
jwSMTP代码只实现了两种验证方式:LOGIN和PLAIN。

说的有点多了,感觉很多原理都解释了,逻辑稍微有一点混乱,主要是自己不是那么擅长组织语言,如果读者有兴趣,可以多了解下原理,知道SMTP和DNS原理,基本上代码就不需要多看了。

最后扔出我的中文源码剖析代码,在Github上:

https://github.com/armsword/Source/tree/master/jwSMTP

参考链接

关于base64编码的原理与实现
POP3 SMTP协议分析学习笔记
DNS消息格式

目录

  1. 1. jwSMTP
  2. 2. 两种调用方式
    1. 2.1. 第一种方式
    2. 2.2. 第二种方式
  3. 3. base64编码
  4. 4. 程序流程
  5. 5. DNS协议
  6. 6. SMTP验证方式
  7. 7. 参考链接