1. 程式人生 > >smtp互動過程

smtp互動過程

事先宣告,整個過程以LOGIN認證方式為例,其他認證方式大同小異。按照時間順序,主要分為22個步驟。

1、客戶端TCP連線伺服器25埠;

2、三次握手以後,連線建立成功,伺服器主動推送服務就緒資訊

網易郵箱一般都形如“220 163.com Anti-spam GT for Coremail System (163com[20111010])”;雅虎郵箱形如“220 smtp108.mail.gq1.yahoo.com ESMTP”;Google郵箱形如“220 mx.google.com ESMTP nw8sm917193igc.7”。其中220代表服務就緒,每一條服務就緒資訊以“\r\n”為結尾標示符。    

3、客戶端向伺服器說明身份

交代自己認證SMTP伺服器的域名,例如雅虎郵箱的SMTP伺服器為smtp.mail.yahoo.com,則傳送“EHLO smtp.mail.yahoo.com\r\n”。

4、如果身份有效,則伺服器進入等待認證狀態,主動推送自身支援的所有SMTP認證方式

網易郵箱傳送的內容如下:

  1. 250-mail\r\n  
  2. 250-PIPELINING\r\n  
  3. 250-AUTH LOGIN PLAIN\r\n  
  4. 250-AUTH=LOGIN PLAIN\r\n  
  5. 250-coremail\r\n 1Uxr2xKj7kG0xkI17xGrU7I0s8FY2U3Uj8Cz28x1UUUUU7Ic2I0Y2UFWNUp_UCa0xDrUUUUj  
  6. 250-STARTTLS\r\n  
  7. 250 8BITMIME\r\n  

表示其支援LOGIN、PLAIN兩種認證方式;

雅虎郵箱傳送的內容如下:

  1. 250-smtp206.mail.ne1.yahoo.com\r\n  
  2. 250-AUTH LOGIN PLAIN XYMCOOKIE\r\n  
  3. 250-PIPELINING\r\n  
  4. 250 8BITMIME\r\n  

表示其支援LOGIN、PLAIN、XYMCOOKIE三種認證方式。

5、客戶端判斷自身是否支援伺服器提供的SMTP認證方式

如果認證方式指定“auto”則採用服務端提供的第一個認證方式,如果指定其他方式,則判斷服務端是否支援該方式,否則返回錯誤。
這一歩相當關鍵,因為客戶端程式可以根據具體的認證方式載入相應外掛來完成認證過程。

6、客戶端向伺服器請求認證

傳送“AUTH LOGIN\r\n”。

7、如果認證請求合理,伺服器將進入等待使用者輸入狀態

傳送“334 VXNlcm5hbWU6\r\n”,334表示等待客戶端輸入,VXNlcm5hbWU6表示等待輸入使用者名稱。

8、客戶端向伺服器傳送轉碼後的使用者名稱

傳送經過Base64轉碼後的使用者名稱(taotown)”dGFvdG93bg==\r\n“。

9、伺服器再次進入等待使用者輸入狀態

傳送“334 UGFzc3dvcmQ6\r\n”,334表示等待客戶端輸入,UGFzc3dvcmQ6表示等待輸入密碼

10、客戶端向伺服器傳送轉碼後的密碼

傳送經過Base64轉碼後的密碼(Haier)“SGFpZXI=\r\n”。

11、如果使用者名稱或者密碼出錯,伺服器將返回530錯誤,傳送“530 Access denied\r\n”,表示認證失敗。否則將返回235,傳送“235 OK, go ahead\r\n”,表示使用者認證成功。

12、客戶端告訴伺服器郵件來自何方

傳送“MAIL FROM: <[email protected]> \r\n”。

13、如果合理,服務端返回250表示成功

傳送“250 OK , completed\r\n”。

14、客戶端告訴伺服器郵件去往何地

傳送“RCPT TO: <[email protected]> \r\n”。

15、如果合理,伺服器返回250表示成功

傳送“250 OK , completed\r\n”。

16、客戶端告訴伺服器自己準備傳送郵件正文

傳送“DATA\r\n”。

17、伺服器返回354,表示自己已經作好接受郵件的準備

傳送“354 Start Mail. End with CRLF.CRLF\r\n”,提醒客戶端開始傳送郵件並以“.”結束。

18、客戶端傳送郵件正文(如果正文過長,可以分多次傳送)

傳送

  1. “To: [email protected]\r\n  
  2. Subject: Hello Trevor\r\n  
  3. My name is TaoZhen\r\n  
  4. ”。  

19、客戶端傳送完正文以後,緊接著傳送結束符

傳送“.”。

20、如果合理,服務端返回250表示成功

傳送“250 OK , completed\r\n”。

21、郵件傳送結束,客戶端請求斷開連線

傳送“QUIT\r\n”。

22、伺服器返回211,提示斷開申請被採納,並主動斷開連線,整個郵件傳送過程結束。

傳送“221 Service Closing transmission\r\n”。

附:如果服務端傳過來的錯誤碼後面緊跟這”-“,則說明該次訊息分了很多節,直到最後一節沒有”-“為止。

資料二

在以前接觸的專案中,一直都是在做網站時用到了傳送mail 的功能,在asp 和.net 中都有相關的傳送mail 的類, 實現起來非常簡單。最近這段時間因工作需要在C++ 中使用傳送mail 的功能,上網搜了一大堆資料,終於得以實現,總結自己開發過程中碰到的一些問題,希望對需的人有所幫助, 由於能力有限, 文中不免有些誤解之處,望大家能指正!!

其實,使用C++ 傳送mail 也是很簡的事, 只需要瞭解一點SMTP 協議和socket 程式設計就OK 了, 網路上也有很多高人寫好的mail 類原始碼,有興趣的朋友可以下載看看.

1.     SMTP 常用命令簡介

1). SMTP 常用命令

HELO/EHLO 向伺服器標識使用者身份

MAIL 初始化郵件傳輸

mail from:

RCPT 標識單個的郵件接收人;常在MAIL 命令後面

可有多個rcpt to:

DATA 在單個或多個RCPT 命令後,表示所有的郵件接收人已標識,並初始化資料傳輸,以. 結束。

VRFY 用於驗證指定的使用者/ 郵箱是否存在;由於安全方面的原因,伺服器常禁止此命令

EXPN 驗證給定的郵箱列表是否存在,擴充郵箱列表,也常被禁用

HELP 查詢伺服器支援什麼命令

NOOP 無操作,伺服器應響應OK

QUIT 結束會話

RSET 重置會話,當前傳輸被取消

如你對SMTP 命令不瞭解,可以用telnet 命令登陸到smtp 伺服器用help 命令進行檢視:

220 tdcsw.maintek.corpnet.asus ESMTP Sendmail 8.13.8/8.13.8; Sat, 9 Jan 2010 10:
45:09 +0800
help
214-2.0.0 This is sendmail
214-2.0.0 Topics:
214-2.0.0       HELO    EHLO    MAIL    RCPT    DATA
214-2.0.0       RSET    NOOP    QUIT    HELP    VRFY
214-2.0.0       EXPN    VERB    ETRN    DSN     AUTH
214-2.0.0       STARTTLS
214-2.0.0 For more info use "HELP <topic>".
214-2.0.0 To report bugs in the implementation see
214-2.0.0       http://www.sendmail.org/email-addresses.html
214-2.0.0 For local information send email to Postmaster at your site.
214 2.0.0 End of HELP info

2).SMTP 返回碼含義

  *   郵件服務返回程式碼含義 

  *   500   格式錯誤,命令不可識別(此錯誤也包括命令列過長) 

  *   501   引數格式錯誤 

  *   502   命令不可實現 

  *   503   錯誤的命令序列 

  *   504   命令引數不可實現 

  *   211    系統狀態或系統幫助響應 

  *   214   幫助資訊 

  *   220     服務就緒 

  *   221     服務關閉傳輸通道 

  *   421     服務未就緒,關閉傳輸通道(當必須關閉時,此應答可以作為對任何命令的響應) 

  *   250   要求的郵件操作完成 

  *   251   使用者非本地,將轉發向 

  *   450   要求的郵件操作未完成,郵箱不可用(例如,郵箱忙) 

  *   550   要求的郵件操作未完成,郵箱不可用(例如,郵箱未找到,或不可訪問) 

  *   451   放棄要求的操作;處理過程中出錯 

  *   551   使用者非本地,請嘗試 

  *   452   系統儲存不足,要求的操作未執行 

  *   552   過量的儲存分配,要求的操作未執行 

  *   553   郵箱名不可用,要求的操作未執行(例如郵箱格式錯誤) 

  *   354   開始郵件輸入,以. 結束 

  *   554   操作失敗 

  *   535   使用者驗證失敗 

  *   235   使用者驗證成功 

  *   334   等待使用者輸入驗證資訊 for next connection>;

3) SMTP 命令應用

我們下需使用telnet 命令實現smtp 郵件的傳送,具體操作如下:

220 tdcsw.com ESMTP Sendmail 8.13.8/8.13.8; Wed, 23 Dec 2009 18
:18:18 +0800
HELO tdcsw
250 tdcsw.com Hello x-128-101-1-240.ahc.umn.edu [128.101.1.240], pleased to meet you
MAIL FROM:[email protected]
250 2.1.0 [email protected] Sender ok
RCPR TO:[email protected]
250 2.1.5 [email protected] Recipient ok
DATA
354 Enter mail, end with "." on a line by itself
SUBJECT:HELLO
HI:
HAR are you?
.
250 2.0.0 nBNAIIG4000507 Message accepted for delivery
quit
221 2.0.0 tdcsw.maintek.corpnet.asus closing connection
Connection to host lost.

2.     用C++ 實現Mail 傳送

為了便於理解, 在此就不封裝Mail 類了, 而是以過程式函式方式給出.

1). 首先需要建立TCP 套接字, 連線埠依伺服器而定,SMTP 服務預設埠為25, 我們以 預設埠為例

WSADATA wsaData;

int  SockFD;

WSAStartup(MAKEWORD(2,2), &wsaData);

SockFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

ServAddr.sin_family = AF_INET;

ServAddr.sin_addr.s_addr = inet_addr (“192.168.1.1”);             //192.168.1.1 為伺服器地址

ServAddr.sin_port = htons(25);

connect(SockFD, (struct sockaddr *)&ServAddr, sizeof(ServAddr));

2). 傳送SMTP 命令及資料

const char HEADER[] = "HELO smtpSrv/r/n"

  "MAIL FROM: [email protected]/r/n"

  "RCPT TO: [email protected]/r/n"

  "DATA/r/n"

  "FROM: [email protected]/r/n"

  "TO: [email protected]/r/n"

  "SUBJECT: this is a test/r/n"

  "Date: Fri, 8 Jan 2010 16:12:30/r/n"

"X-Mailer: shadowstar's mailer/r/n"

  "MIME-Version: 1.0/r/n"

  "Content-type: text/plain/r/n/r/n";

//send HEADER

send(SockFD, HEADER, strlen(HEADER), 0);

const char CONTENT[]="this is content./r/n";

//send CONTENT

send(SockFD, CONTENT, strlen(CONTENT), 0);

send(SockFD, "./r/n", strlen("./r/n"), 0);   //end

send(SockFD, "QUIT/r/n", strlen("QUIT/r/n"), 0); //quit

mail 傳送的功能基本上就完成了, 當然, 如果是應用的話還是需要很多改動的地方的, 比如說新增附件等.

3). 附件功能

要使用SMTP 傳送附件, 需要對SMTP 頭資訊進行說明, 改變Content-type 及為每一段正文新增BOUNDARY名, 示例如下:

"DATA/r/n"

  "FROM: [email protected]/r/n"

  "TO: [email protected]/r/n"

  "SUBJECT: this is a test/r/n"

  "Date: Fri, 8 Jan 2010 16:12:30/r/n"

"X-Mailer: shadowstar's mailer/r/n"

  "MIME-Version: 1.0/r/n"

  "Content-type: multipart/mixed; boundary=/"#BOUNDARY#/"/r/n/r/n";

// 正文

"--#BOUNDARY#/r/n"

  "Content-Type: text/plain; charset=gb2312/r/n"

  "Content-Transfer-Encoding: quoted-printable/r/n"

郵件正文……….

// 附件

"/r/n--#BOUNDARY#/r/n"

  "Content-Type: application/octet-stream; name=att.txt/r/n"

  "Content-Disposition: attachment; filename=att.txt/r/n"

  "Content-Transfer-Encoding: base64/r/n"

  "/r/n"

附件正資訊(base64 編碼)…..

Base64 編碼函式在網路上很容易找到, 這裡就不給出原始碼了, 如需要支援HTML 格式而又不知道如何寫這些頭資訊, 可以用outlook 或foxmail 寫一封支援HTML 格式的mail, 檢視其原文資訊, 依照相同的格式傳送就行了.

4). 實現抄送及密送

在SMTP 命令集中並沒有RCPT CC 或RCPT BCC 相關命令, 那要如何來實現抄送和密送功能呢?

在網路上找到這樣一句話: “ 所有的接收者協商都通過RCPT TO 命令來實現,如果是BCC ,則協商傳送後在對方接收時被刪掉信封接收者”, 開始一直不明白這句話是什麼意思? 後來通看檢視foxmail 的郵件原文發現:

Date: Wed, 6 Jan 2010 12:11:48 +0800

From: "carven_li" < carven_li @smtp.com>

To: "carven" <[email protected]>

Cc: "sam" <[email protected]>,

  "yoyo" <[email protected]>

BCC: "clara" <[email protected]>

Subject: t

X-mailer: Foxmail 5.0 [cn]

Mime-Version: 1.0

Content-Type: multipart/mixed;

    boundary="=====001_Dragon237244850520_====="

才恍然大悟, 所謂的” 協商” 應該就是指傳送方在Data 中指定哪些為CC, 哪些為BCC, 預設情況下什麼都不寫, 只發送第一個RCPT TO 的mail, 其他的都被過濾掉

3. SMTP身份認證
SMTP身份認證方式有很多種, 每種認證方式驗證傳送的資訊都有點細微的差別, 這裡我主要介紹下LOGIN,PLAIN及NTLM三種簡單的認證方式, 附帶CRAM-MD5和DIGEST-MD5方式(驗證沒通過, 不知道問題出在哪了? 有待高人幫忙解決!).

要進行身份認證, 先要知道當前SMTP伺服器支援哪些認證方式, 在ESMTP中有個與HELO命令相同功能的命令EHLO可以得到當前伺服器支援的認證方式(有些伺服器無返回資訊, 可能伺服器端作了限制).


1) LOGIN認證方式
LOGIN認證方式是基於明文傳輸的, 因此沒什麼安全性可言, 如資訊被截獲, 那麼使用者名稱和密碼也就洩露了. 認證過程如下:
AUTH LOGIN
334 VXNlcm5hbWU6                            //伺服器返回資訊, Base64編碼的Username:
bXlOYW1l                                //輸入使用者名稱, 也需Base64編碼
334 UGFzc3dvcmQ6                            //伺服器返回資訊, Base64編碼的Password::
bXlQYXNzd29yZA==                            //輸入密碼, 也需Base64編碼
235 2.0.0 OK Authenticated                        // 535 5.7.0 authentication failed

2). NTLM認證方式
NTLM認證方式過程與LOGIN認證方式是一模一樣的, 只需將AUTH LOGN改成AUTH NTLM.就行了.

3). PLAIN認證方式
PLAIN認證方式訊息過過程與LOGIN和NTLM有所不同, 其格式為: “NULL+UserName+NULL+Password”, 其中NULL為C語言中的’/0’, 不方便使用命令列測試, 因此下面給出C++程式碼來實現:
char szSend[] = "$user$pwd";
size_t n = stlen(szSend);
for(int i=0; i<n; i++)
    if(szSend[i] == '$') szSend[i] = '/0';

char szMsg[512]
base64_encode(szSend, n, szMsg);
send (skt, szMsg, strlen(szMsg), 0);

4). CRAM-MD5認證方式
前面所介紹的三種方式, 都是將使用者名稱和密碼經過BASE64編碼後直接傳送到伺服器端的, BASE64編碼並不是一種安全的加密演算法, 其所有資訊都可能通過反編碼, 沒有什麼安全性可言. 而CRAM-MD5方式與前三種不同, 它是基於Challenge/Response的方式, 其中Challenge是由伺服器產生的, 每次連線產生的Challenge都不同, 而Response是由使用者名稱,密碼,Challenge組合而成的, 具體格式如下:
response=base64_encode(username : H_MAC(challenge, password))
H_MAC是Keyed MD5演算法(見http://www.faqs.org/rfcs/rfc2195.html), 先由challenge和password生成16位的雜湊碼, 將其轉換成16進位制32個位元組的字串陣列digest(即以%02x輸出), 再對(username+空格+digest[32])進行base64編碼,就是要傳送的response了.
另外, 在http://www.net-track.ch/opensource/cmd5/提供了SMTP CRAM-MD5認證原始碼, 可用於測試CRAM-MD5認證, 但不知道是不是我這邊測試的SendMail伺服器配置有問題, 測試時一直不能通過.

5). DIGEST-MD5認證方式
DIGEST-MD5認證也是Challenge/Response的方式, 與CRAM-MD5相比, 它的Challenge資訊更多, 其Response計算方式也非常複雜, 我在測試時也是以認證失敗而告終, 只是將在網上找到的資料整理於此, 能為後來研究的人多提供點資料, 或者有興趣的朋友們可以和我一起討論下.

我們先看下DIGEST-MD5認證傳送響應資訊:

DIGEST-MD5伺服器格式說明(見RFC 2831 Digest SASL Mechanism Mai 2000):
   digest-challenge =
         1 # (Reich | Nonce | qop-Optionen | schal | MAXBUF | charset
               Algorithmus | Chiffre-opts | auth-param)

        realm = "Reich" "=" < "> Reich-Wert <">
        Reich-Wert = qdstr-val
        Nonce = "Nonce" "=" < "> Nonce-Wert <">
        Nonce-Wert = qdstr-val
        qop-options = "qop" "=" < "> qop-Liste <">
        qop-list = 1 # qop-Wert
        qop-Wert = "auth" | "auth-int" | "auth-conf" |
                             Token
        stale = "veraltete" "=" "true"
        MAXBUF = "MAXBUF" "=" MAXBUF-Wert
        MAXBUF-Wert = 1 * DIGIT
        charset = "charset" = "" UTF-8 "
        algorithm = "Algorithmus" "=" "md5-sess"
        Chiffre-opts = "Chiffre" "=" < "> 1 # Null-Wert <">
        Chiffre-value = "3des" | "des" | "RC4-40" | "RC4" |
                            "RC4-56" | Token
        auth-param = Token "=" (token | quoted-string)
DIGEST-MD5客戶端響應格式說明(見RFC 2831 Digest SASL Mechanism Mai 2000):
   digest-response = 1 # (Benutzername | Reich | Nonce | cnonce |
                          Nonce-count | qop | digest-uri | Antwort |
                          MAXBUF | charset | Chiffre | authzid |
                          auth-param)

       username = "username" = "<"> username-Wert < ">
       Benutzernamen-Wert = qdstr-val
       cnonce = "cnonce" "=" < "> cnonce-Wert <">
       cnonce-Wert = qdstr-val
       Nonce-count = "nc" "=" nc-Wert
       nc-Wert = 8LHEX
       qop = "qop" "=" qop-Wert
       digest-uri = "digest-uri" = "<"> digest-uri-value < ">
       digest-uri-value = serv-type "/" host [ "/" serv-name]   //eg: smtp/mail3.example.com/example.com
       serv-type = 1 * ALPHA            //www for web-service, ftp for ftp-dienst, SMTP for mail-versand-service …
       host = 1 * (ALPHA | DIGIT | "-" | ".")
       serv-name = host
       response = "Antwort" "=" Response-Wert
       response-value = 32LHEX
       LHEX = "0" | "1" | "2" | "3" |
                          "4" | "5" | "6" | "7" |
                          "8" | "9" | "a" | "b" |
                          "c" | "d" | "e" | "f"
       cipher = "Chiffre" "=" Null-Wert
       authzid = "authzid" "=" < "> authzid-Wert <">
       authzid-Wert = qdstr-val

其各欄位具體含義見相關文件, 這裡只介始幾個需要用到的欄位是如何產生的, C/S響應示例如下:
    S: realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
       algorithm=md5-sess,charset=utf-8
    C: charset=utf-8,username="chris",realm="elwood.innosoft.com",
       nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
       digest-uri="imap/elwood.innosoft.com",
       response=d388dad90d4bbd760a152321f2143af7,qop=auth
    S: rspauth=ea40f60335c427b5527b84dbabcdfffd

    The password in this example was "secret".
從這個示例可以看出, 客戶端返回的資訊比伺服器端傳送過來的多了以下幾個:
username, nc, cnonce, digest-uri和respone
username就不用說了, nc是8位長的16進位制數字符串,統計客戶端使用nonce發出請求的次數(包含當前請求),例示我們可以設為”00000001”, cnonce是是用了4個隨機陣列成一個8位長16進位制的字串,digest-uri是由在realm前加上請求型別(如http, smtp等), response是一個32位長的16進位制陣列, 計算公式如下:
If the "qop" value is "auth" or "auth-int":
      request-digest  = <"> < KD ( H(A1),     unq(nonce-value)
                                          ":" nc-value
                                          ":" unq(cnonce-value)
                                          ":" unq(qop-value)
                                          ":" H(A2)
                                  ) <">
   If the "qop" directive is not present (this construction is for
   compatibility with RFC 2069):
      request-digest  =
                 <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) >
   <">
   See below for the definitions for A1 and A2.
Read more: http://www.faqs.org/rfcs/rfc2617.html#ixzz0c4s8ck3F

KD(secret,data)表示分類演算法,其中data指資料,secret表示採用的方法.如果表示校驗和演算法時,data要寫成H(data);而unq(X)表示將帶引號字串的引號去掉。       
           對於"MD5" 和"MD5-sess" 演算法: 
H(data) = MD5(data)
和 
KD(secret, data) = H(concat(secret, ":", data))

如果"algorithm"指定為"MD5"或沒有指定,A1計算方式如下:
A1  =  unq(username-value) ":" unq(realm-value) ":" passwd
//Password為使用者密碼
如果"algorithm"指定為"MD5-sess", 則需要nonce和cnonce的參與:
A1       = H(unq(username-value) ":" unq(realm-value) ":" passwd )
                     ":" unq(nonce-value) ":" unq(cnonce-value)

如果"qop"沒有指定或指定為"auth", A2計算方式如下:
A2  = Method ":" digest-uri-value
如果"qop"沒有指定或指定為"auth int", A2計算方式如下:
A2 = Metho