- 内容提要
- 作者简介
- 技术评审者简介
- 致谢
- 译者序 会编程的人不一样
- 前言
- 本书的读者对象
- 编码规范
- 什么是编程
- 本书简介
- 下载和安装 Python
- 启动 IDLE
- 如何寻求帮助
- 聪明地提出编程问题
- 小结
- 第一部分 Python 编程基础
- 第1章 Python 基础
- 第2章 控制流
- 第3章 函数
- 第4章 列表
- 第5章 字典和结构化数据
- 第6章 字符串操作
- 第二部分 自动化任务
- 第7章 模式匹配与正则表达式
- 第8章 读写文件
- 第9章 组织文件
- 第10章 调试
- 第11章 从 Web 抓取信息
- 第12章 处理 Excel 电子表格
- 第13章 处理 PDF 和 Word 文档
- 第14章 处理 CSV 文件和 JSON 数据
- 第15章 保持时间、计划任务和启动程序
- 第16章 发送电子邮件和短信
- 第17章 操作图像
- 第18章 用 GUI 自动化控制键盘和鼠标
- 附录A 安装第三方模块
- 附录B 运行程序
- 附录C 习题答案
16.4 用 IMAP 获取和删除电子邮件
在Python中,查找和获取电子邮件是一个多步骤的过程,需要第三方模块imapclient和pyzmail。作为概述,这里有一个完整的例子,包括登录到IMAP服务器,搜索电子邮件,获取它们,然后从中提取电子邮件的文本。
>>> import imapclient >>> imapObj = imapclient.IMAPClient('imap.gmail.com', ssl=True) >>> imapObj.login('my_email_address@gmail.com', 'MY_SECRET_PASSWORD') 'my_email_address@gmail.com Jane Doe authenticated (Success)' >>> imapObj.select_folder('INBOX', readonly=True) >>> UIDs = imapObj.search(['SINCE 05-Jul-2014']) >>> UIDs [40032, 40033, 40034, 40035, 40036, 40037, 40038, 40039, 40040, 40041] >>> rawMessages = imapObj.fetch([40041], ['BODY[]', 'FLAGS']) >>> import pyzmail >>> message = pyzmail.PyzMessage.factory(rawMessages[40041]['BODY[]']) >>> message.get_subject() 'Hello!' >>> message.get_addresses('from') [('Edward Snowden', 'esnowden@nsa.gov')] >>> message.get_addresses('to') [(Jane Doe', 'jdoe@example.com')] >>> message.get_addresses('cc') [] >>> message.get_addresses('bcc') [] >>> message.text_part != None True >>> message.text_part.get_payload().decode(message.text_part.charset) 'Follow the money.\r\n\r\n-Ed\r\n' >>> message.html_part != None True >>> message.html_part.get_payload().decode(message.html_part.charset) '< div dir="ltr">< div>So long, and thanks for all the fish!< br>< br>< /div>- Al< br>< /div>\r\n' >>> imapObj.logout()
你不必记住这些步骤。在详细介绍每一步之后,你可以回来看这个概述,加强记忆。
16.4.1 连接到IMAP服务器
就像你需要一个SMTP对象连接到SMTP服务器并发送电子邮件一样,你需要一个IMAPClient对象,连接到IMAP服务器并接收电子邮件。首先,你需要电子邮件服务提供商的IMAP服务器域名。这和SMTP服务器的域名不同。表16-2列出了几个流行的电子邮件服务提供商的IMAP服务器。
表16-2 电子邮件提供商及其IMAP服务器
提供商
IMAP服务器域名
Gmail
imap.gmail.com
Outlook.com/Hotmail.com
imap-mail.outlook.com
Yahoo Mail
imap.mail.yahoo.com
AT&T
imap.mail.att.net
Comcast
imap.comcast.net
Verizon
incoming.verizon.net
得到IMAP服务器域名后,调用imapclient.IMAPClient()函数,创建一个IMAPClient对象。大多数电子邮件提供商要求SSL加密,传入SSL= TRUE关键字参数。在交互式环境中输入以下代码(使用你的提供商的域名):
>>> import imapclient >>> imapObj = imapclient.IMAPClient('imap.gmail.com', ssl=True)
在接下来的小节里所有交互式环境的例子中,imapObj变量将包含imapclient.IMAPClient()函数返回的IMAPClient对象。在这里,客户端是连接到服务器的对象。
16.4.2 登录到IMAP服务器
取得IMAPClient对象后,调用它的login()方法,传入用户名(这通常是你的电子邮件地址)和密码字符串。
>>> imapObj.login('my_email_address@gmail.com', 'MY_SECRET_PASSWORD') 'my_email_address@gmail.com Jane Doe authenticated (Success)'
要记住,永远不要直接在代码中写入密码!应该让程序从input()接受输入的密码。
如果IMAP服务器拒绝用户名/密码的组合,Python会抛出imaplib.error异常。对于Gmail账户,你可能需要使用应用程序专用的密码。详细信息请参阅16.2.5节中的“Gmail应用程序专用密码”。
16.4.3 搜索电子邮件
登录后,实际获取你感兴趣的电子邮件分为两步。首先,必须选择要搜索的文件夹。然后,必须调用IMAPClient对象的search()方法,传入IMAP搜索关键词字符串。
16.4.4 选择文件夹
几乎每个账户默认都有一个INBOX文件夹,但也可以调用IMAPClient对象的list_folders()方法,获取文件夹列表。这将返回一个元组的列表。每个元组包含一个文件夹的信息。输入以下代码,继续交互式环境的例子:
>>> import pprint >>> pprint.pprint(imapObj.list_folders()) [(('\\HasNoChildren',), '/', 'Drafts'), (('\\HasNoChildren',), '/', 'Filler'), (('\\HasNoChildren',), '/', 'INBOX'), (('\\HasNoChildren',), '/', 'Sent'), --snip-- (('\\HasNoChildren', '\\Flagged'), '/', '[Gmail]/Starred'), (('\\HasNoChildren', '\\Trash'), '/', '[Gmail]/Trash')]
如果你有一个Gmail账户,这就是输出可能的样子(Gmail将文件夹称为label,但它们的工作方式与文件夹相同)。每个元组的三个值,例如 (('\HasNoChildren',), '/', 'INBOX'),解释如下:
· 该文件夹的标志的元组(这些标志代表到底是什么超出了本书的讨论范围,你可以放心地忽略该字段)。
· 名称字符串中用于分隔父文件夹和子文件夹的分隔符。
· 该文件夹的全名。
要选择一个文件夹进行搜索,就调用IMAPClient对象的select_folder()方法,传入该文件夹的名称字符串。
>>> imapObj.select_folder('INBOX', readonly=True)
可以忽略select_folder()的返回值。如果所选文件夹不存在,Python会抛出imaplib.error异常。
readonly=True关键字参数可以防止你在随后的方法调用中,不小心更改或删除该文件夹中的任何电子邮件。除非你想删除的电子邮件,否则将readonly设置为True总是个好主意。
16.4.5 执行搜索
文件夹选中后,就可以用IMAPClient对象的search()方法搜索电子邮件。search()的参数是一个字符串列表,每一个格式化为IMAP搜索键。表16-3介绍了各种搜索键。
表16-3 IMAP搜索键
搜索键
含义
'ALL'
返回该文件夹中的所有邮件。如果你请求一个大文件夹中的所有消息,可能会遇到imaplib的大小限制。参见16.4.6小节“大小限制”
'BEFORE date', 'ON date', 'SINCE date'
这三个搜索键分别返回给定date之前、当天和之后IMAP服务器接收的消息。日期的格式必须像05-Jul-2015。此外,虽然'SINCE 05-Jul-2015'将匹配7月5日当天和之后的消息,但'BEFORE 05-Jul-2015'仅匹配7月5日之前的消息,不包括7月5日当天
'SUBJECT string', 'BODY string', 'TEXT string'
分别返回string出现在主题、正文、主题或正文中的消息。如果string中有空格,就使用双引号:'TEXT "search with spaces"'
'FROM string', 'TO string', 'CC string', 'BCC string'
返回所有消息,其中string分别出现在“from”邮件地址,“to”邮件地址,“cc”(抄送)地址,或“bcc”(密件抄送)地址中。 如果string中有多个电子邮件地址,就用空格将它们分开,并使用双引号: 'CC "firstcc@example.com secondcc@example.com"'
'SEEN', 'UNSEEN'
分别返回包含和不包含\ Seen标记的所有信息。如果电子邮件已经被fetch()方法调用访问(稍后描述),或者你曾在电子邮件程序或网络浏览器中点击过它,就会有\ Seen标记。比较常用的说法是电子邮件“已读”,而不是“已看”,但它们的意思一样。
'ANSWERED', 'UNANSWERED'
分别返回包含和不包含\ Answered标记的所有消息。如果消息已答复,就会有\ Answered标记
'DELETED', 'UNDELETED'
分别返回包含和不包含\Deleted标记的所有信息。用delete_messages()方法删除的邮件就会有\Deleted标记,直到调用expunge()方法才会永久删除(请参阅16.4.10节“删除电子邮件”)。请注意,一些电子邮件提供商,例如Gmail,会自动清除邮件
'DRAFT', 'UNDRAFT'
分别返回包含和不包含\ Draft标记的所有消息。草稿邮件通常保存在单独的草稿文件夹中,而不是在收件箱中
'FLAGGED', 'UNFLAGGED'
分别返回包含和不包含\Flagged标记的所有消息。这个标记通常用来标记电子邮件为“重要”或“紧急”
'LARGER N', 'SMALLER N'
分别返回大于或小于N个字节的所有消息
'NOT search-key'
返回搜索键不会返回的那些消息
'OR search-key1 search-key2'
返回符合第一个或第二个搜索键的消息
请注意,在处理标志和搜索键方面,某些IMAP服务器的实现可能稍有不同。可能需要在交互式环境中试验一下,看看它们实际的行为如何。
在传入search()方法的列表参数中,可以有多个IMAP搜索键字符串。返回的消息将匹配所有的搜索键。如果想匹配任何一个搜索键,使用OR搜索键。对于NOT和OR搜索键,它们后边分别跟着一个和两个完整的搜索键。
下面是search()方法调用的一些例子,以及它们的含义:
imapObj.search(['ALL']) 返回当前选定的文件夹中的每一个消息。
imapObj.search(['ON 05-Jul-2015'])返回在2015年7月5日发送的每个消息。
imapObj.search(['SINCE 01-Jan-2015', 'BEFORE 01-Feb-2015', 'UNSEEN'])返回2015年1月发送的所有未读消息(注意,这意味着从1月1日直到2月1日,但不包括2月1日)。
imapObj.search(['SINCE 01-Jan-2015', 'FROM alice@example.com'])返回自2015年开始以来,发自alice@example.com的消息。
imapObj.search(['SINCE 01-Jan-2015', 'NOT FROM alice@example.com'])返回自2015年开始以来,除alice@example.com外,其他所有人发来的消息。
imapObj.search(['OR FROM alice@example.com FROM bob@example.com'])返回发自alice@example.com或bob@example.com的所有信息。
imapObj.search(['FROM alice@example.com', 'FROM bob@example.com'])恶作剧例子!该搜索不会返回任何消息,因为消息必须匹配所有搜索关键词。因为只能有一个“from”地址,所以一条消息不可能既来自alice@example.com,又来自bob@example.com。
search()方法不返回电子邮件本身,而是返回邮件的唯一整数ID(UID)。然后,可以将这些UID传入fetch()方法,获得邮件内容。
输入以下代码,继续交互式环境的例子:
>>> UIDs = imapObj.search(['SINCE 05-Jul-2015']) >>> UIDs [40032, 40033, 40034, 40035, 40036, 40037, 40038, 40039, 40040, 40041]
这里,search()返回的消息ID列表(针对7月5日以来接收的消息)保存在UIDs中。计算机上返回的UIDs列表与这里显示的不同,它们对于特定的电子邮件账户是唯一的。如果你稍后将UID传递给其他函数调用,请用你收到的UID值,而不是本书例子中打印的。
16.4.6 大小限制
如果你的搜索匹配大量的电子邮件,Python可能抛出异常imaplib.error: got more than 10000 bytes。如果发生这种情况,必须断开并重连IMAP服务器,然后再试。
这个限制是防止Python程序消耗太多内存。遗憾的是,默认大小限制往往太小。可以执行下面的代码,将限制从10000字节改为10000000字节:
>>> import imaplib >>> imaplib._MAXLINE = 10000000
这应该能避免该错误消息再次出现。也许要在你写的每一个IMAP程序中加上这两行。
16.4.7 取邮件并标记为已读
得到UID的列表后,可以调用IMAPClient对象的fetch()方法,获得实际的电子邮件内容。
UID列表是fetch()的第一个参数。第二个参数应该是['BODY[]'],它告诉fetch()下载UID列表中指定电子邮件的所有正文内容。
使用IMAPClient的gmail_search()方法
如果登录到imap.gmail.com服务器来访问Gmail账户,IMAPClient对象提供了一个额外的搜索函数,模拟Gmail网页顶部的搜索栏,如图16-1中高亮的部分所示。
图16-1 在Gmail网页顶部的搜索栏
除了用IMAP搜索键搜索,可以使用Gmail更先进的搜索引擎。Gmail在匹配密切相关的单词方面做得很好(例如,搜索driving也会匹配drive和drove),并按照匹配的程度对搜索结果排序。也可以使用Gmail的高级搜索操作符(更多信息请参见http://nostarch.com/automatestuff/)。如果登录到Gmail账户,向gmail_search()方法传入搜索条件,而不是search()方法,就像下面交互式环境的例子:
>>> UIDs = imapObj.gmail_search('meaning of life') >> UIDs [42]
啊,是的,那封电子邮件包含了生命的意义!我一直在期待。
让我们继续交互式环境的例子。
>>> rawMessages = imapObj.fetch(UIDs, ['BODY[]']) >>> import pprint >>> pprint.pprint(rawMessages) {40040: {'BODY[]': 'Delivered-To: my_email_address@gmail.com\r\n' 'Received: by 10.76.71.167 with SMTP id ' --snip-- '\r\n' '------=_Part_6000970_707736290.1404819487066--\r\n', 'SEQ': 5430}}
导入 pprint,将 fetch()的返回值(保存在变量 rawMessages 中)传入pprint.pprint(),“漂亮打印”它。你会看到,这个返回值是消息的嵌套字典,其中以UID作为键。每条消息都保存为一个字典,包含两个键:'BODY[]'和'SEQ'。'BODY[]'键映射到电子邮件的实际正文。'SEQ'键是序列号,它与UID的作用类似。你可以放心地忽略它。
正如你所看到的,在'BODY[]'键中的消息内容是相当难理解的。这种格式称为RFC822,是专为IMAP服务器读取而设计的。但你并不需要理解RFC 822格式,本章稍后的pyzmail模块将替你来理解它。
如果你选择一个文件夹进行搜索,就用readonly=True关键字参数来调用select_ folder()。这样做可以防止意外删除电子邮件,但这也意味着你用fetch()方法获取邮件时,它们不会标记为已读。如果确实希望在获取邮件时将它们标记已读,就需要将readonly=False传入select_folder()。如果所选文件夹已处于只读模式,可以用另一个 select_folder()调用重新选择当前文件夹,这次用readonly=False关键字参数:
>>> imapObj.select_folder('INBOX', readonly=False)
16.4.8 从原始消息中获取电子邮件地址
对于只想读邮件的人来说,fetch()方法返回的原始消息仍然不太有用。pyzmail模块解析这些原始消息,将它们作为PyzMessage对象返回,使邮件的主题、正文、“收件人”字段、“发件人”字段和其他部分能用Python代码轻松访问。
用下面的代码继续交互式环境的例子(使用你自己的邮件账户的UID,而不是这里显示的):
>>> import pyzmail >>> message = pyzmail.PyzMessage.factory(rawMessages[40041]['BODY[]'])
首先,导入pyzmail。然后,为了创建一个电子邮件的PyzMessage对象,调用pyzmail.PeekMessage.factory()函数,并传入原始邮件的'BODY[]'部分。结果保存在message中。现在,message中包含一个PyzMessage对象,它有几个方法,可以很容易地获得的电子邮件主题行,以及所有发件人和收件人的地址。get_subject()方法将主题返回为一个简单字符串。get_addresses()方法针对传入的字段,返回一个地址列表。例如,该方法调用可能像这样:
>>> message.get_subject() 'Hello!' >>> message.get_addresses('from') [('Edward Snowden', 'esnowden@nsa.gov')] >>> message.get_addresses('to') [(Jane Doe', 'my_email_address@gmail.com')] >>> message.get_addresses('cc') [] >>> message.get_addresses('bcc') []
请注意,get_addresses()的参数是'from'、'to'、'cc'或 'bcc'。get_addresses()的返回值是一个元组列表。每个元组包含两个字符串:第一个是与该电子邮件地址关联的名称,第二个是电子邮件地址本身。如果请求的字段中没有地址,get_addresses()返回一个空列表。在这里,'cc'抄送和'bcc'密件抄送字段都没有包含地址,所以返回空列表。
16.4.9 从原始消息中获取正文
电子邮件可以是纯文本、HTML 或两者的混合。纯文本电子邮件只包含文本,而HTML电子邮件可以有颜色、字体、图像和其他功能,使得电子邮件看起来像一个小网页。如果电子邮件仅仅是纯文本,它的PyzMessage对象会将html_part属性设为None。同样,如果电子邮件只是HTML,它的PyzMessage对象会将text_part属性设为None。
否则,text_part或html_part将有一个get_payload()方法,将电子邮件的正文返回为bytes数据类型(bytes数据类型超出了本书的范围)。但是,这仍然不是我们可以使用的字符串。啊!最后一步对get_payload()返回的bytes值调用decode()方法。decode()方法接受一个参数:这条消息的字符编码,保存在text_part.charset或html_part.charset属性中。最后,这返回了邮件正文的字符串。
输入以下代码,继续交互式环境的例子:
❶ >>> message.text_part != None True >>> message.text_part.get_payload().decode(message.text_part.charset) ❷ 'So long, and thanks for all the fish!\r\n\r\n-Al\r\n' ❸ >>> message.html_part != None True ❹ >>> message.html_part.get_payload().decode(message.html_part.charset) '< div dir="ltr">< div>So long, and thanks for all the fish!< br>< br>< /div>-Al < br>< /div>\r\n'
我们正在处理的电子邮件包含纯文本和HTML内容,因此保存在message中的PyzMessage对象的text_part和html_part属性不等于None❶❸。对消息的text_part调用get_payload(),然后在bytes值上调用decode(),返回电子邮件的文本版本的字符串❷。对消息的html_part调用get_payload()和decode(),返回电子邮件的HTML版本的字符串❹。
16.4.10 删除电子邮件
要删除电子邮件,就向IMAPClient对象的delete_messages()方法传入一个消息UID的列表。这为电子邮件加上\Deleted标志。调用expunge()方法,将永久删除当前选中的文件夹中带\Deleted标志的所有电子邮件。请看下面的交互式环境的例子:
❶ >>> imapObj.select_folder('INBOX', readonly=False) ❷ >>> UIDs = imapObj.search(['ON 09-Jul-2015']) >>> UIDs [40066] >>> imapObj.delete_messages(UIDs) ❸ {40066: ('\\Seen', '\\Deleted')} >>> imapObj.expunge() ('Success', [(5452, 'EXISTS')])
这里,我们调用了IMAPClient对象的select_folder()方法,传入'INBOX'作为第一个参数,选择了收件箱。我们也传入了关键字参数readonly=False,这样我们就可以删除电子邮件❶。我们搜索收件箱中的特定日期收到的消息,将返回的消息ID保存在UIDs中❷。调用delete_message()并传入UIDs,返回一个字典,其中每个键值对是一个消息 ID 和消息标志的元组,它现在应该包含\Deleted标志❸。然后调用expunge(),永久删除带\Deleted标志的邮件。如果清除邮件没有问题,就返回一条成功信息。请注意,一些电子邮件提供商,如Gmail,会自动清除用delete_messages()删除的电子邮件,而不是等待来自IMAP客户端的expunge命令。
16.4.11 从IMAP服务器断开
如果程序已经完成了获取和删除电子邮件,就调用IMAPClient的logout()方法,从IMAP服务器断开连接。
>>> imapObj.logout()
如果程序运行了几分钟或更长时间,IMAP服务器可能会超时,或自动断开。在这种情况下,接下来程序对IMAPClient对象的方法调用会抛出异常,像下面这样:
imaplib.abort: socket error: [WinError 10054] An existing connection was forcibly closed by the remote host
在这种情况下,程序必须调用imapclient.IMAPClient(),再次连接。
哟!齐活了。要跳过很多圈圈,但你现在有办法让Python程序登录到一个电子邮件账户,并获取电子邮件。需要回忆所有步骤时,你可以随时参考16.4节“用IMAP获取和删除电子邮件”。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论