IMAP协议-从网易邮箱的骚操作到QQ邮箱的BUG

前言

本文主要是记录一下在使用 IMAP 协议访问网易邮箱时遇到的一个很常见的问题以及之后我是如何解决并一步一步发现 QQ 邮箱 BUG 的过程。

发现问题

由于工作需要,开发的系统中需要访问用户设置的邮箱内的邮件列表。我们选用的方式是采用 IMAP 协议进行读取。简化后的代码如下:

1
2
3
4
5
import imaplib

client = imaplib.IMAP4_SSL(host="imap.163.com", port=993)
client.login("username", "password")
client.select()

一般邮箱都没有问题,但在访问网易邮箱时,select 命令返回的结果是:

1
# ('NO', [b'SELECT Unsafe Login. Please contact kefu@188.com for help'])

解决问题

原因分析

首先,百度一下很容易就知道答案,此处不赘述。可参考文章最后的引用链接。

简述一下就是:

网易邮箱服务器会检查 IMAP 客户端有没有发送 ID 命令来表明自己的身份,如果没有,则拒绝访问。

关于 ID 命令,ID 命令的设计初衷是为了让服务器可以统计不同客户端的使用情况,但并非强制的。

协议中明确表示,服务器不得以 ID 命令内的信息而对客户端拒绝提供服务。

详细内容可参见 RFC2971 的定义:https://datatracker.ietf.org/doc/html/rfc2971。

解决方案

原因知道之后,解决方案其实很简单,登陆之后再发送一个ID命令就好了。代码示例如下:

1
2
3
4
5
6
7
import imaplib
imaplib.Commands["ID"] = "NONAUTH"

client = imaplib.IMAP4_SSL(host="imap.163.com", port=993)
client.login("username", "password")
client._simple_command("ID",'("name" "test" "version" "0.0.1")')
client.select()

引申一下

解决方案很简单,但总感觉不够完美,少了点啥。

在实际应用中,用户会访问不同的邮箱的,那么就需要考虑以下两个问题:

  1. 是否有可能某些邮箱服务器不支持 ID 命令?

  2. 如果邮箱支持 ID 命令,但并不强制使用,会对其他正常功能有影响吗?

第一个问题,这个我觉得应该是有的,毕竟 ID 并非标准协议中的一部分,有可能某些邮箱服务器开发的比较早,并且无人维护了。所以针对这种情况是需要测试 IMAP 对于不认识的命令是如何处理的。

第二个问题,应该是不会有影响的,不过简单测试一下以求个安心。

于是,我先试了一下 QQ 邮箱。

先说第二个问题的测试结果,QQ 邮箱是没有啥影响的,这个也不重要,跳过就好。

但第一个问题有点出乎意料,测试方式很简单,分别向网易邮箱和 QQ 邮箱发送不支持的命令,看一下是否会影响接下来的使用即可。代码如下:

1
2
3
4
5
6
7
8
9
10
11
import imaplib

imaplib.Debug = 100
imaplib.Commands["XXX"] = "NONAUTH"

host = "imap.163.com"
# host = "imap.qq.com"

client = imaplib.IMAP4_SSL(host=host, port=993)
client._simple_command("XXX", '("name" "aaa")')
client.select()

其中网易邮箱在接收到未知命令时,返回结果是:

1
# b'PMGB1 BAD command not support' 

日志如图

这个符合预期,直接捕获一下异常即可。

但 QQ 邮箱在接收到未知命令时,返回的结果是:

1
# b'* BAD Command!'

日志如图

且代码在这里直接卡住了,等待几分钟后:

1
ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接

QQ 邮箱服务器主动断开了跟我的链接。

WTF ! ! !

网易邮箱的抛出异常,我可以直接捕获跳过即可,对程序主流程无影响。但 QQ 邮箱卡在这里是啥意思???

为了搞清楚这个事情,又开始了对代码卡住的分析。

代码分析过程不赘述,各种断点调试一通,最终发现,代码卡在了等待邮箱服务器返回数据的部分。

但从调试日志中可以看到,QQ 邮箱服务器已经返回了数据:

1
# b'* BAD Command!'

为啥还要等呢?难道数据有问题?

然后又是一通搜索,在 IMAP RFC3501 定义得到了答案,其中 2.2.1 节定义了邮箱服务端的返回数据格式规范:

返回数据的第一部分应该是 tag、*、+ 其中之一。

  • tag 是客户端在发送命令时携带的一个唯一标识,主要用于客户端区分收到的响应是那个命令的响应
  • * 代表此命令的相应内容尚未完结,应继续等待后续响应
  • + 代表服务端继续等待客户端发送尚未发送完的命令

因此,这里 QQ 邮箱返回的 b’* BAD Command!’ 是不对的,应该是 b’tag BAD Command!’。

至此,感觉已经没有什么可做的事情了。

不过,我还是给 QQ 邮箱发了封邮件,简单描述了一下问题,也不知道他们改不改,等结果吧。。。

然后我的应用代码改成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
import imaplib

client = imaplib.IMAP4_SSL(host="imap.163.com", port=993)
client.login("username", "password")
typ, dat = client.select()
if typ != "OK":
try:
client._simple_command("ID", '("name" "test" "version" "0.0.1")')
except Exception as e:
print(e)
typ, dat = client.select()
if typ != "OK":
raise Exception("邮箱登录失败: {} {}".format(typ, dat))

后记

待回复

不相关知识点整理

  • POP3 可以认为是只读的协议,客户端内的操作不会影响到服务端,只用来下载邮件

  • IMAP 是双向的协议,客户端的操作会反馈到服务端上,服务端也同步进行操作,不仅可以下载邮件,还可以删除,移动邮件,但不能发送邮件

  • SMTP 是用来发送邮件的

参考文档

IMAP4 ID 扩展

   https://datatracker.ietf.org/doc/html/rfc2971

IMAP4

  https://www.rfc-editor.org/rfc/inline-errata/rfc3501.html

第三方邮件客户端收取163邮件问题

  https://www.cnblogs.com/chjbbs/p/9858672.html

IMAP 与 POP3 有什么不同?

  http://help.163.com/10/0203/17/5UK7GVU100753VB9.html?servCode=6020251

什么是POP3、SMTP及IMAP?

  https://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac21b87735d7227c217

imap连接提示Unsafe Login,被阻止的收信行为

  https://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac211b1978002df8b23

Python解决的办法

  https://blog.csdn.net/jony_online/article/details/108638571