网络公有协议之邮件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: bripengandre@163.com /* 发送者邮箱 */

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

CRCPT TO: bripengandre@smail.hust.edu.cn /* 接收者邮箱 */

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

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

max_min_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值