深入理解SMTP协议之邮件客户端
本文将使用Python从零实现一个简易的邮件客户端,通过本文你将对SMTP协议有更深入的了解,同时掌握使用Python实现标准协议的经验。
我们将开发一个简单的邮件客户端,将邮件发送给任意收件人。我们的客户端将需要连接到邮件服务器(QQ邮件服务器),使用SMTP协议与邮件服务器进行对话,并向邮件服务器发送电子邮件。
Python提供了一个名为smtplib
的模块,它内置了使用SMTP协议发送邮件的方法。但是我们不会使用此模块,因为它隐藏了SMTP和套接字编程的细节,我们将完全从零开始实现自己的邮件客户端。
1.基本邮件客户端
我们先来了解SMTP客户和SMTP服务器之间交换报文时在客户端需要发送哪些命令,服务器又是如何对每个命令作出回答,其中每个回答含有一个响应码和英文解释。
命令 | 含义 | 响应码及其英文解释 |
---|---|---|
HELO <domain><CRLF> | HELLO的缩写,客户端为标识自己的身份而发送的命令,通常带域名 | 220 <domain> Service ready |
MAIL FROM: <reverse-path><CRLF> | 标识邮件的发件人,<reverse-path>为发送者的地址,此命令告诉接收方一个新邮件发送的开始,并对所有的状态和缓冲区进行初始化 | 250 Requested mail action okay, completed |
RCPT TO: <forward-path><CRLF> | 标识邮件的收件人,<forward-path>为收件人的地址 | 250 Requested mail action okay, completed |
DATA<CRLF> | 标识邮件数据传输的开始,<CRLF>.<CRLF>标识数据的结尾,客户端发送的、用于启动邮件内容传输的命令 | 354 Start mail input; end with <CRLF>.<CRLF> |
QUIT<CRLF> | 表示会话的终止 | 221 <domain> Service closing transmission channel |
注意:<CRLF>中的CR和LF分别表示回车和换行。SMTP响应码的每一个数字都是有特定含义的,如第一位数字为2时表示命令成功,为5时表示失败,为3时表示没有完成。
以上命令是正常完成一次邮件传输的必不可少的命令,我们将在后面的代码中用到它们,更多的命令这里不做更多的介绍。下面我们来看代码实现:
from socket import *
from base64 import b64encode
global clientSocket
def init():
global clientSocket
mail_server = 'smtp.qq.com'
clientSocket = socket(AF_INET, SOCK_STREAM)
while True:
clientSocket.connect((mail_server, 25))
recv = clientSocket.recv(1024).decode()
print(recv)
if recv[:3] == '220':
print('成功与邮件服务器建立TCP连接!')
break
def command_send(command, success_code, data_type='str'):
if data_type == 'str':
command = command.encode()
while True:
print(command)
clientSocket.send(command)
recv = clientSocket.recv(1024).decode()
print(recv)
if recv[:3] == success_code:
break
else:
print('Failed')
if __name__ == "__main__":
init()
heloCommand = 'HELO Alice\r\n'
command_send(heloCommand, '250')
authLoginCommand = 'AUTH LOGIN\r\n'
command_send(authLoginCommand, '334')
user = b64encode('你的QQ邮箱账户'.encode()) + b'\r\n'
command_send(user, '334', 'bytes')
pwd = b64encode('你的QQ邮箱授权码'.encode()) + b'\r\n'
command_send(pwd, '235', 'bytes')
mailFromCommand = 'MAIL FROM: <发送方邮箱地址>\r\n'
command_send(mailFromCommand, '250')
reptToCommand = 'RCPT TO: <接收方邮件地址>\r\n'
command_send(reptToCommand, '250')
dataCommand = 'DATA\r\n'
command_send(dataCommand, '354')
msg = "FROM: 发送方邮箱地址\r\nTO: 接收方邮件地址\r\nSubject: Say Hello\r\n\r\nHello World!"
clientSocket.send(msg.encode())
endmsg = '\r\n.\r\n'
command_send(endmsg, '250')
quitCommand = 'QUIT\r\n'
command_send(quitCommand, '221')
发送命令和接收响应是每一步都需要做的事情,因此我们其封装到command_send
函数中以降低代码冗余,事实上每一个步骤都有出错的可能,而我们处理差错的方法也很简单:一直循环直到成功为止。
通过观察代码不难发现AUTH LOGIN
命令是我们没介绍的,这是因为我们所采用的QQ邮件服务器要求进行安全认证。最初的SMTP协议并不包含安全认证,而ESMTP通过增加命令EHLO和AUTH在安全性方面扩展了SMTP。如今的SMTP服务器,无论是公网的还是内网的,大多都要求安全认证。
通过向服务器发送EHLO
命令(格式为EHLO <domain><CRLF>
),客户端可以了解到服务器是否支持扩展简单邮件传输协议(ESMTP)。我们向QQ邮件服务器发送该命令收到的响应如下:
250-newxmesmtplogicsvrsza5.qq.com
250-PIPELINING
250-SIZE 73400320
250-STARTTLS
250-AUTH LOGIN PLAIN XOAUTH XOAUTH2
250-AUTH=LOGIN
250-MAILCOMPRESS
250 8BITMIME
上述响应中的AUTH LOGIN PLAIN XOAUTH XOAUTH2
说明了SMTP服务器支持的验证方式,这里我们采用的是LOGIN
方式,具体步骤如下:
- 客户端发送
AUTH LOGIN
命令,指示服务器进行身份认证; - 客户端收到
334 VXNlcm5hbWU6
响应后发送BASE64编码的账户名; - 客户端收到
334 UGFzc3dvcmQ6
响应后发送BASE64编码的密码; - 客户端收到
235 Authentication successful
响应后表明身份验证成功;
注意:对于QQ邮件服务器而言,步骤3中输入的不是账户的登录密码而是授权码,在QQ邮箱中开通POP3/SMTP服务需要生成授权码,具体方法此处不介绍。
2.添加安全套接字层
SSL(Secure Sockets Layer)
即安全套接层,是由Netscape公司于1990年开发,用于保障World Wide Web(WWW)
通讯的安全。其主要任务是提供私密性,信息完整性和身份认证。
SSL是一个不依赖于平台和运用程序的协议,位于TCP/IP
协议与各种应用层协议之间,为数据通信提高安全支持。HTTP是第一个使用SSL保障安全的应用层协议,我们常见的HTTPS的全称就是HTTP over SSL
。
注意:HTTPS默认工作在443端口,而HTTP默认工作在80端口。
与HTTPS类似,诸如SMTP、POP3、IMAP等邮件协议也能支持SSL,基于SSL安全协议的SMTP协议被称为SMTPS(SMTP over SSL)
。在本例中,为了添加安全套接层,我们需要对init()
函数进行修改,具体代码如下:
import ssl
def init():
global clientSocket
mail_server = 'smtp.qq.com'
clientSocket = socket(AF_INET, SOCK_STREAM)
context = ssl.create_default_context()
while True:
clientSocket.connect((mail_server, 465))
clientSocket = context.wrap_socket(sock=clientSocket, server_hostname=mail_server)
recv = clientSocketSSL.recv(1024).decode()
print(recv)
if recv[:3] == '220':
print('成功与邮件服务器建立TCP连接!')
break
context
是SSL创建的默认上下文对象,对象中保存了我们对证书的认证与加密算法选择的偏好设置。通过调用上下文对象的wrap_socket()
方法,表示由OpenSSL库负责控制我们的TCP链接,然后与通信对方交换必要的握手信息,并建立加密链接,最终返回一个SSLSocket
对象,该对象负责进行所有的后续通信。
在与邮件服务器建立连接的过程中,我们连接的端口号是465,而不是25。这是因为465号端口是为SMTPS协议服务开放的,而25号端口是为SMTP协议服务开放的。
3.发送图像信息
到目前为止,尽管我们的SMTP邮件客户端可以正常工作,但是只能在电子邮件的正文中发送文本消息。本节我们将修改客户端代码,使其可以发送包含文本和图像的电子邮件。
其实要实现我们的功能很简单,只需要对邮件的格式动动手脚就可以了。下面简要说明一下邮件的格式:
邮件是由邮件头和邮件体构成的,邮件体又可能由文本、超文本和附件等多个部分构成,当在同一邮件体内有多个不同的数据集合时,我们必须在邮件头中通过multipart
参数值显式地指出这一点。邮件体的不同子部分之间是通过边界boundary
封装的,每一部分都会由边界开始,然后包含着邮件子体的头信息(header),空行,然后是邮件正文。需要指出的是,最后一个子部分的后面必须跟一个结尾边界。
关于boundary
的使用方法,我们可以在Content-type
字段的后面是把boundary
的值包含在引号之中。也可以没有引号,但有引号是最保险的。当有一些非法字符出现在boundary
值中时,如果不加引号可能会引起错误。在使用边界封装邮件时,其使用方法是在值的前面加两个-
。其中,对于最后一个部分的结尾边界,还需要在值的后面再加两个-
。
对于客户端代码,其余部分不变,我们只需要修改邮件部分。接下来我们看下修改后的邮件部分:
if __name__ == "__main__":
...省略...
html_data = b64encode(b'<img src="cid:image1">')
with open("test.jpg", "rb") as f:
image_data = b64encode(f.read())
msg = "FROM: 发送方邮箱地址\r\nTO: 接收方邮箱地址\r\nSubject: Transmit Image by E-mail\r\nMIME-Version: " \
"1.0\r\nContent-Type:multipart/related; boundary='12345678'\r\n\r\n--12345678\r\nContent-Type: text/html; " \
"charset=UTF-8\r\nContent-Transfer-Encoding: base64\r\n\r\n".encode()
msg += html_data
msg += "\r\n\r\n--12345678\r\nContent-Type: image/jpeg; name='test.jpg'\r\nContent-Transfer-Encoding: " \
"base64\r\nContent-ID: image1\r\n\r\n".encode()
msg += image_data
msg += "\r\n\r\n--12345678--\r\n".encode()
clientSocketSSL.send(msg)
...省略...
可以看到,修改后的邮件由两部分组成,一部分是HTML数据,另一部分则是图片数据。此处我们通过HTML将图片嵌入到了邮件正文中,具体方法是在HTML中通过引用src="cid:0"
就可以把附件作为图片嵌入了。如果有多个图片,则依次给它们编号,然后引用不同的cid:x
即可,例如这里我们将test.jpg
编号为image1
。
邮件的最终效果如下图所示:
原文地址:https://www.cnblogs.com/marvin-wen/p/15191694.html
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法