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
http://baike.baidu.com/view/5450.htm?fr=aladdin