# SMTP發送郵件
SMTP是發送郵件的協議,Python內置對SMTP的支持,可以發送純文本郵件、HTML郵件以及帶附件的郵件。
Python對SMTP支持有`smtplib`和`email`兩個模塊,`email`負責構造郵件,`smtplib`負責發送郵件。
首先,我們來構造一個最簡單的純文本郵件:
```
from email.mime.text import MIMEText
msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
```
注意到構造`MIMEText`對象時,第一個參數就是郵件正文,第二個參數是MIME的subtype,傳入`'plain'`表示純文本,最終的MIME就是`'text/plain'`,最后一定要用`utf-8`編碼保證多語言兼容性。
然后,通過SMTP發出去:
```
# 輸入Email地址和口令:
from_addr = input('From: ')
password = input('Password: ')
# 輸入收件人地址:
to_addr = input('To: ')
# 輸入SMTP服務器地址:
smtp_server = input('SMTP server: ')
import smtplib
server = smtplib.SMTP(smtp_server, 25) # SMTP協議默認端口是25
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
```
我們用`set_debuglevel(1)`就可以打印出和SMTP服務器交互的所有信息。SMTP協議就是簡單的文本命令和響應。`login()`方法用來登錄SMTP服務器,`sendmail()`方法就是發郵件,由于可以一次發給多個人,所以傳入一個`list`,郵件正文是一個`str`,`as_string()`把`MIMEText`對象變成`str`。
如果一切順利,就可以在收件人信箱中收到我們剛發送的Email:

仔細觀察,發現如下問題:
1. 郵件沒有主題;
2. 收件人的名字沒有顯示為友好的名字,比如`Mr Green <green@example.com>`;
3. 明明收到了郵件,卻提示不在收件人中。
這是因為郵件主題、如何顯示發件人、收件人等信息并不是通過SMTP協議發給MTA,而是包含在發給MTA的文本中的,所以,我們必須把`From`、`To`和`Subject`添加到`MIMEText`中,才是一封完整的郵件:
```
from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
from_addr = input('From: ')
password = input('Password: ')
to_addr = input('To: ')
smtp_server = input('SMTP server: ')
msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
msg['From'] = _format_addr('Python愛好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理員 <%s>' % to_addr)
msg['Subject'] = Header('來自SMTP的問候……', 'utf-8').encode()
server = smtplib.SMTP(smtp_server, 25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
```
我們編寫了一個函數`_format_addr()`來格式化一個郵件地址。注意不能簡單地傳入`name <addr@example.com>`,因為如果包含中文,需要通過`Header`對象進行編碼。
`msg['To']`接收的是字符串而不是list,如果有多個郵件地址,用`,`分隔即可。
再發送一遍郵件,就可以在收件人郵箱中看到正確的標題、發件人和收件人:

你看到的收件人的名字很可能不是我們傳入的`管理員`,因為很多郵件服務商在顯示郵件時,會把收件人名字自動替換為用戶注冊的名字,但是其他收件人名字的顯示不受影響。
如果我們查看Email的原始內容,可以看到如下經過編碼的郵件頭:
```
From: =?utf-8?b?UHl0aG9u54ix5aW96ICF?= <xxxxxx@163.com>
To: =?utf-8?b?566h55CG5ZGY?= <xxxxxx@qq.com>
Subject: =?utf-8?b?5p2l6IeqU01UUOeahOmXruWAmeKApuKApg==?=
```
這就是經過`Header`對象編碼的文本,包含utf-8編碼信息和Base64編碼的文本。如果我們自己來手動構造這樣的編碼文本,顯然比較復雜。
## 發送HTML郵件
如果我們要發送HTML郵件,而不是普通的純文本文件怎么辦?方法很簡單,在構造`MIMEText`對象時,把HTML字符串傳進去,再把第二個參數由`plain`變為`html`就可以了:
```
msg = MIMEText('<html><body><h1>Hello</h1>' +
'<p>send by <a href="http://www.python.org">Python</a>...</p>' +
'</body></html>', 'html', 'utf-8')
```
再發送一遍郵件,你將看到以HTML顯示的郵件:

## 發送附件
如果Email中要加上附件怎么辦?帶附件的郵件可以看做包含若干部分的郵件:文本和各個附件本身,所以,可以構造一個`MIMEMultipart`對象代表郵件本身,然后往里面加上一個`MIMEText`作為郵件正文,再繼續往里面加上表示附件的`MIMEBase`對象即可:
```
# 郵件對象:
msg = MIMEMultipart()
msg['From'] = _format_addr('Python愛好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理員 <%s>' % to_addr)
msg['Subject'] = Header('來自SMTP的問候……', 'utf-8').encode()
# 郵件正文是MIMEText:
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
# 添加附件就是加上一個MIMEBase,從本地讀取一個圖片:
with open('/Users/michael/Downloads/test.png', 'rb') as f:
# 設置附件的MIME和文件名,這里是png類型:
mime = MIMEBase('image', 'png', filename='test.png')
# 加上必要的頭信息:
mime.add_header('Content-Disposition', 'attachment', filename='test.png')
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的內容讀進來:
mime.set_payload(f.read())
# 用Base64編碼:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
msg.attach(mime)
```
然后,按正常發送流程把`msg`(注意類型已變為`MIMEMultipart`)發送出去,就可以收到如下帶附件的郵件:

## 發送圖片
如果要把一個圖片嵌入到郵件正文中怎么做?直接在HTML郵件中鏈接圖片地址行不行?答案是,大部分郵件服務商都會自動屏蔽帶有外鏈的圖片,因為不知道這些鏈接是否指向惡意網站。
要把圖片嵌入到郵件正文中,我們只需按照發送附件的方式,先把郵件作為附件添加進去,然后,在HTML中通過引用`src="cid:0"`就可以把附件作為圖片嵌入了。如果有多個圖片,給它們依次編號,然后引用不同的`cid:x`即可。
把上面代碼加入`MIMEMultipart`的`MIMEText`從`plain`改為`html`,然后在適當的位置引用圖片:
```
msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
'<p><img src="cid:0"></p>' +
'</body></html>', 'html', 'utf-8'))
```
再次發送,就可以看到圖片直接嵌入到郵件正文的效果:

## 同時支持HTML和Plain格式
如果我們發送HTML郵件,收件人通過瀏覽器或者Outlook之類的軟件是可以正常瀏覽郵件內容的,但是,如果收件人使用的設備太古老,查看不了HTML郵件怎么辦?
辦法是在發送HTML的同時再附加一個純文本,如果收件人無法查看HTML格式的郵件,就可以自動降級查看純文本郵件。
利用`MIMEMultipart`就可以組合一個HTML和Plain,要注意指定subtype是`alternative`:
```
msg = MIMEMultipart('alternative')
msg['From'] = ...
msg['To'] = ...
msg['Subject'] = ...
msg.attach(MIMEText('hello', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))
# 正常發送msg對象...
```
## 加密SMTP
使用標準的25端口連接SMTP服務器時,使用的是明文傳輸,發送郵件的整個過程可能會被竊聽。要更安全地發送郵件,可以加密SMTP會話,實際上就是先創建SSL安全連接,然后再使用SMTP協議發送郵件。
某些郵件服務商,例如Gmail,提供的SMTP服務必須要加密傳輸。我們來看看如何通過Gmail提供的安全SMTP發送郵件。
必須知道,Gmail的SMTP端口是587,因此,修改代碼如下:
```
smtp_server = 'smtp.gmail.com'
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
# 剩下的代碼和前面的一模一樣:
server.set_debuglevel(1)
...
```
只需要在創建`SMTP`對象后,立刻調用`starttls()`方法,就創建了安全連接。后面的代碼和前面的發送郵件代碼完全一樣。
如果因為網絡問題無法連接Gmail的SMTP服務器,請相信我們的代碼是沒有問題的,你需要對你的網絡設置做必要的調整。
## 小結
使用Python的smtplib發送郵件十分簡單,只要掌握了各種郵件類型的構造方法,正確設置好郵件頭,就可以順利發出。
構造一個郵件對象就是一個`Messag`對象,如果構造一個`MIMEText`對象,就表示一個文本郵件對象,如果構造一個`MIMEImage`對象,就表示一個作為附件的圖片,要把多個對象組合起來,就用`MIMEMultipart`對象,而`MIMEBase`可以表示任何對象。它們的繼承關系如下:
```
Message
+- MIMEBase
+- MIMEMultipart
+- MIMENonMultipart
+- MIMEMessage
+- MIMEText
+- MIMEImage
```
這種嵌套關系就可以構造出任意復雜的郵件。你可以通過[email.mime文檔](https://docs.python.org/3/library/email.mime.html)查看它們所在的包以及詳細的用法。
## 參考源碼
[send_mail.py](https://github.com/michaelliao/learn-python3/blob/master/samples/mail/send_mail.py)
- JavaScript教程
- JavaScript簡介
- 快速入門
- 基本語法
- 數據類型和變量
- 字符串
- 數組
- 對象
- 條件判斷
- 循環
- Map和Set
- iterable
- 函數
- 函數定義和調用
- 變量作用域
- 方法
- 高階函數
- map/reduce
- filter
- sort
- 閉包
- 箭頭函數
- generator
- 標準對象
- Date
- RegExp
- JSON
- 面向對象編程
- 創建對象
- 原型繼承
- 瀏覽器
- 瀏覽器對象
- 操作DOM
- 更新DOM
- 插入DOM
- 刪除DOM
- 操作表單
- 操作文件
- AJAX
- Promise
- Canvas
- jQuery
- 選擇器
- 層級選擇器
- 查找和過濾
- 操作DOM
- 修改DOM結構
- 事件
- 動畫
- 擴展
- underscore
- Collections
- Arrays
- Functions
- Objects
- Chaining
- Node.js
- 安裝Node.js和npm
- 第一個Node程序
- 模塊
- 基本模塊
- fs
- stream
- http
- buffer
- Web開發
- koa
- mysql
- swig
- 自動化工具
- 期末總結
- Python 2.7教程
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷和循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 函數式編程
- 高階函數
- map/reduce
- filter
- sorted
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 使用__future__
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 面向對象高級編程
- 使用__slots__
- 使用@property
- 多重繼承
- 定制類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- 常用第三方模塊
- PIL
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 協程
- gevent
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫數據庫模塊
- 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
- 期末總結
- Python3教程
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- Python代碼運行助手
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷
- 循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 迭代器
- 函數式編程
- 高階函數
- map/reduce
- filter
- sorted
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 實例屬性和類屬性
- 面向對象高級編程
- 使用__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
- async/await
- 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
- 期末總結
- Git教程
- Git簡介
- Git的誕生
- 集中式vs分布式
- 安裝Git
- 創建版本庫
- 時光機穿梭
- 版本回退
- 工作區和暫存區
- 管理修改
- 撤銷修改
- 刪除文件
- 遠程倉庫
- 添加遠程庫
- 從遠程庫克隆
- 分支管理
- 創建與合并分支
- 解決沖突
- 分支管理策略
- Bug分支
- Feature分支
- 多人協作
- 標簽管理
- 創建標簽
- 操作標簽
- 使用GitHub
- 自定義Git
- 忽略特殊文件
- 配置別名
- 搭建Git服務器
- 期末總結