mtplib 簡單郵件傳輸協議 simble mail transfer protocol library
import smtplib 引入的包
import email 多用戶郵件擴充協議
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
文基于:Spring Boot 2.1.3,理論支持Spring Boot 2.x所有版本。
最近有童鞋問到筆者如何用Spring Boot發送郵件,故而整理下Spring Boot發送郵件的各種姿勢。
說到郵件放松,相信大家對Spring Framework提供的接口 JavaMailSender 都不陌生。那么Spring Boot是否有開箱即用的郵件發送呢?
答案是肯定的。Spring Boot為發送郵件提供了starter:spring-boot-starter-mail 。
本文詳細探討如何用Spring Boot發送郵件。
一、郵箱配置
以126郵箱為例:
1 開啟SMTP服務
2 設置/重置客戶端授權密碼
二、編碼
2.1 準備工作
1 加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
2 寫配置
spring: mail: host: smtp.126.com username: eacdy0000@126.com password: 上面設置的授權碼
2.2 發送簡單郵件
public String simple() { SimpleMailMessage message=new SimpleMailMessage(); // 發件人郵箱 message.setFrom(this.mailProperties.getUsername()); // 收信人郵箱 message.setTo("511932633@qq.com"); // 郵件主題 message.setSubject("簡單郵件測試"); // 郵件內容 message.setText("簡單郵件測試"); this.javaMailSender.send(message); return "success"; }
結果類似下圖:
2.3 發送HTML郵件
簡單郵件是沒有樣式的,很多時候,我們希望發送的郵件內容帶有樣式,此時可發送HTML郵件。
public String html() throws MessagingException { MimeMessage message=javaMailSender.createMimeMessage(); MimeMessageHelper messageHelper=new MimeMessageHelper(message); messageHelper.setFrom(this.mailProperties.getUsername()); messageHelper.setTo("511932633@qq.com"); messageHelper.setSubject("HTML內容郵件測試"); // 第二個參數表示是否html,設為true messageHelper.setText("<h1>HTML內容..</h1>", true); this.javaMailSender.send(message); return "success"; }
結果類似下圖:
2.4 發送帶附件的郵件
很多場景下,需要為郵件插入附件,此時該怎么辦呢?繼續上代碼——
@GetMapping("/attach") public String attach() throws MessagingException { MimeMessage message=this.javaMailSender.createMimeMessage(); // 第二個參數表示是否開啟multipart模式 MimeMessageHelper messageHelper=new MimeMessageHelper(message, true); messageHelper.setFrom(this.mailProperties.getUsername()); messageHelper.setTo("511932633@qq.com"); messageHelper.setSubject("帶附件的郵件測試"); // 第二個參數表示是否html,設為true messageHelper.setText("<h1>HTML內容..</h1>", true); messageHelper.addAttachment("附件名稱", new ClassPathResource("wx.jpg")); this.javaMailSender.send(message); return "success"; }
結果類似下圖:
2.5 發送帶內聯附件的郵件
附件 + HTML基本能滿足日常工作中多數需求。但如果能將附件內聯在郵件內容中,那么體驗就更好啦!如何實現附件的內聯呢?
@GetMapping("/inline-attach") public String inlineAttach() throws MessagingException { MimeMessage message=this.javaMailSender.createMimeMessage(); // 第二個參數表示是否開啟multipart模式 MimeMessageHelper messageHelper=new MimeMessageHelper(message, true); messageHelper.setFrom(this.mailProperties.getUsername()); messageHelper.setTo("511932633@qq.com"); messageHelper.setSubject("內聯附件的郵件測試"); // 第二個參數表示是否html,設為true messageHelper.setText("<h1>HTML內容..<img src=\"cid:attach\"/></h1>", true); messageHelper.addInline("attach", new ClassPathResource("wx.jpg")); this.javaMailSender.send(message); return "success"; }
由代碼可知,只需在想要內聯的地方使用 cid:xx 引用內聯附件,然后用 addInline(xx, file)指定附件即可。兩處的 xx 必須一致。
結果類似下圖:
2.6 發送基于Freemarker模板的郵件
上面的例子中,郵件內容是直接以字符串體現的,這通常不適合生產,因為實際項目中郵件往往帶有變量。此時,可考慮使用Freemarker模板(或者其他模板,Spring Boot 2.x默認支持Freemarker、Groovy、Thymeleaf、Mustache四種模板引擎,也可根據需求使用其他模板引擎)。
?
創建Freemarker模板文件mail.ftl,并將其存放在resources/templates/ 目錄中
<h1>親愛的${username}, 歡迎關注${event}</h1>
?
編碼:
@GetMapping("/freemarker") public String freemarker() throws MessagingException, IOException, TemplateException { MimeMessage message=this.javaMailSender.createMimeMessage(); // 第二個參數表示是否開啟multipart模式 MimeMessageHelper messageHelper=new MimeMessageHelper(message, true); messageHelper.setFrom(this.mailProperties.getUsername()); messageHelper.setTo("511932633@qq.com"); messageHelper.setSubject("基于freemarker模板的郵件測試"); Map<String, Object> model=new HashMap<>(); model.put("username", "itmuch"); model.put("event", "IT牧場大事件"); String content=FreeMarkerTemplateUtils.processTemplateIntoString( this.freemarkerConfiguration.getTemplate("mail.ftl"), model); // 第二個參數表示是否html,設為true messageHelper.setText(content, true); this.javaMailSender.send(message); return "success"; }
此時,結果類似下圖:
三、配套代碼
?GitHub[1]
?Gitee[2]
干貨分享
最近將個人學習筆記整理成冊,使用PDF分享。關注我,回復如下代碼,即可獲得百度盤地址,無套路領取!
?001:《Java并發與高并發解決方案》學習筆記;
?002:《深入JVM內核——原理、診斷與優化》學習筆記;
?003:《Java面試寶典》
?004:《Docker開源書》
?005:《Kubernetes開源書》
?006:《DDD速成(領域驅動設計速成)》
References
[1] GitHub: https://github.com/eacdy/spring-boot-study/tree/master/spring-boot-mail
[2] Gitee: https://gitee.com/itmuch/spring-boot-study/tree/master/spring-boot-mail
查和答復電子郵件會占用大量的時間。當然,你不能只寫一個程序來處理所有電子郵件,因為每個消息都需要有自己的回應。但是,一旦知道怎么編寫收發電子郵件的程序,就可以自動化大量與電子郵件相關的任務。
例如,也許你有一個電子表格,包含許多客戶記錄,希望根據他們的年齡和位置信息,向每個客戶發送不同格式的郵件。商業軟件可能無法做這一點。好在,可以編寫自己的程序來發送這些電子郵件,節省了大量復制和粘貼電子郵件的時間。
也可以編程發送電子郵件和短信,即使你遠離計算機時,也能通知你。如果要自動化的任務需要執行幾個小時,你不希望每過幾分鐘就回到計算機旁邊,檢查程序的狀態。相反,程序可以在完成時向手機發短信,讓你在離開計算機時,能專注于更重要的事情。
正如HTTP是計算機用來通過因特網發送網頁的協議,簡單郵件傳輸協議(SMTP)是用于發送電子郵件的協議。SMTP 規定電子郵件應該如何格式化、加密、在郵件服務器之間傳遞,以及在你點擊發送后,計算機要處理的所有其他細節。但是,你并不需要知道這些技術細節,因為Python的smtplib模塊將它們簡化成幾個函數。
SMTP只負責向別人發送電子郵件。另一個協議,名為IMAP,負責取回發送給你的電子郵件,在16.3節“IMAP”中介紹。
你可能對發送電子郵件很熟悉,通過Outlook、Thunderbird或某個網站,如Gmail或雅虎郵箱。遺憾的是,Python沒有像這些服務一樣提供一個漂亮的圖形用戶界面。作為替代,你調用函數來執行SMTP的每個重要步驟,就像下面的交互式環境的例子。
{注意}
不要在IDLE中輸入這個例子,因為smtp.example.com、bob@example.com、MY_ SECRET_PASSWORD和alice@example.com只是占位符。這段代碼僅僅勾勒出Python發送電子郵件的過程。
>>> smtpObj=smtplib.SMTP('smtp.example.com', 587)
>>> smtpObj.ehlo()
(250, b'mx.example.com at your service, [216.172.148.131]\nSIZE 35882577\
n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nCHUNKING')
>>> smtpObj.starttls()
(220, b'2.0.0 Ready to start TLS')
>>> smtpObj.login('bob@example.com', 'MY_SECRET_PASSWORD')
(235, b'2.7.0 Accepted')
>>> smtpObj.sendmail('bob@example.com', 'alice@example.com', 'Subject: So
long.\nDear Alice, so long and thanks for all the fish. Sincerely, Bob')
{}
>>> smtpObj.quit()
(221, b'2.0.0 closing connection ko10sm23097611pbd.52 - gsmtp')
在下面的小節中,我們將探討每一步,用你的信息替換占位符,連接并登錄到SMTP服務器,發送電子郵件,并從服務器斷開連接。
如果你曾設置了Thunderbird、Outlook或其他程序,連接到你的電子郵件賬戶,你可能熟悉配置SMTP服務器和端口。這些設置因電子郵件提供商而不同,但在網上搜索“< 你的提供商> SMTP設置”,應該能找到相應的服務器和端口。
SMTP服務器的域名通常是電子郵件提供商的域名,前面加上SMTP。例如,Gmail的 SMTP 服務器是smtp.gmail.com。表 16-1 列出了一些常見的電子郵件提供商及其SMTP服務器(端口是一個整數值,幾乎總是587,該端口由命令加密標準TLS使用)。
表16-1 電子郵件提供商及其SMTP服務器
得到電子郵件提供商的域名和端口信息后,調用smtplib.SMTP()創建一個SMTP對象,傳入域名作為一個字符串參數,傳入端口作為整數參數。SMTP對象表示與SMTP郵件服務器的連接,它有一些發送電子郵件的方法。例如,下面的調用創建了一個SMTP對象,連接到Gmail:
>>> smtpObj=smtplib.SMTP('smtp.gmail.com', 587)
>>> type(smtpObj)
< class 'smtplib.SMTP'>
輸入type(smtpObj)表明,smtpObj中保存了一個SMTP對象。你需要這個SMTP對象,以便調用它的方法,登錄并發送電子郵件。如果smtplib.SMTP()調用不成功,你的SMTP服務器可能不支持TLS端口587。在這種情況下,你需要利用smtplib.SMTP_SSL()和465端口,來創建SMTP對象。
>>> smtpObj=smtplib.SMTP_SSL('smtp.gmail.com', 465)
{注意}
如果沒有連接到因特網,Python將拋出socket.gaierror: [Errno 11004] getaddrinfo failed或類似的異常。
對于你的程序,TLS和SSL之間的區別并不重要。只需要知道你的SMTP服務器使用哪種加密標準,這樣就知道如何連接它。在接下來的所有交互式環境示例中,smtpObj變量將包含smtplib.SMTP()或smtplib.SMTP_SSL()函數返回的SMTP對象。
得到SMTP對象后,調用它的名字古怪的EHLO()方法,向SMTP電子郵件服務器“打招呼”。這種問候是SMTP中的第一步,對于建立到服務器的連接是很重要的。你不需要知道這些協議的細節。只要確保得到SMTP對象后,第一件事就是調用ehlo()方法,否則以后的方法調用會導致錯誤。下面是一個ehlo()調用和返回值的例子:
>>> smtpObj.ehlo()
(250, b'mx.google.com at your service, [216.172.148.131]\nSIZE 35882577\
n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nCHUNKING')
如果在返回的元組中,第一項是整數250(SMTP中“成功”的代碼),則問候成功了。
如果要連接到SMTP服務器的587端口(即使用TLS加密),接下來需要調用starttls()方法。這是為連接實現加密必須的步驟。如果要連接到465端口(使用SSL),加密已經設置好了,你應該跳過這一步。
下面是starttls()方法調用的例子:
>>> smtpObj.starttls()
(220, b'2.0.0 Ready to start TLS')
starttls()讓SMTP連接處于TLS模式。返回值220告訴你,該服務器已準備就緒。
到SMTP服務器的加密連接建立后,可以調用login()方法,用你的用戶名(通常是你的電子郵件地址)和電子郵件密碼登錄。
>>> smtpObj.login('my_email_address@gmail.com', 'MY_SECRET_PASSWORD')
(235, b'2.7.0 Accepted')
傳入電子郵件地址字符串作為第一個參數,密碼字符串作為第二個參數。返回值235表示認證成功。如果密碼不正確,Python會拋出smtplib. SMTPAuthenticationError異常。
將密碼放在源代碼中要當心。如果有人復制了你的程序,他們就能訪問你的電子郵件賬戶!調用input(),讓用戶輸入密碼是一個好主意。每次運行程序時輸入密碼可能不方便,但這種方法不會在未加密的文件中留下你的密碼,黑客或筆記本電腦竊賊不會輕易地得到它。
登錄到電子郵件提供商的SMTP服務器后,可以調用的sendmail()方法來發送電子郵件。sendmail()方法調用看起來像這樣:
>>> smtpObj.sendmail('my_email_address@gmail.com', 'recipient@example.com',
'Subject: So long.\nDear Alice, so long and thanks for all the fish. Sincerely,
Bob')
{}
sendmail()方法需要三個參數。
電子郵件正文字符串必須以’Subject: \n’開頭,作為電子郵件的主題行。’\n’換行符將主題行與電子郵件的正文分開。
sendmail()的返回值是一個字典。對于電子郵件傳送失敗的每個收件人,該字典中會有一個鍵值對。空的字典意味著對所有收件人已成功發送電子郵件。
{Gmail應用程序專用密碼!!}
Gmail有針對谷歌賬戶的附加安全功能,稱為應用程序專用密碼。如果當你的程序試圖登錄時,收到“需要應用程序專用密碼”的錯誤信息,就必須在Python腳本設置這樣一個密碼。具體如何設置谷歌賬戶的應用程序專用密碼,參見http://nostarch.com/automatestuff/。
確保在完成發送電子郵件時,調用quit()方法。這讓程序從SMTP服務器斷開。
>>> smtpObj.quit()
(221, b'2.0.0 closing connection ko10sm23097611pbd.52 - gsmtp')
返回值221表示會話結束。
要復習連接和登錄服務器、發送電子郵件和斷開的所有步驟,請參閱 16.2節“發送電子郵件”。
正如SMTP是用于發送電子郵件的協議,因特網消息訪問協議(IMAP)規定了如何與電子郵件服務提供商的服務器通信,取回發送到你的電子郵件地址的電子郵件。Python帶有一個imaplib模塊,但實際上第三方的imapclient模塊更易用。本章介紹了如何使用IMAPClient,完整的文檔在http://imapclient.readthedocs.org/。
imapclient模塊從IMAP服務器下載電子郵件,格式相當復雜。你很可能希望將它們從這種格式轉換成簡單的字符串。pyzmail模塊替你完成解析這些郵件的辛苦工作。在http://www.magiksys.net/pyzmail/可以找到PyzMail的完整文檔。
從終端窗口安裝imapclient和pyzmail。附錄A包含了如何安裝第三方模塊的步驟。
在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()
你不必記住這些步驟。在詳細介紹每一步之后,你可以回來看這個概述,加強記憶。
就像你需要一個SMTP對象連接到SMTP服務器并發送電子郵件一樣,你需要一個IMAPClient對象,連接到IMAP服務器并接收電子郵件。首先,你需要電子郵件服務提供商的IMAP服務器域名。這和SMTP服務器的域名不同。表16-2列出了幾個流行的電子郵件服務提供商的IMAP服務器。
表16-2 電子郵件提供商及其IMAP服務器
得到IMAP服務器域名后,調用imapclient.IMAPClient()函數,創建一個IMAPClient對象。大多數電子郵件提供商要求SSL加密,傳入SSL=TRUE關鍵字參數。在交互式環境中輸入以下代碼(使用你的提供商的域名):
>>> import imapclient
>>> imapObj=imapclient.IMAPClient('imap.gmail.com', ssl=True)
在接下來的小節里所有交互式環境的例子中,imapObj變量將包含imapclient.IMAPClient()函數返回的IMAPClient對象。在這里,客戶端是連接到服務器的對象。
取得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應用程序專用密碼”。
登錄后,實際獲取你感興趣的電子郵件分為兩步。首先,必須選擇要搜索的文件夾。然后,必須調用IMAPClient對象的search()方法,傳入IMAP搜索關鍵詞字符串。
幾乎每個賬戶默認都有一個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總是個好主意。
文件夾選中后,就可以用IMAPClient對象的search()方法搜索電子郵件。search()的參數是一個字符串列表,每一個格式化為IMAP搜索鍵。表16-3介紹了各種搜索鍵。
表16-3 IMAP搜索鍵
請注意,在處理標志和搜索鍵方面,某些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值,而不是本書例子中打印的。
如果你的搜索匹配大量的電子郵件,Python可能拋出異常imaplib.error: got more than 10000 bytes。如果發生這種情況,必須斷開并重連IMAP服務器,然后再試。
這個限制是防止Python程序消耗太多內存。遺憾的是,默認大小限制往往太小。可以執行下面的代碼,將限制從10000字節改為10000000字節:
>>> import imaplib
>>> imaplib._MAXLINE=10000000
這應該能避免該錯誤消息再次出現。也許要在你寫的每一個IMAP程序中加上這兩行。
得到UID的列表后,可以調用IMAPClient對象的fetch()方法,獲得實際的電子郵件內容。
UID列表是fetch()的第一個參數。第二個參數應該是[‘BODY[]’],它告訴fetch()下載UID列表中指定電子郵件的所有正文內容。
{使用IMAPClient的gmail_search()方法!!}
如果登錄到imap.gmail.com服務器來訪問Gmail賬戶,IMAPClient對象提供了一個額外的搜索函數,模擬Gmail網頁頂部的搜索欄,如圖16-1中高亮的部分所示。
除了用IMAP搜索鍵搜索,可以使用Gmail更先進的搜索引擎。Gmail在匹配密切相關的單詞方面做得很好(例如,搜索driving也會匹配drive和drove),并按照匹配的程度對搜索結果排序。也可以使用Gmail的高級搜索操作符(更多信息請參見http://nostarch.com/automatestuff/)。如果登錄到Gmail賬戶,向gmail_search()方法傳入搜索條件,而不是search()方法,就像下面交互式環境的例子:
>>> UIDs=imapObj.gmail_search('meaning of life')
>> UIDs
[42]
啊,是的,那封電子郵件包含了生命的意義!我一直在期待。
圖16-1 在Gmail網頁頂部的搜索欄
讓我們繼續交互式環境的例子。
>>> 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)
對于只想讀郵件的人來說,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’密件抄送字段都沒有包含地址,所以返回空列表。
電子郵件可以是純文本、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版本的字符串?。
要刪除電子郵件,就向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命令。
如果程序已經完成了獲取和刪除電子郵件,就調用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獲取和刪除電子郵件”。
假定你一直“自愿”為“強制自愿俱樂部”記錄會員會費。這確實是一項枯燥的工作,包括維護一個電子表格,記錄每個月誰交了會費,并用電子郵件提醒那些沒交的會員。不必你自己查看電子表格,而是向會費超期的會員復制和粘貼相同的電子郵件。你猜對了,讓我們編寫一個腳本,幫你完成任務。
在較高的層面上,下面是程序要做的事:
這意味著代碼需要做到以下幾點:
打開一個新的文件編輯器窗口,并保存為sendDuesReminders.py。
假定用來記錄會費支付的 Excel 電子表格看起來如圖 16-2 所示,放在名為duesRecords.xlsx的文件中。可以從http://nostarch.com/automatestuff/下載該文件。
圖16-2 記錄會員會費支付電子表格
該電子表格中包含每個成員的姓名和電子郵件地址。每個月有一列,記錄會員的付款狀態。在成員交納會費后,對應的單元格就記為paid。
該程序必須打開duesRecords.xlsx,通過調用get_highest_column()方法,弄清楚最近一個月的列(可以參考第12章,了解用openpyxl模塊訪問Excel電子表格文件單元格的更多信息)。在文件編輯器窗口中輸入以下代碼:
#! python3
# sendDuesReminders.py - Sends emails based on payment status in spreadsheet.
import openpyxl, smtplib, sys
# Open the spreadsheet and get the latest dues status.
? wb=openpyxl.load_workbook('duesRecords.xlsx')
? sheet=wb.get_sheet_by_name('Sheet1')
? lastCol=sheet.get_highest_column()
? latestMonth=sheet.cell(row=1, column=lastCol).value
# TODO: Check each member's payment status.
# TODO: Log in to email account.
# TODO: Send out reminder emails.
導入openpyxl、smtplib和sys模塊后,我們打開duesRecords.xlsx文件,將得到的Workbook對象保存在wb中?。然后,取得Sheet 1,將得到的Worksheet對象保存在sheet中?。既然有了Worksheet對象,就可以訪問行、列和單元格。我們將最后一列保存在lastCol中?,然后用行號1和lastCol來訪問應該記錄著最近月份的單元格。取得該單元格的值,并保存在latestMonth 中?。
一旦確定了最近一個月的列數(保存在lastCol中),就可以循環遍歷第一行(這是列標題)之后的所有行,看看哪些成員在該月會費的單元格中寫著paid。如果會員沒有支付,就可以從列1和2中分別抓取成員的姓名和電子郵件地址。這些信息將放入unpaidMembers字典,它記錄最近一個月沒有交費的所有成員。將以下代碼添加到sendDuesReminder.py中。
#! python3
# sendDuesReminders.py - Sends emails based on payment status in spreadsheet.
--snip--
# Check each member's payment status.
unpaidMembers={}
? for r in range(2, sheet.get_highest_row() + 1):
? payment=sheet.cell(row=r, column=lastCol).value
if payment !='paid':
? name=sheet.cell(row=r, column=1).value
? email=sheet.cell(row=r, column=2).value
? unpaidMembers[name]=email
這段代碼設置了一個空字典unpaidMembers,然后循環遍歷第一行之后所有的行?。對于每一行,最近月份的值保存在payment中?。如果payment不等于’paid’,則第一列的值保存在name中?,第二列的值保存在email中?,name和email添加到unpaidMembers中?。
得到所有未付費成員的名單后,就可以向他們發送電子郵件提醒了。將下面的代碼添加到程序中,但要代入你的真實電子郵件地址和提供商的信息:
#! python3
# sendDuesReminders.py - Sends emails based on payment status in spreadsheet.
--snip--
# Log in to email account.
smtpObj=smtplib.SMTP('smtp.gmail.com', 587)
smtpObj.ehlo()
smtpObj.starttls()
smtpObj.login('my_email_address@gmail.com', sys.argv[1])
調用smtplib.SMTP()并傳入提供商的域名和端口,創建一個SMTP對象。調用ehlo()和starttls(),然后調用login(),并傳入你的電子郵件地址和sys.argv[1],其中保存著你的密碼字符串。在每次運行程序時,將密碼作為命令行參數輸入,避免在源代碼中保存密碼。
程序登錄到你的電子郵件賬戶后,就應該遍歷unpaidMembers字典,向每個會員的電子郵件地址發送針對個人的電子郵件。將以下代碼添加到sendDuesReminders.py:
#! python3
# sendDuesReminders.py - Sends emails based on payment status in spreadsheet.
--snip--
# Send out reminder emails.
for name, email in unpaidMembers.items():
? body="Subject: %s dues unpaid.\nDear %s,\nRecords show that you have not
paid dues for %s. Please make this payment as soon as possible. Thank you!'" %
(latestMonth, name, latestMonth)
? print('Sending email to %s...' % email)
? sendmailStatus=smtpObj.sendmail('my_email_address@gmail.com', email, body)
? if sendmailStatus !={}:
print('There was a problem sending email to %s: %s' % (email,
sendmailStatus))
smtpObj.quit()
這段代碼循環遍歷unpaidMembers中的姓名和電子郵件。對于每個沒有付費的成員,我們用最新的月份和成員的名稱,定制了一條消息,并保存在body中?。我們打印輸出,表示正在向這個會員的電子郵件地址發送電子郵件?。然后調用sendmail(),向它傳入地址和定制的消息?。返回值保存在sendmailStatus中。
回憶一下,如果SMTP服務器在發送某個電子郵件時報告錯誤,sendmail()方法將返回一個非空的字典值。for循環最后部分在?行檢查返回的字典是否非空,如果非空,則打印收件人的電子郵件地址以及返回的字典。
程序完成發送所有電子郵件后,調用quit()方法,與SMTP服務器斷開連接。
如果運行該程序,輸出會像這樣:
Sending email to alice@example.com...
Sending email to bob@example.com...
Sending email to eve@example.com...
收件人將收到如圖16-3所示的電子郵件。
圖16-3 從sendDuesReminders.py自動發送的電子郵件
大多數人更可能靠近自己的手機,而不是自己的電腦,所以與電子郵件相比,短信發送通知可能更直接、可靠。此外,短信的長度較短,讓人更有可能閱讀它們。
在本節中,你將學習如何注冊免費的Twilio服務,并用它的Python模塊發送短信。Twilio是一個SMS網關服務,這意味著它是一種服務,讓你通過程序發送短信。雖然每月發送多少短信會有限制,并且文本前面會加上Sent from a Twilio trial account,但這項試用服務也許能滿足你的個人程序。免費試用沒有限期,不必以后升級到付費的套餐。
Twilio不是唯一的SMS網關服務。如果你不喜歡使用Twilio,可以在線搜索free sms gateway、python sms api,甚至twilio alternatives,尋找替代服務。
注冊Twilio賬戶之前,先安裝twilio模塊。附錄A詳細介紹了如何安裝第三方模塊。
本節特別針對美國。Twilio 確實也在美國以外的國家提供手機短信服務,本書并不包括這些細節。但twilio 模塊及其功能,在美國以外的國家也能用。更多信息請參見http://twilio.com/。
訪問http://twilio.com/并填寫注冊表單。注冊了新賬戶后,你需要驗證一個手機號碼,短信將發給該號碼(這項驗證是必要的,防止有人利用該服務向任意的手機號碼發送垃圾短信)。
收到驗證號碼短信后,在Twilio網站上輸入它,證明你擁有要驗證的手機。現在,就可以用twilio模塊向這個電話號碼發送短信了。
Twilio提供的試用賬戶包括一個電話號碼,它將作為短信的發送者。你將需要兩個信息:你的賬戶SID和AUTH(認證)標志。在登錄Twilio賬戶時,可以在Dashboard頁面上找到這些信息。從Python程序登錄時,這些值將作為你的Twilio用戶名和密碼。
一旦安裝了twilio模塊,注冊了Twilio賬號,驗證了你的手機號碼,登記了Twilio電話號碼,獲得了賬戶的SID和auth標志,你就終于準備好通過Python腳本向你自己發短信了。
與所有的注冊步驟相比,實際的Python代碼很簡單。保持計算機連接到因特網,在交互式環境中輸入以下代碼,用你的真實信息替換accountSID、authToken、myTwilioNumber和myCellPhone變量的值:
? >>> from twilio.rest import TwilioRestClient
>>> accountSID='ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
>>> authToken='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
? >>> twilioCli=TwilioRestClient(accountSID, authToken)
>>> myTwilioNumber='+14955551234'
>>> myCellPhone='+14955558888'
? >>> message=twilioCli.messages.create(body='Mr. Watson - Come here - I want
to see you.', from_=myTwilioNumber, to=myCellPhone)
鍵入最后一行后不久,你會收到一條短信,內容為:Sent from your Twilio trial account - Mr. Watson - Come here – I want to see you.。
因為twilio模塊的設計方式,導入它時需要使用from twilio.rest import TwilioRestClient,而不僅僅是import twilio?。將賬戶的SID保存在accountSID,認證標志保存在authToken中,然后調用TwilioRestClient(),并傳入accountSID和authToken。TwilioRestClient()調用返回一個TwilioRestClient對象?。該對象有一個message屬性,該屬性又有一個create()方法,可以用來發送短信。正是這個方法,將告訴Twilio的服務器發送短信。將你的Twilio號碼和手機號碼分別保存在myTwilioNumber和myCellPhone中,然后調用create(),傳入關鍵字參數,指明短信的正文、發件人的號碼(myTwilioNumber),以及收信人的電話號碼(myCellPhone)?
create()方法返回的Message對象將包含已發送短信的相關信息。輸入以下代碼,繼續交互式環境的例子:
>>> message.to
'+14955558888'
>>> message.from_
'+14955551234'
>>> message.body
'Mr. Watson - Come here - I want to see you.'
to、from和body屬性應該分別保存了你的手機號碼、Twilio號碼和消息。請注意,發送手機號碼是在from屬性中,末尾有一個下劃線,而不是from。這是因為from是一個Python關鍵字(例如,你在from modulename import *形式的import語句中見過它),所以它不能作為一個屬性名。輸入以下代碼,繼續交互式環境的例子:
>>> message.status
'queued'
>>> message.date_created
datetime.datetime(2015, 7, 8, 1, 36, 18)
>>> message.date_sent==None
True
status 屬性應該包含一個字符串。如果消息被創建和發送,date_created 和date_sent屬性應該包含一個datetime對象。如果已收到短信,而status屬性卻設置為’queued’,date_sent屬性設置為None,這似乎有點奇怪。這是因為你先將Message對象記錄在message變量中,然后短信才實際發送。你需要重新獲取Message對象,查看它最新的status和date_sent。每個Twilio消息都有唯一的字符串ID(SID),可用于獲取Message對象的最新更新。輸入以下代碼,繼續交互式環境的例子:
>>> message.sid
'SM09520de7639ba3af137c6fcb7c5f4b51'
? >>> updatedMessage=twilioCli.messages.get(message.sid)
>>> updatedMessage.status
'delivered'
>>> updatedMessage.date_sent
datetime.datetime(2015, 7, 8, 1, 36, 18)
輸入message.sid將顯示這個消息的SID。將這個SID傳入Twilio客戶端的get()方法?,你可以取得一個新的Message對象,包含最新的信息。在這個新的Message對象中,status和date_sent屬性是正確的。
status屬性將設置為下列字符串之一:’queued’、’sending’、’sent’、’delivered’、’undelivered’或’failed’。這些狀態不言自明,但對于更準確的細節,請查看http://nostarch. com/automatestuff/的資源。
{用Python接收短信!!}
遺憾的是,用Twilio接收短信比發送短信更復雜一些。Twilio需要你有一個網站,運行自己的Web應用程序。這已超出了本書的范圍,但你可以在本書的資源中找到更多細節(http://nostarch.com/automatestuff/)。
最常用你的程序發短信的人可能就是你。當你遠離計算機時,短信是通知你自己的好方式。如果你已經用程序自動化了一個無聊的任務,它需要運行幾小時,你可以在它完成時,讓它用短信通知你。或者可以定期運行某個程序,它有時需要與你聯系,例如天氣檢查程序,用短信提醒你帶傘。
舉一個簡單的例子,下面是一個Python小程序,包含了textmyself()函數,它將傳入的字符串參數作為短信發出。打開一個新的文件編輯器窗口,輸入以下代碼,用自己的信息替換帳戶SID,認證標志和電話號碼。將它保存為textMyself.py。
#! python3
# textMyself.py - Defines the textmyself() function that texts a message
# passed to it as a string.
# Preset values:
accountSID='ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
authToken='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
myNumber='+15559998888'
twilioNumber='+15552225678'
from twilio.rest import TwilioRestClient
? def textmyself(message):
? twilioCli=TwilioRestClient(accountSID, authToken)
? twilioCli.messages.create(body=message, from_=twilioNumber, to=myNumber)
該程序保存了賬戶的SID、認證標志、發送號碼及接收號碼。然后它定義了textmyself(),接收參數?,創建TwilioRestClient對象?,并用你傳入的消息調用create()?。
如果你想讓其他程序使用textmyself()函數,只需將textMyself.py文件和Python的可執行文件放在同一個文件夾中(Windows上是C:\Python34,OS X上是/usr/local/lib/python3.4,Linux上是/usr/bin/python3)。現在,你可以在其他程序中使用該函數。只要想在程序中發短信給你,就添加以下代碼:
import textmyself
textmyself.textmyself('The boring task is finished.')
注冊Twilio和編寫短信代碼只要做一次。在此之后,從任何其他程序中發短信,只要兩行代碼。
通過因特網和手機網絡,我們用幾十種不同的方式相互通信,但以電子郵件和短信為主。你的程序可以通過這些渠道溝通,這給它們帶來強大的新通知功能。甚至可以編程運行在不同的計算機上,相互直接通過電子郵件能信,一個程序用SMTP發送電子郵件,另一個用IMAP收取。
Python 的 smtplib 提供了一些函數,利用 SMTP,通過電子郵件提供商的SMTP服務器發送電子郵件。同樣,第三方的imapclient和pyzmail模塊讓你訪問IMAP服務器,并取回發送給你的電子郵件。雖然IMAP比SMTP復雜一些,但它也相當強大,允許你搜索特定電子郵件、下載它們、解析它們,提取主題和正文作為字符串值。
短信與電子郵件有點不同,因為它不像電子郵件,發送短信不僅需要互聯網連接。好在,像Twilio這樣的服務提供了模塊,允許你通過程序發送短信。一旦通過了初始設置過程,就能夠只用幾行代碼來發送短信。掌握了這些模塊,就可以針對特定的情況編程,在這些情況下發送通知或提醒。現在,你的程序將超越運行它們的計算機!
本文摘自《Python編程快速上手 讓繁瑣工作自動化》
本書是一本面向實踐的Python編程實用指南。本書的目的,不僅是介紹Python語言的基礎知識,而且還通過項目實踐教會讀者如何應用這些知識和技能。本書的第一部分介紹了基本的Python編程概念,第二部分介紹了一些不同的任務,通過編寫Python程序,可以讓計算機自動完成它們。第二部分的每一章都有一些項目程序,供讀者學習。每章的末尾還提供了一些習題和深入的實踐項目,幫助讀者鞏固所學的知識。附錄部分提供了所有習題的解答。
本書適合任何想要通過Python學習編程的讀者,尤其適合缺乏編程基礎的初學者。通過閱讀本書,讀者將能利用最強大的編程語言和工具,并且將體會到Python編程的快樂。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。