第二十三章 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()。 - 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编码
复制代码用户在使用SMTP例程时,可根据代码注释与自身需求更改以下字符串内容即可。 - 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";
复制代码
邮件发送具体过程很简单,先解析126邮箱的服务器域名smtp.126.com,成功以后就执行邮件发送函数,邮件发送成功以后就跳出循环。主循环处判断到邮件发送成功后让程序进入了一个死循环,这样程序将不再跳到邮件发送函数去,避免重复发送相同的邮件,这样使得在W7500EVB的运行模式下,按一下Reset键或者上电一次,只发送一封邮件。下面介绍发送邮件主函数: - 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. }
复制代码
由于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 模式。下面介绍邮件具体的发送过程。 - 1. uint8_t SMTP_STATE=waitfor220;
- 2. void send_mail(void)
- 3. {
- 4. switch(SMTP_STATE)
- 5. {
- 6. case waitfor220: //等待连接成功的正确响应状态
- 7. if(strstr((const char *)RX_BUF,"220")!=NULL)
- 8. {
- 9. send(SOCK_SMTP,(uint8_t *)hello,strlen(hello));//发送hello命令
- 10. SMTP_STATE=waitforHELO250;
- 11. }
- 12. break;
- 13. case waitforHELO250: //等待hello命令的正确响应状态
- 14. if(strstr((const char *)RX_BUF,hello_reply)!=NULL&&strstr((const char *)RX_BUF,"Mail")==NULL)
- 15. {
- 16. send(SOCK_SMTP,(uint8_t *)AUTH,strlen(AUTH));//发送AUTH认证请求
- 17. SMTP_STATE=waitforAUTH334;
- 18. }
- 19. break;
- 20. case waitforAUTH334: //等待AUTH认证请求的正确响应
- 21. if(strstr((const char *)RX_BUF,AUTH_reply)!=NULL)
- 22. {
- 23. send(SOCK_SMTP,(uint8_t *)base64name_126,strlen(base64name_126));//发送邮箱登录名
- 24. SMTP_STATE=waitforuser334;
- 25. }
- 26. break;
- 27. case waitforuser334: //等待邮箱登录名登陆成功的正确响应
- 28. if(strstr((const char *)RX_BUF,name_reply)!=NULL)
- 29. {
- 30. send(SOCK_SMTP,(uint8_t *)base64password_126,strlen(base64password_126));//发送邮箱登录密码
- 31. SMTP_STATE=waitforpassword235;
- 32. }
- 33. break;
- 34. case waitforpassword235: //等待邮箱密码登陆成功正确响应
- 35. if(strstr((const char *)RX_BUF,password_reply)!=NULL)
- 36. {
- 37. send(SOCK_SMTP,(uint8_t *)mailfrom,strlen(mailfrom));//发送发件人信息
- 38. SMTP_STATE=waitforsend250;
- 39. }
- 40. break;
- 41. case waitforsend250: //等待发件人信息发送成功正确响应
- 42. if(strstr((const char *)RX_BUF,from_reply)!=NULL&&strstr((const char *)RX_BUF,"queued as")==NULL)
- 43. {
- 44. send(SOCK_SMTP,(uint8_t *)rcptto,strlen(rcptto)); //发送收件人信息
- 45. SMTP_STATE=waitforrcpt250;
- 46. }
- 47. break;
- 48. case waitforrcpt250: //等待收件人信息发送成功正确响应
- 49. if(strstr((const char *)RX_BUF,to_reply)!=NULL)
- 50. {
- 51. send(SOCK_SMTP,(uint8_t *)data_init,strlen(data_init));//发送数据传输请求命令
- 52. SMTP_STATE=waitfordate354;
- 53. }
- 54. break;
- 55. case waitfordate354: //等待发送数据传输请求命令发送成功正确响应
- 56. if(strstr((const char *)RX_BUF,data_reply)!=NULL)
- 57. {
- 58. send(SOCK_SMTP,(uint8_t *)mime,strlen(mime)); //发送mime类型命令
- 59. SMTP_STATE=waitformime250;
- 60. }
- 61. break;
- 62. case waitformime250: //等待mime的正确响应
- 63. if(strstr((const char *)RX_BUF,mime_reply)!=NULL)
- 64. {
- 65. mail_send_ok=1; //邮件发送成功
- 66. printf("mail send OK\r\n");
- 67. }
- 68. break;
- 69. default:
- 70. break;
- 71. }
- 72. }
复制代码
接下来是发送邮件的具体过程,同样是个状态机。根据SMTP 服务器的响应,我们一共定义了9个不同的状态。 第6行,TCP连接建立后,等待SMTP 服务器的220响应,如果接收到服务器成功回应,发送HELO localhost的命令,进入等待250OK响应的状态。 第13行,如果SMTP 服务器正确回应,发送AUTH 认证命令,等待认证请求的正确响应。 第20行,当AUTH认证请求正确回应时,发送BASE64编码的登录名,进入等待SMTP 服务器正确响应。 第27行,当服务器成功回应时,发送经过BASE64编码的登录密码。 第34行,password235成功响应,发送发件人邮箱名。 第41行,如果得到250 Mail OK的响应,发送收件人邮箱。 第48行,如果收到250 Mail OK 的响应,请求发送数据,发送DATA 命令,进入等待354报头格式的响应。 第55行,发送mime类型的邮件正文,然后等待服务器的回应。 第62行,如果SMTP Server成功回应,邮件发送成功,此时把邮件发送成功的标志位置1。 程序编译无误以后烧录,打开串口工具,首先解析到smtp.126.com邮件服务器的IP,然后发送邮件,如果发送成功,会得到mail send OK的信息,结果如图23.6.1
图23.6.1 SMTP例程打印与邮件接收截图
|