|
前段時間事情太多了,忙著寫畢業(yè)論文,考試,然后又被抽到了盲審,不過好在有驚無險,最后也在學(xué)院提交三月中旬申請答辯成功,如果盲審順利的話,4月份就可以畢業(yè)了。不過這段時間總算可以看代碼、看書了,感覺自己操作系統(tǒng)方面有些不扎實,索性買了本孫鐘秀的《操作系統(tǒng)教程》看,之后順便閱讀和分析了jwSMTP源碼,這里寫篇文章記錄下。本文不想對代碼細(xì)節(jié)作太多分析,因為代碼比較好讀,并且文章末尾我會放出我注釋過的源碼鏈接,所以此文多介紹下原理吧。
jwSMTP
jwSMTP是一個由C++編寫的發(fā)送郵件的庫,支持Linux、Windows等平臺。可使用HTML或純文本方式發(fā)送郵件。也可添加附件,支持多個收件人。并且支持LOGIN和PLAIN兩種服務(wù)器驗證方式。
兩種調(diào)用方式
第一種方式
|
1
2
3
4
5
6
7
|
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
|
主要區(qū)別是一個查詢MX record,一個不查詢MX record,直接發(fā)送給SMTP Server。
base64編碼
Base64是網(wǎng)絡(luò)上最常見的用于傳輸8Bit字節(jié)代碼的編碼方式之一,設(shè)計此種編碼是為了使二進制數(shù)據(jù)可以通過非純8bit的傳輸層傳輸,Base64編碼可用于在HTTP環(huán)境下傳遞較長的標(biāo)識信息,另一方面,采用Base64編碼具有不可讀性,即所編碼的數(shù)據(jù)不會被人用肉眼所直接看到。
電子郵件的主題,MIME都會用到base64編碼。我們現(xiàn)在說下其原理:
Base64編碼方法:
-
base64的編碼都是按字符串長度,以每3個8bit的字符為一組,然后針對每組,首先獲取每個字符的ASCII編碼,然后將ASCII編碼轉(zhuǎn)換成8bit的二進制,得到一組3*8=24bit的字節(jié),然后再將這24bit劃分為4個6bit的字節(jié),并在每個6bit的字節(jié)前面都填兩個高位0,得到4個8bit的字節(jié),然后將這4個8bit的字節(jié)轉(zhuǎn)換成10進制,對照Base64編碼表,得到對應(yīng)編碼后的字符。
-
不是3的整數(shù)倍的,需要補齊而出現(xiàn)的0,轉(zhuǎn)化成十進制的時候就不能按常規(guī)用base64編碼表來對應(yīng),可以理解成為一種特殊的“異?!保幋a應(yīng)該對應(yīng)“=”。
代碼base64.cpp/base64.h是對Base64編碼的實現(xiàn),更多原理請參考下參考鏈接關(guān)于base64編碼的原理與實現(xiàn)一文。打開一封Email,查看其原始信息,一般為如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Date: Thu, 25 Dec 2014 06:33:07 +0800
To: "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
|
程序流程
程序一般為先設(shè)置發(fā)件人信息,之后設(shè)置收件人信息,對應(yīng)的函數(shù)為setsender()和addrecipient()函數(shù),此處沒什么可說的。之后是setmessage/setmessageHTML函數(shù),兩者的主要區(qū)別是不是需要base64編碼,方法前面已說,此處主要說下checkRFCcompat()函數(shù),此函數(shù)主要功能是:將消息結(jié)尾改為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
}
}
}
}
|
此處是根據(jù)RFC2821(SMTP協(xié)議)的4.5.2 Transparency編寫的,內(nèi)容為下:
-
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.
-
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小節(jié)。
之后的一些setsubject、setserver、addrecipent等等函數(shù),都不做解釋了,都是用來添加/刪除主機、設(shè)置服務(wù)器、增加/刪除收件人列表相關(guān)的,很好明白。我們重點說下郵件發(fā)送函數(shù)send()里的operator()()函數(shù),如果lookupMXRecord為真,就調(diào)用gethostaddresses()函數(shù),用來查詢MX記錄,這涉及到DNS協(xié)議相關(guān)知識,請查DNS小節(jié)。如果為fasle,則直接連接SMTP
server,operator()()和makesmtpmessage()函數(shù)主要是完成了如下流程(并不完全一致,僅參考):
|
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服務(wù)器,協(xié)議規(guī)定SMTP服務(wù)器的端口號為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 (協(xié)議規(guī)定的握手過程,格式為HELO + 服務(wù)器名稱)
S: 250 OK (250 表示握手成功)
C: AUTH LOGIN (AUTH LOGIN 是用戶登錄命令)
S: 334 dXNlcm5hbWU6 (334表示服務(wù)器接受)
C: tommy_mail (輸入明文用戶名)
S: 535 Error: authentication failed
(服務(wù)器拒絕,因為SMTP要求用戶名和密碼都通過64位編碼后再發(fā)送)
C: AUTH LOGIN (重新要求SMTP登錄)
S: 334 dXNlcm5hbWU6
C: dG9tb*****FpbA== (用編碼后的內(nèi)容發(fā)送)
S: 334 UGFzc3dvcmQ6 (334表示接受)
C: ********aXZldXA= (編碼后的密碼)
S: 235 Authentication successful (235 登錄成功)
C: MAIL FROM: (MAIL
FROM: <>格式,用來記錄發(fā)送者)
S: 250 Mail OK (250 系統(tǒng)常用確認(rèn)信息)
C: RCPT TO: (接收者郵箱,可多次使用以實現(xiàn)發(fā)送給多個人)
S: 250 Mail OK
C: DATA (DATA明令表示以下為郵件正文)
S: 354 End data with .
C: TO:11@11
(發(fā)送給誰,這里可自由撰寫,也是偽造郵件的一個入口,欺騙一般人可以,但會讀源碼的人欺騙不了)
FROM:22@22 (發(fā)送者是誰,可串改)
SUBJECT:TEST MAIL SMTP (郵件主題)
hello world (空一行寫郵件正文)
. (正文以.結(jié)束)
S: 250 Mail OK queued as smtp3,DdGowLBLAjqD6_JIg1hfBA==.63235S2
1223879684 (服務(wù)器接受)
C: noop (空操作,延遲退出時間)
S: 250 OK
C: quit (退出SMTP服務(wù)器連接)
S: 221 Bye
|
DNS協(xié)議
調(diào)用gethostaddresses()函數(shù),用來查詢MX記錄,這涉及到DNS協(xié)議相關(guān)知識,本函數(shù)可以使用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服務(wù)器的53端口發(fā)送UDP報文,DNS服務(wù)器收到后進行處理,并把結(jié)果記錄仍以UDP報文的形式返回過來。除了報文頭是固定的12字節(jié)外,其他每一部分的長度均為不定字節(jié)數(shù)。我們只關(guān)心的是報文頭、問題、回答這三個部分
DNS的協(xié)議為rfc1035,但是枯燥難懂,可以查看參考鏈接的DNS消息格式,比較容易理解。
|
1
|
unsigned char dns[512] = {1,1, 1,0, 0,1, 0,0, 0,0, 0,0};
|
比如此處即為DNS Header消息頭部信息。
之后幾段代碼是請求部分格式,代碼里我已詳細(xì)注釋,之后發(fā)送請求,解析應(yīng)答即可。
SMTP驗證方式
來自 “ ITPUB博客 ” ,鏈接:http://blog./30118448/viewspace-1415975/,如需轉(zhuǎn)載,請注明出處,否則將追究法律責(zé)任。
|