美文网首页呆鸟的Python数据分析互联网科技
当你发邮件时,你的电脑都干了什么?借助Python探索SMTP协

当你发邮件时,你的电脑都干了什么?借助Python探索SMTP协

作者: 爱科学的程序员小刘 | 来源:发表于2018-11-03 17:34 被阅读22次

    Oct. 30th, 2018

    你是否需要每天使用电子邮件服务?

    电子邮件(email)是互联网上历史悠久又常用的消息收发形式。对于大多数办公室一族,每天到班上的第一件事恐怕就是要查一下新的邮件。虽然即时通信工具在飞速占领着通信市场,但是在商业或者学术圈里,email依然占据着主流地位。

    一般收发email,要么使用电子邮件管理工具(如Outlook, Mac的Mail等),要么登录网页版的email服务网页(gmail, 126等等)。作为一个好奇心很强的程序员,我一直很想知道当自己编辑了一封邮件,点击发送的时候,自己的电脑/手机都在我背后干了什么?收发email的服务器是如何工作的?借助Python的TCP接口和smtplib,我们可以很容易就可以写一个“email服务器“。这里email服务器加了引号,是因为它只是假装自己是一个email服务器(见文尾的讨论)。但是,这个假服务器其实已经具备了真服务器的所有逻辑功能。如果你希望,你完全可以把它做成一个可以正常工作的email服务器!(文尾也会大体讲解如何去做,但是需要你购买一个域名并向DNS服务器进行相应的注册。)

    当你点击“发送”之后,你的邮箱做了那些操作?今天的实验带你亲自看一看。

    引言:互联网协议和SMTP协议

    在之前的一篇文章(点击这里查看)中,笔者讨论了互联网协议的几个层,并且构建实验探讨了提供网页服务的HTTP服务器如何工作。如果你也喜欢探索原理,并且还没有做过这个实验,那么强烈建议你点开连接,跟着文章中设计的实验探索一下HTTP协议。这里,我们会做一个类似的实验来窥探email收发使用的SMTP协议。

    简单概括原理:我们的互联网分为四个层,每一层的正常工作建立在下面层的基础上。工作在最上层的“应用层”,有提供网页服务的HTTP协议,提供邮件收发的SMTP协议,提供文件传输的FTP协议等等。这些协议想要正常工作,都要基于下面“传输层”的支持。传输层比较常用的是TCP协议。今天的实验里,我们将在SMTP层和TCP层两个层面上观察SMTP协议,并且在TCP层上构造一个简单的,需要手动控制的“SMTP服务器”。

    这张图诠释了SMTP连接。当我们说SMTP通信时,它其实是一种虚拟的,抽象的说法。真正建立连接的,是在下面的层上

    实验0(准备工作):查看email服务器地址

    在真正开始实验之前,我们先看一下如何从一个email地址出发,查询它对应的SMTP服务器域名。比如example@126.com,我们知道它的email域名是126.com。但是我们需要知道,126.com背后的SMTP服务器地址是多少。为了便于区分,通常管126.com叫做email域名,而其背后的SMTP服务器地址,叫做mx域名。(mx是mail exchange的缩写。)

    这里我们使用工具nslookup查询mx域名。无论你使用的是Windows系统,还是Mac OS,还是Linux,nslookup都已经存在于你的电脑里了。使用它的步骤如下:

    1. 打开命令行。Windows系统:打开“开始”菜单,输入"cmd",搜索到“命令提示行”工具。打开后界面如下。
    windows中的命令提示行

    Mac OS在应用程序中找到"Terminal"。Linux我就不说命令行在哪里了。

    1. 在命令行中输入nslookup按回车,进入nslookup工具中。输入set q=mx,指定查询mx域名。输入126.com,按下回车,你就会得到查询结果。
    nslookup查询126.com的mx域名

    图中可以看到,126.comemail域名背后有4个mx服务器。后面的讨论中,使用任何一个mx服务器(比如“126mx01.mxmail.netease.com”)都可以。友情提示:复制mx域名时,注意不要把最后的句号复制上了。

    实验1(应用层):使用Python发送email

    Python提供了一个很强大的包进行email的相关操作:smtplib。在这一节,我们通过使用这个包,熟悉一下smtp协议工作的基本步骤,下一节,我们再深入到smtp协议的底层。

    如果你想知道如何姿势正确地发送邮件,你可以参见这一篇文章。这里说的“姿势正确”,意思是你将会使用smtplib登录到你使用的邮箱服务器(比如你注册了新浪的邮箱,你就可以使用smtpliblogin函数登录到新浪邮箱服务器上),然后再对你的目标收件人(比如126的email邮箱)发邮件。只要正确设置,这样发邮件一定不会有问题,因为有你的邮件服务提供商(这个例子里是新浪)给你撑腰,对方的邮箱(这里是126邮箱)不敢拒绝你的邮件。

    但是,这里笔者只想讨论smtp协议的结构。严格来说login并算不上smtp协议的要求(至少并不是基本要求)。很多接收者的邮箱,并不需要发件人有一个具体的email地址,只需要收件人的email地址明确,邮件内容格式正确就可以了。是的,你不需要自己有一个email地址才可以给别人发邮件!

    当然,很多时候这样的邮件会被对方SMTP服务器拒收。即时接收了,也有可能因为来源不明而被放到垃圾邮件里。所以,并不建议读者用这种收发日常邮件。但是为了弄懂SMTP的协议,这样做一两次还是值得的。

    话不多说,先上一个完整的邮件发送的截图。注意,我作为发件人,并没有登录任何自己的邮箱。另外,注意变量s_body的格式。大部分邮件服务器对这个格式很看重。不符合这个格式的邮件经常会被拒绝。最后,注意在server.connect那一行运行之后,后面手速一定要快。隔一会再执行下一行的话,对方服务器通常会断开。

    Python中使用smtplib发送邮件。要先把收件人地址、发件人地址和邮件内容都实现编辑好,存在变量中。避免连接到服务器之后,由于超时没有响应而导致服务器断开连接

    大概来看一下代码里面都干了什么。

    1. 创建了一个叫server的SMTP对象。在调用connect方法之前,事先准备好了收件人、发件人和邮件内容的字符串s_from, s_tos_body。因为服务器允许的连接时间有限。特别注意s_body字符串的格式。
    2. server.connect(...)连接126的mx服务器。这里用到的是上一节中nslookup查询到的信息。
    3. server.sendmail(...)发送邮件。返回{},就说明发送成功了。

    So far so good。 但是,作为一个好奇宝宝,你肯定要问:server.sendmail到底干了什么?使用server.sendmail发邮件,跟打开outlook写好了邮件点发送,似乎并没有太大提高。为了探寻后面发生了什么,我们就要祭出TCP socket了。

    实验2 (传输层):使用TCP socket假装自己是个SMTP服务器

    怎么才能搞清楚server.sendmail背着你跟服务器干了什么事情呢?好吧,换个问题:假设你怀疑你对象在网上见了漂亮妹子/帅气男生就勾搭,怎么才能抓住他/她的把柄呢?一个方法就是,自己注册个上网账号,把自己伪装成漂亮妹子/帅气男生,跟他/她聊。

    我们知道,像requests一样,smtplib要想进行SMTP通信,一定会使用下面传输层的TCP协议,跟对方的SMTP服务器建立TCP连接。所以,我们就像上一篇文章那样,准备一个TCP连接,把对方发过来的数据都显示到屏幕上,具体哪些消息,什么格式,就一目了然了。

    跟http请求不同的是,server.sendmail不是一个单次的请求/响应,而是要求双方使用协议规定的格式反复提问回答几次才可以完成邮件发送。因此,我们的服务器里也要仔细设置响应的内容,确保返回的东西符合格式,使得对话能继续进行。(也就是说,想假装自己是SMTP服务器,比假装自己是HTTP服务器要难一点,穿帮的可能性也更大一点。)为了增强体验感,我们在每次收到信息时,让我们手动填写返回内容。

    我们先来看代码。

    """
    这是一个虚拟服务器。当任何程序简介到它时,它先发送一条欢迎信息WELCOME_MSG,
    然后等待对方发送信息。对方每发送一条信息,它就会把信息显示到屏幕上,然后提示
    我们输入应答内容。紧接着,它会把我们输入的内容后面加上换行符\r\n,发送回去。
    """
    
    SERVER_IP = "localhost"
    SERVER_PORT = 25            #默认的SMTP之一
    MAX_LENGTH = 1023       #规定每条信息长度上限。
    WELCOME_MSG = "220 Virtual Server At Your Service!\r\n" #欢迎信息
    
    socket_list = []
    
    import socket
    
    def close_sockets(): #再程序出现异常退出时关闭所有端口,避免端口占用
        for sock in socket_list:
            sock.close()
    
    def main():
        
        sock_listen = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        socket_list.append(sock_listen)
    
        sock_listen.bind((SERVER_IP, SERVER_PORT))
        sock_listen.listen(1)
        conn, addr = sock_listen.accept()
        socket_list.append(conn)
    
        # Once connection is built: send welcome message and print connection
        print("Connection established. From: " + str(addr))
        if WELCOME_MSG is not None and WELCOME_MSG != '': # 如果WELCOME_MSG为''或者None,
                                  # 则不发送欢迎信息,链接建立后
                                  # 直接进入接收信息状态
            conn.send(WELCOME_MSG.encode())
            print("欢迎信息发出!")
            
    
        while True:
            print("等待对方应答... 信息长度上限: " + str(MAX_LENGTH))
            data = conn.recv(MAX_LENGTH)
            print("收到信息:\n", data)
            if len(data) == 0:
                break
            reply_msg = input("您的回复: ")
            reply_msg += '\r\n'
            conn.send(reply_msg.encode())
            print("回复消息发出!")
    
    if __name__ == "__main__":
        try:
            main()
        except Exception as err:
            print(str(err))
            close_sockets()
            exit()
    

    这段代码比较直接。代码里的注释或者print的提示字都描述着每一部分代码的功能。

    有了这个人工服务器,我们就可以拿它接收smtplib发来的请求了。前面的演示用了Windows和Mac OS的电脑。为了不偏心,这里就用Linux的电脑做演示了。(我才不会告诉你,其实是因为Windows电脑老婆在用,而Mac电脑落在办公室里了T_T)

    1. 服务器开启。注意由于使用了25端口(SMTP协议的默认端口之一),为系统预留端口,因此程序需要管理员权限。第一幅图中,第一次尝试由于没有用管理员权限sudo而被拒绝执行了。加了sudo程序得以启动,并开启端口,等待连接。
    开启服务器。第一次由于没有使用管理员权限而被拒绝。第二次成功开启,进入等待连接状态
    1. 使用Python3的smtplib连接SMTP服务器。这一步跟上面实验是一样的。注意server.connect连接的是'localhost'。此时,右边图中服务器也显示收到了连接,并发送了欢迎消息'220 Virtual Server At Your Service!' 这时从smtplib接收到的信息来看,它识别了这种状态码<空格>回复信息的格式,返回了一个二元素的数组。
    python的smtp连接,服务器发送来欢迎信息

    接下来,我们就重复上面实验中的做法,定义s_from, s_tos_msg,然后交给server.sendmail函数来以邮件形式发送出去。见下图。

    使用smtplib的sendmail发送邮件
    1. 接下来的图很重要!它显示了sendmail函数执行后,服务器上收到的一连串信息
    sendmail函数给服务器发送的一系列SMTP消息。从`helo`开始

    注意,这里为了把换行符也都显示出来,特意没有将Bytes型字符串解码成普通的Python3字符串(即,没有调用decode方法)。所以图里每条信息前面都有个b

    这段对话是这样的:

    sendmail: helo [自己地址]
    我: 250 Nice to meet you # 注意格式是“状态代码<空格>响应信息”。
    sendmail: mail FROM:<发件人地址>
    我: 250 Ok
    sendmail: rcpt TO: <收件人地址>
    我: 250 Ok
    sendmail: data # 这个data单词是告诉对方,注意,我后面要开始发送正文了!
    我: 354 Go ahead # 这里状态码也不再是250,而是354,表示“我等你发信息”
    sendmail: <邮件正文> # 注意:这段文字最后的\r\n.\r\n是SMTP协议定义的data结束符号。
    我: 250 Received

    到这里,sendmail函数就完成了它的任务,发出一封邮件。其实,sendmail正常工作,分析的是每一个请求对方发来的状态码(250, 354这些)。比如sendmail发送data字符的时候,如果你还是回复250而不是354的话,sendmail会认为你这个服务器有问题,就不再理你了。与之相比,后面的响应信息的具体内容,SMTP协议是没有具体要求的。所以才会有五花八门的回复。比如,gmail的服务器响应helo的内容是"at your service",而我这里写的是"Nice to meet you"。

    一切发送完毕后,sendmail返回了熟悉的{},即空字典,表示信息发送成功了。后面我又调用了server.quit()结束对话。从下图可以看到,这个函数在断开连接之前,先给服务器发送了quit信息。我响应了221 bye之后,它才关闭了TCP连接。

    sendmail返回`{}`之后,调用quit函数发送“结束通信”的消息

    总结

    这一篇实验有点长。在准备阶段(实验0),我们了解了如何使用nslookup查询一个邮件域名对应的MX服务器地址。实验1中,我们使用Python的smtplib包发送邮件,观察了在应用层(SMTP层)上的情况,并且掌握了smtplib的使用方法。实验2中,我们下潜到TCP层,开启了一个简单的人工SMTP服务器,接收smtplib发来的邮件发送请求。看到了helo, mail FROM, rcpt TO, data, quit这些标准的SMTP请求报文,也了解了服务器响应信息的"状态代码<空格>响应消息"格式。顺便说一下,其实smtplib里也是提供了server.helo, server.mail,server.rcptserver.data这些函数的。有兴趣的读者可以自己尝试一下。

    通过亲手进行这个实验,相信读者会对SMTP协议有一个更直观的了解,以后再发邮件的时候,脑子里会不会自动浮现出你的邮件管理程序在背后发送的这一连串请求呢?

    感谢您的支持,如果您有任何疑问,或者建议,或者还想看什么简单的探索计算机的小实验,欢迎给我留言!

    相关文章

      网友评论

      本文标题:当你发邮件时,你的电脑都干了什么?借助Python探索SMTP协

      本文链接:https://www.haomeiwen.com/subject/ncnkxqtx.html