網絡公有協議之郵件SMTP篇

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命令退出。

Ctelnet smtp.163.com 25   /* 以telnet方式連接163郵件服務器 */

S220 163.com Anti-spam GT for Coremail System (163com[071018]) /* 220爲響應數字,其後的爲歡迎信息,會應服務器不同而不同*/

CHELO smtp.163.com /* HELO 後用來填寫返回域名(具體含義請參閱RFC821),但該命令並不檢查後面的參數*/

S:250 OK

C: MAIL FROM: [email protected] /* 發送者郵箱 */

S250  ./* “…”代表省略了一些可讀信息 */

CRCPT TO: [email protected] /* 接收者郵箱 */

S250  ./* “…”代表省略了一些可讀信息 */

CDATA  /* 請求發送數據 */

S354 Enter mail, end with "." on a line by itself

CEnjoy Protocol Studing

C.

S250 Message sent

CQUIT /* 退出連接 */

S221 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

           http://baike.baidu.com/view/5450.htm?fr=aladdin

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章