第二十三章 SMTP23.1 SMTP例程概述 SMTP例程主要实现通过W7500EVB发送邮件。邮件的发送是依靠SMTP协议,所以使用前,我们先了解下SMTP协议。 23.2 SMTP协议简介SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP协议属于TCP/IP协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。通过SMTP协议所指定的服务器,就可以把Email寄到收信人的服务器上了,整个过程只要几分钟。 SMTP服务器则是指遵循SMTP协议的发送邮件服务器,用来发送或中转发出的电子邮件。SMTP使用TCP端口25。要为一个给定的域名指定一个邮件交换服务器,需要使用MX(MaileXchange)DNS。SMTP在整个电子邮件通信中所处的位置如图23.2.1所示。 图23..2.1 SMTP邮件传输过程 23.3 SMTP协议工作原理SMTP是工作在两种情况下:一是电子邮件从客户机传输到服务器:二是从某一个服务器传输到另一个服务器。SMTP也是个请求/响应协议,命令和响应都是基于ASCⅡ文本,并以CR和LF符结束。响应包括一个表示返回状态的三位数字代码。SMTP在TCP协议25号端口监听连续请求。 smtp连接和发送过程: (1)建立TCP 连接。 (2)客户端发送HELO命令自己的身份以标识发件人,然后客户端发送MAIL 命令;服务器端以OK作为响应,表明准备接收。 (3)客户端发送RCPT命令,以标识该电子邮件的计划接收人,可以有多个RCPT行;服务器端可选择是否愿意为收件人接收邮件。 (4)协商结束,发送邮件,用命令DATA发送。 (5)以“.”号表示结束输入内容一起发送出去,结束此次发送,用QUIT命令退出。SMTP服务器基于域名服务DNS中计划收件人的域名来路由电子邮件。SMTP服务器基于DNS中的MX记录来路由电子邮件,MX记录注册了域名和相关的SMTP中继主机,属于该域的电子邮件都应向该主机发送。若SMTP服务器mail.abc.com收到一封信要发到shuer@sh.abc.com,则执行以下过程: (1)Sendmail 请求DNS给出主机sh.abc.com的CNAME 记录,如有,假若CNAME(别名记录)到shmail.abc.com,则再次请求shmail.abc.com的CNAME记录,直到没有为止。 (2)假定被CNAME到shmail.abc.com,然后sendmail请求@abc.com域的DNS给出shmail.abc.com的MX记录(邮件路由及记录),shmail MX 5shmail.abc.com10 shmail2.abc.com。 (3)Sendmail组合请求DNS给出shmail.abc.com的A记录(主机名(或域名)对应的IP地址记录),即IP地址,若返回值为1.2.3.4(假设值)。 23.4 SMTP协议命令 SMTP命令是发送于SMTP主机之间的ASCⅡ信息,可能使用到的命令如下表所示。 命令 | 描述 | DATA | 开始信息写作 | EXPN<string> | 验证给定的邮箱列表是否存在,扩充邮箱列表,也常被禁用 | HELO<domain> | 向服务器标识用户身份,返回邮件服务器身份 | HELP<command> | 查询服务器支持什么命令,返回命令中的信息 | MAIL FROM<host> | 在主机上初始化一个邮件会话 | NOOP | 无操作,服务器应响应OK | QUIT | 终止邮件会话 | RCPT TO<user> | 标识单个的邮件接收人;常在MAIL命令后面可有多个rcpt to: | RSET | 重置会话,当前传输被取消 | SAML FROM<host> | 发送邮件到用户终端和邮箱 | SEND FROM<host> | 发送邮件到用户终端 | SOML FROM<host> | 发送邮件到用户终端或邮箱 | TURN | 接收端和发送端交换角色 | VRFY<user> | 用于验证指定的用户/邮箱是否存在;由于安全方面的原因,服务器常禁止此命令 | 23.5 SMTP协议响应SMTP响应的一般形式是:XXX Readable Illustration。XXX是3位十进制数;Readable Illustration是可读的解释说明,用来表明命令是否成功等。XXX具有如下的规律:以2开头的表示成功,以4和5开头的表示失败,以3开头的表示未完成(进行中)。常用响应如下所示: SMTP常用响应 响应代码 | 描述 | Positive Completion Reply | 211 | System status or help reply(系统状态或系统帮助响应) | 214 | Help message(帮助信息) | 220 | Service ready(服务就绪) | 221 | Service closing transmission channel(服务关闭) | 250 | Request command completed(要求的邮件操作完成) | 251 | User not local;the message will be forwarded(用户非本地,将转发) | Positive Intermediate Reply | 354 | Start mail input(操作失败) | Transient Negative Completion Reply | 421 | Service not available(服务不可用) | 450 | Mailbox not available(邮箱不可用) | 451 | Command aborted: local error(命令未执行:本地错误) | 452 | Command aborted: insufficient system storage(命令未执行:系统存储不足) | Permanent Negative Completion Reply | 500 | Syntax error: unrecognized command(命令未识别) | 501 | Syntax error in parameters or arguments(参数格式错误) | 502 | Command not implemented(命令不可实现) | 503 | Bad sequence of commands(错误的命令序列) | 504 | Command temporarily not implemented(命令参数不可实现) | 550 | Command is not executed: mailbox unavailable (命令未执行:邮箱不可用) | 551 | User not local(用户非本地) | 552 | Requested action aborted: exceeded storage location (要求的操作未执行:过量的存储分配) | 553 | Requested action not taken: mailbox name not allowed
(要求的操作未执行:邮箱名不可用) | 554 | Transaction failed(操作失败) |
命令和响应的格式是语法,各命令和响应的意思则是语义,各命令和响应在时间上的关系则是同步。 23.6 SMTP例程解析 由于资源受限,在没有操作系统的支持下,通过单片机发送邮件与传统的电脑操作将有很大的不同。这里用W7500EVB与126邮箱通信为例来具体分析邮件的发送过程。在本示例代码中,发件人邮箱名为:wiznet2013@126.com,邮箱密码为:hello123。收件人邮箱地址为:1846955430@qq.com,邮件内容为:Hello!WIZnet!。如果想用别的邮箱做测试的话,请修改代码中收件人和发件人的邮箱名和密码。 本文将发送邮件的整个过程分为3个部分讲解,main.c主程序负责初始化和主循环,smtp.c实现邮件的发送及命令信息的处理,dns.c实现SMTP服务器域名的解析,126邮箱的服务器域名为smtp.126.com,我们调用的其他函数在其他文件中声明。 DNS解析SMTP服务器域名的部分本章前面的章节已经介绍过,这里不再叙述。主函数初始化过程基本一样,主要添加了邮件初始化函数mail_message()。 [mw_shl_code=applescript,true]while(1)
{
ret = do_dns(SOCK_DNS, test_buf); //解析126邮箱服务器IP地址
if(ret)
{
do_smtp(); //发送邮件
if(mail_send_ok)
while(1);
}
}
char hello[50]="HELO localhost"; //身份标识命令
char hello_reply[]="250 OK"; //身份标识成功响应
char AUTH[50]="AUTH LOGIN"; //认证请求
char AUTH_reply[]="334 dXNlcm5hbWU6"; //认证请求发送成功响应
char name_126[100]="wiznet2013@126.com"; //126登录邮箱名
char base64name_126[200]; //126登录邮箱名的base64编码[/mw_shl_code] 用户在使用SMTP例程时,可根据代码注释与自身需求更改以下字符串内容即可。
[mw_shl_code=applescript,true]char name_reply[]="334 UGFzc3dvcmQ6"; //发送登录名成功响应
char password_126[50]="hello123"; //126 登陆邮箱密码
char base64password_126[100]; //base64 126登录邮箱密码
char password_reply[]="235 Authentication successful";//登陆成功响应
char from[]="wiznet2013@126.com"; //发人邮箱
char from_reply[]="250 Mail OK";
char to[]="2429075983@qq.com"; //收件人邮箱
char to_reply[]="250 Mail OK";
char data_init[10]="data"; //请求数据传输
char data_reply[]="354"; //请求成功响应 HEAD
char Cc[]="2429075983@qq.com"; //抄送人邮箱
char subject[]="Hello!WIZnet!"; //主题
char content[]="Hello!WIZnet!"; //正文
char mime_reply[]="250 Mail OK queued as"; //邮件发送成功响应
char mailfrom[50]="MAIL FROM:<>";
char rcptto[50]="rcpt to:<>";
char mime[200]="From:\r\n";
char mime1[50]="To:\r\n";
char mime2[50]="Cc:\r\n";
char mime3[50]="Subject:\r\n";
char mime4[50]="MIME-Version:1.0\r\nContent-Type:text/plain\r\n\r\n";
char mime5[50]="\r\n.\r\n";[/mw_shl_code] 邮件发送具体过程很简单,先解析126邮箱的服务器域名smtp.126.com,成功以后就执行邮件发送函数,邮件发送成功以后就跳出循环。主循环处判断到邮件发送成功后让程序进入了一个死循环,这样程序将不再跳到邮件发送函数去,避免重复发送相同的邮件,这样使得在W7500EVB的运行模式下,按一下Reset键或者上电一次,只发送一封邮件。下面介绍发送邮件主函数: [mw_shl_code=applescript,true]1. void do_smtp(void) //SMTP 主函数
2. {
3. uint8_t ret;
4. uint8_t ch=SOCK_SMTP; //定义一个变量并赋值SMTP通信socket号
5. uint16_t len;
6. uint16_t anyport=5000; //定义SMTP Client的通信端口号
7. uint8_t Smtp_PORT=25; //SMTP Server 的端口号,默认为25
8. memset(RX_BUF,0,sizeof(RX_BUF));
9. switch(getSn_SR(ch)) //读取W7500的socket状态
10. {
11. case SOCK_INIT: //初始化完成
12. connect(ch, ConfigMsg.rip ,Smtp_PORT );//连接SMTP Server
13. break;
14. case SOCK_ESTABLISHED: //socket建立成功
15. if(getSn_IR(ch) & Sn_IR_CON)
16. {
17. setSn_IR(ch, Sn_IR_CON); //清除接收中断标志
18. }
19. if ((len = getSn_RX_RSR(ch)) > 0)
20. {
21. while(!mail_send_ok) //如果邮件没有发送成功
22. {
23. memset(RX_BUF,0,sizeof(RX_BUF));//接受缓存的内存空间清零
24. len = recv(ch, (uint8_t*)RX_BUF,len); //W7500接收数据并存入RX_BUF
25. send_mail(); //发送邮件
26. }
27. disconnect(ch); //断开socket连接
28. }
29. break;
30. case SOCK_CLOSE_WAIT: /*等待socket关闭*/
31. if ((len = getSn_RX_RSR(ch)) > 0)
32. {
33. while(!mail_send_ok)
34. {
35. len = recv(ch, (uint8_t*)RX_BUF, len);
36. send_mail();
37. }
38. }
39. close(ch);
40. break;
41. case SOCK_CLOSED: //socket关闭
42. if((ret=socket(ch, Sn_MR_TCP, anyport, 0x01)) != ch)//开启socket
43. { //移植过程中,W7500旧版本库flag标志位用0x01和0X00没有区太大影响,但是用iolibary库时,一定改为非零的,一般为0X01。
44. //一定注意这个问题,也是之前不能调试成功的问题所在
45. printf(" %d\r\n",ret);
46. }
47. break;
48. default:
49. break;
50. }
51. }[/mw_shl_code] 由于SMTP发送邮件使用TCP协议,是面向链接的可靠传输。我们这里还是使用熟悉的TCP状态机来实现数据交互。第4行定义W7500的一个socket用于SMTP通信,第5行定义一个变量用于存储W7500接收到的数据长度,第6、7行就是分别定义client、server端的端口号,这里需要注意的是SMTP服务器默认监听的TCP端口是25。因此第6行里面的W7500本地端口可以随便设置。第8行把用于保存W7500接收数据缓存的RX_BUF清空。 通过第9行获取socket 状态,然后根据在后面的switch中针对socket的不同状态做不同的操作。第11行,当时socket处于初始完成状态时,向SMTP服务器发送链接请求。第14行,当socket处于连接建立状态时,清空Sn_IR响应的中断位。如果邮件没有发送成功,就执行接收SMTP Server的响应,发送邮件,直到邮件发送OK,然后断开连接。第30行,当socket处于等待关闭状态时,由于socket此时还能进行数据交互,所以执行动作就和socket连接建立状态相同。第42行,当处于socket关闭状态时,初始化socket,并将其配置为TCP 模式。下面介绍邮件具体的发送过程。
|