返回介绍

16.4 用 IMAP 获取和删除电子邮件

发布于 2024-01-22 21:44:06 字数 13691 浏览 0 评论 0 收藏 0

在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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文