# Python 學習筆記 : 以 Gmail 寄送郵件的方法 (一)
模組 smtplib 詳細文件參考 :
# https://docs.python.org/3/library/smtplib.html
本篇測試主要參考了下列書籍 :
# Python 入門邁向高手之路-王者歸來 (深石, 洪錦魁) 第 26 章
# Python:網路爬蟲與資料分析入門實戰 (博碩, 林俊瑋) 第 8 章
在上一篇文章中使用 smtplib 的 sendmail() 函數傳送郵件, 但那只適用於 ASCII 編碼之郵件內容, 例如英數字或符號等, 如果參數 msg 中含有中文字元, 則將出現 UnicodeEncodeError 錯誤訊息, 例如 :
TLS 版 :
>>> import smtplib
>>> smtp=smtplib.SMTP("smtp.gmail.com", 587)
>>> type(smtp)
<class 'smtplib.SMTP'>
>>> smtp.ehlo()
(250, b'smtp.gmail.com at your service, [111.254.51.104]\nSIZE 35882577\n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8')
>>> smtp.sendmail("mygmail@gmail.com", "myhinetmail@ms5.hinet.net", "您好")
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "C:\Python36\lib\smtplib.py", line 854, in sendmail
msg = _fix_eols(msg).encode('ascii')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
SSL 版 :
>>> import smtplib
>>> smtpssl=smtplib.SMTP_SSL("smtp.gmail.com", 465)
>>> smtpssl.ehlo()
(250, b'smtp.gmail.com at your service, [111.254.51.104]\nSIZE 35882577\n8BITMIME\nAUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8')
>>> smtpssl.sendmail("mygmail@gmail.com", "myhinetmail@ms5.hinet.net", "您好")
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "C:\Python36\lib\smtplib.py", line 854, in sendmail
msg = _fix_eols(msg).encode('ascii')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 38-39: ordinal not in range(128)
# 多用途郵件擴展協定
# MIME協議在郵件中的應用詳解
MIME 協定原本是為了解決舊的 Email 傳輸協定 RFC822 無法傳送多媒體格式資料的問題, 後來也被 HTTP 協定採用為標準的一部份. MIME 主要由 type (資料型態) 與 subtype (次型態) 構成, type 有四種 :
MIME 資料型態 type | 說明 |
text | 文字 |
image | 圖片 |
audio | 音訊 |
video | 視訊 |
multipart | 連結各部分資料為一整體資訊 |
message | 包裝 Email 訊息 |
application | 應用程式資料或二進位資料 |
常用的次型態 subtype 如下表所示 :
MIME 資料次型態 subtype | 說明 |
application/octet-stream | 二進位資料 |
application/pdf | Adobe pdf 文件 |
application/msword | Microsoft Word 文件 |
text/plain | 純文字 |
text/html | HTML 網頁 |
image/gif | GIF 圖片 |
image/png | PNG 圖片 |
image/jpeg | JPEG 圖片 |
video/mp4 | MP4 影片 |
video/mpeg | MPEG 影片 |
video/ogg | OGG 影片 |
video/webm | WEBM 影片 |
multipart/x-www-form-urlencoded | HTTP 以 POST 提交之表單 |
multipart/form-data | HTTP 以 POST 提交之表單有附檔上傳 |
Python 內建模組 email.mime.text 的 MIMEText 類別支援 MIME 協定, 可以處理非 ASCII 編碼文字郵件與二進位檔問題, 其介面如下 :
email.mime.text.MIMEText(text [, subtype [, charset]])
這些參數都是字串, 說明如下 :
- text : 要傳送的資料 (文字, 圖片, 視訊, 檔案等)
- subtype : 資料的次型態, 預設為 "plain" (純文字)
- charset : 資料編碼類型, 預設為 "us-ascii"
使用 MIME 協定傳送多媒體郵件前須從 email.mime.text 模組匯入 MIMEText() 類別 :
>>> from email.mime.text import MIMEText
然後呼叫 MIMEText() 建構子建立 MIME 物件 :
>>> mime=MIMEText("您好! 我是 Tony.", "plain", "utf-8")
>>> type(mime)
<class 'email.mime.text.MIMEText'>
- Subject : 主旨
- From : 寄件人暱稱
- To : 收件人暱稱
- Cc : 副本收件者 Email 位址 (多個用逗號隔開)
>>> mime["Subject"]="Gmail sent by Python scripts(MIME)"
>>> mime["From"]="Your best friend"
>>> mime["To"]="mailgroup"
>>> mime["Cc"]="myyahoomail@yahoo.com, mycompanymail@blablabla.com.tw"
>>> msg=mime.as_string() #物件轉成字串
>>> type(msg)
<class 'str'>
>>> print(msg)
'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: base64\nSubject: Gmail sent by Python scripts(MIME)\nFrom: Your best friend\nTo: mailgroup\nCc: myyahoomail@yahoo.com, mycompanymail@blablabla.com.tw\n\n5oKo5aW9ISDmiJHmmK8gVG9ueS4=\n'
準備好 MIME 字串後便可連線 Gmaail 主機傳送信件了 :
>>> smtp=smtplib.SMTP("smtp.gmail.com", 587)
>>> smtp.ehlo()
>>> smtp.starttls()
>>> smtp.login("mygmail@gmail.com", "yguxhsurqwpseksw")
>>> from_addr="mygmail@gmail.com"
>>> to_addr=["myhinetmail@msa.hinet.net"]
>>> status=smtp.sendmail(from_addr, to_addr, msg)
>>> if status=={}:
print("郵件傳送成功!")
else:
print("郵件傳送失敗!")
郵件傳送成功!
>>> smtp.quit()
(221, b'2.0.0 closing connection x23-v6sm18066135pfh.56 - gsmtp')
完整程式如下 :
測試 1 : 用 MIME 傳送中文信件
import smtplib
from email.mime.text import MIMEText
mime=MIMEText("您好! 我是 Tony.", "plain", "utf-8")
mime["Subject"]="Gmail sent by Python scripts(MIME)"
mime["From"]="Your best friend"
mime["To"]="mailgroup"
mime["Cc"]="myyahoomail@yahoo.com, mycompanymail@blablabla.com.tw"
msg=mime.as_string()
smtp=smtplib.SMTP("smtp.gmail.com", 587)
smtp.ehlo()
smtp.starttls()
smtp.login("mygmail@gmail.com", "yguxhsurqwpseksw")
from_addr="mygmail@gmail.com"
to_addr=["myhinetmail@msa.hinet.net"]
from_addr="mygmail@gmail.com"
to_addr=["myhinetmail@msa.hinet.net"]
status=smtp.sendmail(from_addr, to_addr, msg)
if status=={}:
print("郵件傳送成功!")
else:
print("郵件傳送失敗!")
smtp.quit()
測試 2 : 用 MIME 傳送 HTML 信件
import smtplib
from email.mime.text import MIMEText
html="""
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<title>HTML mail</title>
</head>
<body>
<b>HTML 郵件測試</b>
</body>
</html>
"""
mime=MIMEText(html, "html", "utf-8")
mime["Subject"]="Gmail sent by Python scripts(MIME)"
mime["From"]="Your best friend"
mime["To"]="mailgroup"
mime["Cc"]="myyahoomail@yahoo.com, mycompanymail@blablabla.com.tw"
msg=mime.as_string()
print(msg)
smtp=smtplib.SMTP("smtp.gmail.com", 587)
smtp.ehlo()
smtp.starttls()
smtp.login("mygmail@gmail.com", "yguxhsurqwpseksw")
from_addr="mygmail@gmail.com"
to_addr=["myhinetmail@msa.hinet.net"]
status=smtp.sendmail(from_addr, to_addr, msg)
if status=={}:
print("郵件傳送成功!")
else:
print("郵件傳送失敗!")
smtp.quit()
此程式中的 print(msg) 將轉成字串後的信件內容列印出來, 如下所示 :
>>> %Run test.py
Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Subject: Gmail sent by Python scripts(MIME)
From: Your best friend
To: mailgroup
Cc: mycompanymail@blablabla.com.tw
CjwhZG9jdHlwZSBodG1sPgo8aHRtbD4KPGhlYWQ+CiAgPG1ldGEgY2hhcnNldD0ndXRmLTgnPgog
IDx0aXRsZT5IVE1MIG1haWw8L3RpdGxlPgo8L2hlYWQ+Cjxib2R5PgogIDxiPkhUTUwg6YO15Lu2
5ris6KmmPC9iPgo8L2JvZHk+CjwvaHRtbD4K
郵件傳送成功!
其中最底下的一堆怪碼就是以 unicode 編碼後的 html 郵件內容.
傳送文字檔附件須以 binary 方式讀取檔案後, 以 base64 次型態編碼成 ASCII 字元傳送, 關於 base64 參考 :
# https://zh.wikipedia.org/wiki/Base64
另外, MIME 物件須設定 Content-Type 屬性為 octet-stream, Content-Disposition 屬性設為 attachment 並指定檔案名稱, 例如 :
測試 3 : 以郵件傳送純文字附檔
import smtplib
from email.mime.text import MIMEText
with open("test.txt", "rb") as file:
filecontent=file.read()
mime=MIMEText(filecontent, "base64", "utf-8")
mime["Content-Type"]="application/octet-stream"
mime["Content-Disposition"]='attachment; filename="test.txt"'
mime["Subject"]="Gmail sent by Python scripts(附檔測試)"
mime["From"]="Your best friend"
mime["To"]="mailgroup"
mime["Cc"]="mycompanymail@blablabla.com.tw"
msg=mime.as_string()
print(msg)
smtp=smtplib.SMTP("smtp.gmail.com", 587)
smtp.ehlo()
smtp.starttls()
smtp.login("mygmail@gmail.com", "yguxhsurqwpseksw")
from_addr="mygmail@gmail.com"
to_addr=["myhinetmail@msa.hinet.net"]
status=smtp.sendmail(from_addr, to_addr, msg)
if status=={}:
print("郵件傳送成功!")
else:
print("郵件傳送失敗!")
smtp.quit()
此程式開啟所在目錄底下的文字檔 test.txt, 其內容如下 :
"您好! 這是附檔測試."
Hinet Webmail 收到的信件如下, 可見信件中有附件 test.txt 檔 :
注意, 在 MIME 物件的 Content-Disposition 中, 附檔的檔名必須用雙引號括起來, 不要用單引號, 整個屬性則用單引號, 如下所示 :
mime["Content-Disposition"]='attachment; filename="test.txt"'
如果附檔的檔名用單引號, 整個屬性就要用雙引號括起來 :
mime["Content-Disposition"]="attachment; filename='test.txt'"
但是這樣收到的附件檔名會變成 'test.txt', 即兩邊的單引號會變成檔名的一部分, 下載下來後還要變更檔案名稱很麻煩, 如下所示 :
在郵件中傳送圖檔須使用內建模組的 email.mime.image.MIMEImage 類別, 其介面如下 :
email.mime.image.MIMEImage(_imagedata[, _subtype[, _encoder[, **_params]]])
處理附帶圖檔之郵件要匯入如下模組與類別 :
import smtplib
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
圖檔以 binary 方式讀取後傳給 MIMEImage 類別的建構子建立 mime 物件, 與傳送文字檔案一樣指定其 Content-Type 屬性為 "application/octet-stream", 於 Content-Disposition 屬性中指定附檔檔名即可, 例如 :
測試 4 : 以郵件傳送圖檔
import smtplib
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
with open("桌面_海灣.jpg", "rb") as file:
filecontent=file.read()
mime=MIMEImage(filecontent)
mime["Content-Type"]="application/octet-stream"
mime["Content-Disposition"]='attachment; filename="桌面_海灣.jpg"'
mime["Subject"]="Gmail sent by Python scripts(圖片附檔測試)"
mime["From"]="Your best friend"
mime["To"]="mailgroup"
mime["Cc"]="mycompanymail@blablabla.com.tw"
msg=mime.as_string()
print(msg)
smtp=smtplib.SMTP("smtp.gmail.com", 587)
smtp.ehlo()
smtp.starttls()
smtp.login("mygmail@gmail.com", "yguxhsurqwpseksw")
from_addr="mygmail@gmail.com"
to_addr=["myhinetmail@msa.hinet.net"]
status=smtp.sendmail(from_addr, to_addr, msg)
if status=={}:
print("郵件傳送成功!")
else:
print("郵件傳送失敗!")
smtp.quit()
奇怪的是檔名變成 "HiNet-ATT001" 且沒有副檔名 jpg, 但下載添加副檔名 jpg 後開啟卻又是是預期之圖檔 :
改用 Outlook 開啟收到的 Email, 收到的附檔是副檔名為 .dat 的附檔 :
下載 .dat 附檔後更改副檔名為 .jpg 同樣是預期的圖檔. 可見不同的郵件伺服器對 "application/stream-octet" 次型態資料附檔有不同的處理方式.
不過既然是 jpg 檔, 次型態不是應該用 "image/jpeg" 嗎? 我將上面的範例如下 :
測試 5 : 以郵件傳送圖檔 (修正)
import smtplib
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
with open("桌面_海灣.jpg", "rb") as file:
filecontent=file.read()
mime=MIMEImage(filecontent, "image/jpeg")
mime["Content-Type"]="image/jpeg"
mime["Content-Disposition"]='attachment; filename="桌面_海灣.jpg"'
mime["Subject"]="Gmail sent by Python scripts(圖片附檔測試)"
mime["From"]="Your best friend"
mime["To"]="mailgroup"
#mime["Cc"]="mycompanymail@blablabla.com.tw"
msg=mime.as_string()
print(msg)
smtp=smtplib.SMTP("smtp.gmail.com", 587)
smtp.ehlo()
smtp.starttls()
smtp.login("mygmail@gmail.com", "yguxhsurqwpseksw")
from_addr="mygmail@gmail.com"
to_addr=["myhinetmail@msa.hinet.net"]
status=smtp.sendmail(from_addr, to_addr, msg)
if status=={}:
print("郵件傳送成功!")
else:
print("郵件傳送失敗!")
smtp.quit()
結果在 Hinet webmail 上收到的附檔依然是沒有副檔名的 HiNet-ATT001, 下載後添加副檔名 .jpg 也是正確之圖檔. 但是在 Outlook 上收信則明確帶了 .jpg 的副檔名 :
可見指定正確的 subtype 還是有作用的, 但 Hinet webmail 郵件主機無法帶上正確副檔名還是個問題.
參考 :
# Python加密與解密
# python字符串加密解密的三种方法分享(base64 win32com)
沒有留言:
張貼留言