1、寫在開始之前
之前在工作中也是遇到過smtp協議,那個時候因爲解決出現的bug比較急,所以並沒有仔細去學習或者深入瞭解smtp相關知識,剛好最近工作又碰到相關問題,因爲bug的奇怪,所以不得不放下手頭的相關工作,好好研究了下smtp協議的相關流程和具體實施,所以記錄下來和大家一起分享。
2、smtp理論基礎知識
smpt(全稱爲 simple mail transfer protocol),中文的意思也就是簡單的郵件傳輸協議,它是一組用於有源地址到目的地址傳輸郵件的規則,是由它來控制信件的中轉方式。其實關於smtp協議在百度百科上講了非常明白了,我也主要通過這裏的相關介紹,然後自己實踐代碼抓包分析服務器迴應迴應來深入學習的。例如:當你的一個朋友向你發送郵件時,他的郵件服務器和你的郵件服務器假設是通過SMTP協議通信,將郵件傳遞給你郵件地址所指示的郵件服務器上,然後你的客戶端通過POP3或SMPT協議與郵件服務器交互,將郵件信息傳遞到客戶端。這就完成了一個發送的過程,可以參考百度百科上的例圖的主要流程,具體的交互過程細節以及代碼實現,將下面繼續爲大家逐步分析
3、smtp交互流程
SMTP的命令和響應都是基於文本,以命令行爲單位,換行符爲CR/LF。響應信息一般只有一行,由一個3位數的代碼開始,代表你發送後的響應結果,後面則是附上很簡短的文字說明。
SMTP要經過建立連接、傳送郵件和釋放連接3個階段。具體爲:
a TCP連接。
b 客戶端向服務器發送EHLO命令以標識發件人自己的身份,併發送自身的地址和密碼通過認證,然後客戶端發送MAIL命令。
c 服務器端以OK作爲響應,表示準備接收。
d 客戶端發送MAIL FROM和RCPT TO命令,表明發送方和接收方,當然接收方可以多個。
e 服務器端表示是否願意爲收件人接收郵件。
f 協商結束,發送郵件,用命令DATA發送輸入內容。
g 結束此次發送,發送'.'和QUIT命令退出。
C:telnet smtp.163.com 25 /* 以telnet方式連接163郵件服務器 */
S:220 163.com Anti-spam GT for Coremail System (163com[071018]) /* 220爲響應數字,其後的爲歡迎信息,會應服務器不同而不同*/
C:HELO smtp.163.com /* HELO 後用來填寫返回域名(具體含義請參閱RFC821),但該命令並不檢查後面的參數*/
S:250 OK
C: MAIL FROM: [email protected] /* 發送者郵箱 */
S:250 … ./* “…”代表省略了一些可讀信息 */
C:RCPT TO: [email protected] /* 接收者郵箱 */
S:250 … ./* “…”代表省略了一些可讀信息 */
C:DATA /* 請求發送數據 */
S:354 Enter mail, end with "." on a line by itself
C:Enjoy Protocol Studing
C:.
S:250 Message sent
C:QUIT /* 退出連接 */
S:221 Bye
大致流程也就如上所示了,當然後面如果發送附件的話,也是在郵件體後面添加就好,有幾點需要提醒下大家:
1、每個命令都需要以CR+LF結束,且不能有多餘的信息,否則服務器會直接返回命令未實現或者格式不對。
2、每次操作成功後,服務器響應操作正確的返回值並不是都相同的
3、最後發送完以後,也需要發送一個boundary,並且結尾需要再加上'--'
4、具體代碼實現
首先是和smtp協議交互的相互信息:
/*
@remark:發送郵件之前到smtp協議交互和認證
@param : param [in] 郵件用戶相關信息
len [in] the length of param
@return: 0 success and others failed
*/
int smtp_start_server(void *param, int len)
{
char smtpSnd[96];
if( param == NULL || len != sizeof(SmtpInfo_S))
{
DBG_SMTP_INFO(" param error!\n");
return -1;
}
SmtpInfo_S *pSmtpInfo = (SmtpInfo_S *)param;
/************ 1 step: connect to the smtp server ************************************/
char srvPort[8];
memset(srvPort, 0, 8);
sprintf(srvPort, "%d", pSmtpInfo->smtpPort);
int smtpSock = hi_tcp_noblock_connect(NULL, NULL, pSmtpInfo->smtpSrv, srvPort, SMTP_TIMEOUT);
if( smtpSock <= 0 ||
smtp_rcvfrom_server(smtpSock) != 220 ) // 220 is this option success
{
DBG_SMTP_INFO("connect %s failed:%s", pSmtpInfo->smtpSrv, strerror(errno));
goto SMTP_ERROR;
}
/************ 2 step: send 'EHLO'***************************************/
memset(smtpSnd, 0, 96);
sprintf(smtpSnd, "EHLO %s\r\n", pSmtpInfo->smtpSrv);
if( smtp_sendto_server(smtpSock, smtpSnd, strlen(smtpSnd)) != 0 ||
(smtp_rcvfrom_server(smtpSock) != 250 )) // 250 is this option success
{
DBG_SMTP_INFO(" EHLO failed\n");
goto SMTP_ERROR;
}
/************ 3 step: auth login *************************************/
memset(smtpSnd, 0, 96);
strcpy(smtpSnd, "AUTH LOGIN\r\n");
if( smtp_sendto_server(smtpSock, smtpSnd, strlen(smtpSnd)) != 0 ||
(smtp_rcvfrom_server(smtpSock) != 334)) // 334 is this option success
{
DBG_SMTP_INFO("Auth login failed\n");
goto SMTP_ERROR;
}
/* send username */
memset(smtpSnd, 0, 96);
base64_bits_to_64((unsigned char *)smtpSnd, (unsigned char *)pSmtpInfo->smtpFromUsername, strlen(pSmtpInfo->smtpFromUsername));
strcat(smtpSnd, "\r\n");
if( smtp_sendto_server(smtpSock, smtpSnd, strlen(smtpSnd)) != 0 ||
(smtp_rcvfrom_server(smtpSock) != 334 )) // 334 is this option success
{
DBG_SMTP_INFO(" Auth username failed\n");
goto SMTP_ERROR;
}
/* send password */
memset(smtpSnd, 0, 96);
base64_bits_to_64((unsigned char *)smtpSnd, (unsigned char *)pSmtpInfo->smtpFromPassword, strlen(pSmtpInfo->smtpFromPassword));
strcat(smtpSnd, "\r\n");
if( smtp_sendto_server(smtpSock, smtpSnd, strlen(smtpSnd)) != 0 ||
(smtp_rcvfrom_server(smtpSock) != 235 )) // 235 is auth option success
{
DBG_SMTP_INFO(" Auth password failed\n");
goto SMTP_ERROR;
}
/****************** 4 step: start to send mail ***********************************/
if( smtp_send_email_start(smtpSock, pSmtpInfo) != 0 )
goto SMTP_ERROR;
/* 5 step: end to send mail */
if(smtp_send_email_end(smtpSock) != 0)
goto SMTP_ERROR;
return 0;
SMTP_ERROR:
return -1;
}
當完成基本的協議需要操作後就需要發送郵件實際消息,藉口實現如下:
/*
@remark:send email
@param :param all [in]
@return: 0 success, and -1 is failed
*/
int smtp_send_email_start(int sockfd, SmtpInfo_S *pSmtp)
{
int dst_num = 0;
char smtpField[96];
char smtpHeader[256];
char smtpbody[SMTP_BODY_SIZE];
if( sockfd <=0 || pSmtp == NULL )
{
DBG_SMTP_INFO(" param error!\n");
return -1;
}
/********************** 1 step: send the src address *********************************/
memset(smtpField, 0, 96);
sprintf(smtpField, "MAIL FROM: <%s>\r\n", pSmtp->smtpFromUsername);
if( smtp_sendto_server(sockfd, smtpField, strlen(smtpField)) != 0 ||
(smtp_rcvfrom_server(sockfd) != 250 )) // 250 is this option success
{
DBG_SMTP_INFO(" send src mail address failed\n");
goto SMTP_SEND_ERR;
}
/************************* 2 step: send the dst address *********************/
for(dst_num =0; dst_num < 1; dst_num ++)//這裏可以循環發送多個接收方
{
memset(smtpField, 0, 96);
sprintf(smtpField, "RCPT TO: <%s>\r\n", pSmtp->smtpFromToUsername[dst_num]);
if( smtp_sendto_server(sockfd, smtpField, strlen(smtpField)) != 0 ||
(smtp_rcvfrom_server(sockfd) != 250 )) // 250 is this option success
{
DBG_SMTP_INFO(" send %d dst mail address failed\n", dst_num +1);
goto SMTP_SEND_ERR;
}
}
/************************ 3 step: send 'DATA' ****************************/
memset(smtpField, 0, 96);
strcpy(smtpField, "DATA\r\n");
if( smtp_sendto_server(sockfd, smtpField, strlen(smtpField)) != 0 ||
(smtp_rcvfrom_server(sockfd) != 354 )) // 354 is this option success
{
DBG_SMTP_INFO(" send 'DATA' field failed\n");
goto SMTP_SEND_ERR;
}
//這裏纔是真正開始發送數據,前面都是確認爲smtp協議的鋪墊工作
/********************** 4 step: send mail header *****************************/
memset(smtpHeader, 0, 256);
sprintf(smtpHeader, SMTP_HEARDER_FORMAT,
pSmtp->smtpFromUsername,
pSmtp->smtpFromToUsername[0],
(char *)"SMTP-Test");
DBG_SMTP_INFO("Header:%s\n", smtpHeader);
if( smtp_sendto_server(sockfd, smtpHeader, strlen(smtpHeader)) != 0)
{
DBG_SMTP_INFO(" send smtp header field failed\n");
goto SMTP_SEND_ERR;
}
/********************* 5 step: send mail body ******************************/
memset(smtpbody, 0, SMTP_BODY_SIZE);
sprintf(smtpbody, SMTP_CONTENT_FORMAT,
(char *)"just for test the smtp protocol!!!!!");
DBG_SMTP_INFO("body:\n%s\n", smtpbody);
if( smtp_sendto_server(sockfd, smtpbody, strlen(smtpbody)) != 0)
{
DBG_SMTP_INFO(" send smtp body field failed\n");
goto SMTP_SEND_ERR;
}
return 0;
SMTP_SEND_ERR:
return -1;
}
最後發送完結束後,需要發送'.'和QUIT信令,如下:/*
@remark: send the quit field msg
@param : sockfd [in]
@return: 0 success, and -1 is failed
*/
int smtp_send_email_end(int sockfd)
{
char smtpField[48];
/*************** 1 step: send the last boundary ************************/
memset(smtpField, 0, 48);
strcpy(smtpField, "\r\n--smtp-test-boundary--\r\n");
if( smtp_sendto_server(sockfd, smtpField, strlen(smtpField)) != 0)
{
DBG_SMTP_INFO(" send last boundary field failed\n");
return -1;
}
/**************** 2 step: send '.' ************************************/
memset(smtpField, 0, 48);
strcpy(smtpField, "\r\n.\r\n");
if( smtp_sendto_server(sockfd, smtpField, strlen(smtpField)) != 0 ||
(smtp_rcvfrom_server(sockfd) != 250 )) // 250 is this option success
{
DBG_SMTP_INFO(" send '.' field failed\n");
return -1;
}
/**************** 3 step: send 'QUIT' *********************************/
memset(smtpField, 0, 48);
strcpy(smtpField, "QUIT\r\n");
if( smtp_sendto_server(sockfd, smtpField, strlen(smtpField)) != 0 ||
(smtp_rcvfrom_server(sockfd) != 221 )) // 250 is this option success
{
DBG_SMTP_INFO(" send 'QUIT' field failed\n");
return -1;
}
return 0;
}
在發送郵件的過程中定義的Header和content結構如下:
// DEBUG
#define DBG_SMTP_INFO(pFmt, ...) \
do{\
fprintf(stderr, "[SMTP_DBG]-[%s]-[%d]:"pFmt, __func__, __LINE__, ##__VA_ARGS__);\
fflush(stderr);\
}while(0)
// SMTP Header def
#define SMTP_HEARDER_FORMAT \
"From:%s\r\n"\
"To:%s\r\n"\
"Subject:%s\r\n"\
"MIME-Version:1.0\r\n"\
"Content-type:multipart/mixed;boundary=\"smtp-test-boundary\"\r\n"\
"\r\n"
// SMTP Content def
#define SMTP_CONTENT_FORMAT \
"\r\n--smtp-test-boundary\r\n"\
"Content-type:text/plain; charset=utf-8\r\n"\
"Content-Transfer-Encoding: 7bit\r\n"\
"\r\n"\
"%s\r\n"
/* mail user info */
typedef struct _SmtpInfo_S_
{
char smtpSrv[16];
int smtpPort;
char smtpFrom[32];
char smtpFromUsername[32];
char smtpFromPassword[32];
char smtpFromToUsername[3][32]; //最多三個接收者
char smtpSSLFlag;
char smtpReserverd[7];
}SmtpInfo_S;
注意點:在頭中定義的boudary = smtp-test-boundary,那麼在後面的內容或者附件的每次開始的時候都需要加上“--smtp-test-boundary”,並且在郵件體發送結束後,則需要加上“--smtp-test-boundary--”("smtp-test-boundary"的值可以根據自己定義,只要保持和頭中的一致即可)。
5 、抓包對比分析
整個smtp協議的交互流程就走完,下面是通過程序的分析如下:
如上圖所示了,對於紅色標出部分即爲boudary,每次email信息體都需要包含獨自一個開頭,但是最後之需要一個結尾,注意結 尾和開頭的不同
6、相關錯誤碼對比,各個動作返回的錯誤碼對比如下:
‘*************************
‘* 郵件服務返回代碼含義
‘* 500 格式錯誤,命令不可識別(此錯誤也包括命令行過長)
‘* 501 參數格式錯誤
‘* 502 命令不可實現
‘* 503 錯誤的命令序列
‘* 504 命令參數不可實現
‘* 211 系統狀態或系統幫助響應
‘* 214 幫助信息
‘* 220 服務就緒
‘* 221 服務關閉傳輸信道
‘* 421 服務未就緒,關閉傳輸信道(當必須關閉時,此應答可以作爲對任何命令的響應)
‘* 250 要求的郵件操作完成
‘* 251 用戶非本地,將轉發向
‘* 450 要求的郵件操作未完成,郵箱不可用(例如,郵箱忙)
‘* 550 要求的郵件操作未完成,郵箱不可用(例如,郵箱未找到,或不可訪問)
‘* 451 放棄要求的操作;處理過程中出錯
‘* 551 用戶非本地,請嘗試
‘* 452 系統存儲不足,要求的操作未執行
‘* 552 過量的存儲分配,要求的操作未執行
‘* 553 郵箱名不可用,要求的操作未執行(例如郵箱格式錯誤)
‘* 354 開始郵件輸入,以.結束
‘* 554 操作失敗
‘* 535 用戶驗證失敗
‘* 235 用戶驗證成功
‘* 334 等待用戶輸入驗證信息
8 尾聲
匆匆寫下,可能還有諸多細節沒有點出,如有疑惑,就留言相互請教學習,交流也是一種學習方式。
本文藉助的相關參考:
http://blog.csdn.net/bripengandre/article/details/2191048
http://linux.chinaunix.net/techdoc/system/2008/09/06/1030551.shtml