SMTP用于發送郵件,如果要收取郵件呢?
收取郵件就是編寫一個**MUA**作為客戶端,從**MDA**把郵件獲取到用戶的電腦或者手機上。收取郵件最常用的協議是**POP**協議,目前版本號是3,俗稱**POP3**。
Python內置一個`poplib`模塊,實現了POP3協議,可以直接用來收郵件。
注意到POP3協議收取的不是一個已經可以閱讀的郵件本身,而是郵件的原始文本,這和SMTP協議很像,SMTP發送的也是經過編碼后的一大段文本。
要把POP3收取的文本變成可以閱讀的郵件,還需要用`email`模塊提供的各種類來解析原始文本,變成可閱讀的郵件對象。
所以,收取郵件分兩步:
第一步:用`poplib`把郵件的原始文本下載到本地;
第二部:用`email`解析原始文本,還原為郵件對象。
### 通過POP3下載郵件
POP3協議本身很簡單,以下面的代碼為例,我們來獲取最新的一封郵件內容:
~~~
import poplib
# 輸入郵件地址, 口令和POP3服務器地址:
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 server: ')
# 連接到POP3服務器:
server = poplib.POP3(pop3_server)
# 可以打開或關閉調試信息:
server.set_debuglevel(1)
# 可選:打印POP3服務器的歡迎文字:
print(server.getwelcome().decode('utf-8'))
# 身份認證:
server.user(email)
server.pass_(password)
# stat()返回郵件數量和占用空間:
print('Messages: %s. Size: %s' % server.stat())
# list()返回所有郵件的編號:
resp, mails, octets = server.list()
# 可以查看返回的列表類似[b'1 82923', b'2 2184', ...]
print(mails)
# 獲取最新一封郵件, 注意索引號從1開始:
index = len(mails)
resp, lines, octets = server.retr(index)
# lines存儲了郵件的原始文本的每一行,
# 可以獲得整個郵件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出郵件:
msg = Parser().parsestr(msg_content)
# 可以根據郵件索引號直接從服務器刪除郵件:
# server.dele(index)
# 關閉連接:
server.quit()
~~~
用POP3獲取郵件其實很簡單,要獲取所有郵件,只需要循環使用`retr()`把每一封郵件內容拿到即可。真正麻煩的是把郵件的原始內容解析為可以閱讀的郵件對象。
### 解析郵件
解析郵件的過程和上一節構造郵件正好相反,因此,先導入必要的模塊:
~~~
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import poplib
~~~
只需要一行代碼就可以把郵件內容解析為`Message`對象:
~~~
msg = Parser().parsestr(msg_content)
~~~
但是這個`Message`對象本身可能是一個`MIMEMultipart`對象,即包含嵌套的其他`MIMEBase`對象,嵌套可能還不止一層。
所以我們要遞歸地打印出`Message`對象的層次結構:
~~~
# indent用于縮進顯示:
def print_info(msg, indent=0):
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header=='Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type=='text/plain' or content_type=='text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))
~~~
郵件的Subject或者Email中包含的名字都是經過編碼后的str,要正常顯示,就必須decode:
~~~
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
~~~
`decode_header()`返回一個list,因為像`Cc`、`Bcc`這樣的字段可能包含多個郵件地址,所以解析出來的會有多個元素。上面的代碼我們偷了個懶,只取了第一個元素。
文本郵件的內容也是str,還需要檢測編碼,否則,非UTF-8編碼的郵件都無法正常顯示:
~~~
def guess_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset
~~~
把上面的代碼整理好,我們就可以來試試收取一封郵件。先往自己的郵箱發一封郵件,然后用瀏覽器登錄郵箱,看看郵件收到沒,如果收到了,我們就來用Python程序把它收到本地:

運行程序,結果如下:
~~~
+OK Welcome to coremail Mail Pop3 Server (163coms[...])
Messages: 126\. Size: 27228317
From: Test <xxxxxx@qq.com>
To: Python愛好者 <xxxxxx@163.com>
Subject: 用POP3收取郵件
part 0
--------------------
part 0
--------------------
Text: Python可以使用POP3收取郵件……...
part 1
--------------------
Text: Python可以<a href="...">使用POP3</a>收取郵件……...
part 1
--------------------
Attachment: application/octet-stream
~~~
我們從打印的結構可以看出,這封郵件是一個`MIMEMultipart`,它包含兩部分:第一部分又是一個`MIMEMultipart`,第二部分是一個附件。而內嵌的`MIMEMultipart`是一個`alternative`類型,它包含一個純文本格式的`MIMEText`和一個HTML格式的`MIMEText`。
### 小結
用Python的`poplib`模塊收取郵件分兩步:第一步是用POP3協議把郵件獲取到本地,第二步是用`email`模塊把原始郵件解析為`Message`對象,然后,用適當的形式把郵件內容展示給用戶即可。
### 參考源碼
[fetch_mail.py](https://github.com/michaelliao/learn-python3/blob/master/samples/mail/fetch_mail.py)
- 關于
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- Python代碼運行助手
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷
- 循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 迭代器
- 函數式編程
- 高階函數
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 實例屬性和類屬性
- 面向對象高級編程
- 使用slots
- 使用@property
- 多重繼承
- 定制類
- 使用枚舉類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- StringIO和BytesIO
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- datetime
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- urllib
- 常用第三方模塊
- PIL
- virtualenv
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 異步IO
- 協程
- asyncio
- aiohttp
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫Web App骨架
- Day 3 - 編寫ORM
- Day 4 - 編寫Model
- Day 5 - 編寫Web框架
- Day 6 - 編寫配置文件
- Day 7 - 編寫MVC
- Day 8 - 構建前端
- Day 9 - 編寫API
- Day 10 - 用戶注冊和登錄
- Day 11 - 編寫日志創建頁
- Day 12 - 編寫日志列表頁
- Day 13 - 提升開發效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 編寫移動App
- FAQ
- 期末總結