zoukankan      html  css  js  c++  java
  • 深入理解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
    '
        command_send(heloCommand, '250')
    
        authLoginCommand = 'AUTH LOGIN
    '
        command_send(authLoginCommand, '334')
    
        user = b64encode('你的QQ邮箱账户'.encode()) + b'
    '
        command_send(user, '334', 'bytes')
    
        pwd = b64encode('你的QQ邮箱授权码'.encode()) + b'
    '
        command_send(pwd, '235', 'bytes')
    
        mailFromCommand = 'MAIL FROM: <发送方邮箱地址>
    '
        command_send(mailFromCommand, '250')
    
        reptToCommand = 'RCPT TO: <接收方邮件地址>
    '
        command_send(reptToCommand, '250')
    
        dataCommand = 'DATA
    '
        command_send(dataCommand, '354')
    
        msg = "FROM: 发送方邮箱地址
    TO: 接收方邮件地址
    Subject: Say Hello
    
    Hello World!"
        clientSocket.send(msg.encode())
    
        endmsg = '
    .
    '
        command_send(endmsg, '250')
    
        quitCommand = 'QUIT
    '
        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方式,具体步骤如下:

    1. 客户端发送AUTH LOGIN命令,指示服务器进行身份认证;
    2. 客户端收到334 VXNlcm5hbWU6响应后发送BASE64编码的账户名;
    3. 客户端收到334 UGFzc3dvcmQ6响应后发送BASE64编码的密码;
    4. 客户端收到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: 发送方邮箱地址
    TO: 接收方邮箱地址
    Subject: Transmit Image by E-mail
    MIME-Version: " 
              "1.0
    Content-Type:multipart/related; boundary='12345678'
    
    --12345678
    Content-Type: text/html; " 
              "charset=UTF-8
    Content-Transfer-Encoding: base64
    
    ".encode()
        msg += html_data
        msg += "
    
    --12345678
    Content-Type: image/jpeg; name='test.jpg'
    Content-Transfer-Encoding: " 
               "base64
    Content-ID: image1
    
    ".encode()
        msg += image_data
        msg += "
    
    --12345678--
    ".encode()
        clientSocketSSL.send(msg)
    
        ...省略...
    

    可以看到,修改后的邮件由两部分组成,一部分是HTML数据,另一部分则是图片数据。此处我们通过HTML将图片嵌入到了邮件正文中,具体方法是在HTML中通过引用src="cid:0"就可以把附件作为图片嵌入了。如果有多个图片,则依次给它们编号,然后引用不同的cid:x即可,例如这里我们将test.jpg编号为image1

    邮件的最终效果如下图所示:

    mail.PNG

  • 相关阅读:
    Chapter 03Using SingleRow Functions to Customize Output(03)
    Chapter 03Using SingleRow Functions to Customize Output(01)
    Chapter 04Using Conversion Functions and Conditional ExpressionsNesting Functions
    Chapter 04Using Conversion Functions and Conditional ExpressionsGeneral Functions
    Chapter 11Creating Other Schema Objects Index
    传奇程序员John Carmack 访谈实录 (zz.is2120)
    保持简单纪念丹尼斯里奇(Dennis Ritchie) (zz.is2120.BG57IV3)
    王江民:传奇一生 (zz.is2120)
    2011台湾游日月潭
    2011台湾游星云大师的佛光寺
  • 原文地址:https://www.cnblogs.com/marvin-wen/p/15191694.html
Copyright © 2011-2022 走看看