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: bripengandre@163.com /* 发送者邮箱 */
S:250 … ./* “…”代表省略了一些可读信息 */
C:RCPT TO: bripengandre@smail.hust.edu.cn /* 接收者邮箱 */
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