這兩天在 Mapleboard 上安裝 postfix 與 mailutils 工具包成功地從命令列透過 Google SMTP 郵件伺服器傳送 Email, 其實也可以用 Python 的 smtplib 套件來做, 我之前在鄉下那台樹莓派 Pi 3 主機也是用這方法將光世代網路的外網 IP 寄到我的 Hinet 信箱以便能遠端連線, 參考 :
今天我將其中 Python 程式 reportip3.py 的寄信功能寫成一個 send_mail() 函式方便應用程式呼叫.
1. 傳送 ASCII 信件 :
如果信件內容是英數字等 ASCII 編碼的字元, 可用下列函式 send_mail() 來傳送 :
# smtp_mail_test_1.py
import smtplib
def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, body=''):
if cc_addr is None:
cc_addr=[] # 預設空串列
smtp=smtplib.SMTP('smtp.gmail.com', 587)
smtp.ehlo()
smtp.starttls() # 啟動 TLS 安全傳輸
smtp.login(account, password) # 登入 Google SMTP 伺服器
'''content=(
f"Subject:{subject}\n"
f"From:{from_addr}\n"
f"To:{', '.join(to_addr)}\n"
f"Cc:{', '.join(cc_addr)}\n"
f"{body}"
)'''
content=(
"Subject:{subject}\n"
"From:{from_addr}\n"
"To:{to_addr}\n"
"Cc:{cc_addr}\n"
"{body}"
).format(
subject=subject,
from_addr=from_addr,
to_addr=', '.join(to_addr),
cc_addr=', '.join(cc_addr),
body=body
)
all_recipients=to_addr + cc_addr # 合併收件人和副本收件人
status=smtp.sendmail(from_addr, all_recipients, content)
if status == {}:
print('郵件傳送成功!')
else:
print('郵件傳送失敗!')
smtp.quit()
account='mygmail@gmail.com' # Gmail 帳號
password='azfkqbjbftodjucd' # Google 應用程式密碼 (這是樣本)
subject='' # 主旨
from_addr='mygmail@gmail.com' # 寄件人
to_addr=['myhinet@ms5.hinet.net'] # 收件人
cc_addr=['tony@xxx.com.tw'] # 副本收件人(無設為 None)
body='您好, 這是測試信' # 信件內容
send_mail(account, password, subject, from_addr, to_addr, cc_addr, body)
此處因為我的 Pi 3 安裝的 Python 是 3.5 版不支援 f 字串, 因此改用 format() 來塞變數到字串中. 如果是 Python 3.6+ 版就可以使用 f 字串了. 此程式是從我的 Gmail 信箱傳送 Email 到我的 Hinet 與公司信箱 (副本), 結果兩個信箱都有成功地收到郵件 :
注意, 程式中的 password 並不是 Google 密碼, 而是啟用 Google 二階段驗證之後, 到下列網址產生的 Google 應用程式密碼 :
在 "應用程式名稱" 框內輸入可辨識的名稱, 按右下角的 "建立" 鈕即可 :
將產生的密碼複製下來, 去除中間的分隔用的空白字元後即可使用, 但最好先儲存到文字檔中備查, 因為它只顯示一次, 若沒記下來, 下次要用時須重新建立.
2. 傳送非 ASCII 信件 :
不過上面的函式只能用來傳送 ASCII 字元, 如果主旨與內容含有非 ASCII 字元例如中文, 執行時會出現 "UnicodeEncodeError: 'ascii' codec can't encode characters" 的錯誤訊息.
解決辦法是用 Python 內建的 email 模組中的 email.mime.text.MIMEText 類別來處理 Unicode, 此外, 為了能寄出帶有 PDF與圖檔等附件的信件, 還需要 email.mime.multipart.MIMEMultipart 來打包郵件內容, 完整程式碼如下 :
# smtp_mail_test_2.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, body=''):
if cc_addr is None:
cc_addr=[] # 預設空串列
# 登入 Google SMTP 伺服器
smtp=smtplib.SMTP('smtp.gmail.com', 587)
smtp.ehlo()
smtp.starttls() # 啟動 TLS 安全傳輸
smtp.login(account, password) # 登入 Google SMTP 伺服器
# 建立郵件內容
content=MIMEMultipart()
content['Subject']=subject
content['From']=from_addr
content['To']=', '.join(to_addr)
content['Cc']=', '.join(cc_addr)
# 添加郵件正文
content.attach(MIMEText(body, 'plain', 'utf-8'))
# 合併收件人和副本收件人
all_recipients=to_addr + cc_addr
status=smtp.sendmail(from_addr, all_recipients, content.as_string())
if status == {}:
print('郵件傳送成功!')
else:
print('郵件傳送失敗!')
smtp.quit()
account='mygmail@gmail.com' # Gmail 帳號
password='azfkqbjbftodjucd' # Google 應用程式密碼 (這是樣本)
subject='郵件測試' # 主旨
from_addr='mygmail@gmail.com' # 寄件人
to_addr=['myhinet@ms5.hinet.net'] # 收件人
cc_addr=['tony@xxx.com.tw'] # 副本收件人(無設為 None)
body='您好, 這是測試信' # 信件內容
send_mail(account, password, subject, from_addr, to_addr, cc_addr, body)
結果如下 :
如果只是傳送純文字的中文 Email, 那麼這個程式就堪用了. 但是如果傳送 HTML 格式的內容, 例如將 body 改為如下粗體 :
body='<b>您好, 這是測試信</b>'
則它會以 HTML 碼純文字傳送, 結果如下 :
3. 傳送網頁格式信件 :
如果郵件內容是 HTML 網頁碼, 則在呼叫建構式 MIMEText() 時要傳入 mode 參數為 'html' :
MIMEText(body, 'html', 'utf-8')
修改 send_mail() 函式添加一個 mode 參數來控制內容模式為 'plain' 或 'html' :
# smtp_mail_test_3.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, body='', mode='html'):
if cc_addr is None:
cc_addr=[] # 預設空串列
# 登入 Google SMTP 伺服器
smtp=smtplib.SMTP('smtp.gmail.com', 587)
smtp.ehlo()
smtp.starttls() # 啟動 TLS 安全傳輸
smtp.login(account, password) # 登入 Google SMTP 伺服器
# 建立郵件內容
content=MIMEMultipart()
content['Subject']=subject
content['From']=from_addr
content['To']=', '.join(to_addr)
content['Cc']=', '.join(cc_addr)
# 添加郵件正文
if mode == 'html':
content.attach(MIMEText(body, 'html', 'utf-8'))
else:
content.attach(MIMEText(body, 'plain', 'utf-8'))
# 合併收件人和副本收件人
all_recipients=to_addr + cc_addr
status=smtp.sendmail(from_addr, all_recipients, content.as_string())
if status == {}:
print('郵件傳送成功!')
else:
print('郵件傳送失敗!')
smtp.quit()
# 使用範例
account='mygmail@gmail.com' # Gmail 帳號
password='azfkqbjbftodjucd' # Google 應用程式密碼 (這是樣本)
subject='郵件測試' # 主旨
from_addr='mygmail@gmail.com' # 寄件人
to_addr=['myhinet@ms5.hinet.net'] # 收件人
cc_addr=['tony@xxx.com.tw'] # 副本收件人(無設為 None)
body='<b><i>您好, 這是測試信<i><b>' # 信件內容
mode='html' # 內容模式 'plain'/'html'
send_mail(account, password, subject, from_addr, to_addr, cc_addr, body, mode)
此處信件內容為粗體斜體的中文字, 結果如下 :
如果將 mode 改成純文字 mode='html' 就沒有網頁效果了 :
4. 傳送 Text+HTML雙模格式信件 :
上面的 send_mail() 函式呼叫時要用 mode 參數決定內容格式為純文字 (plain) 或網頁 (html) 格式, 純文字格式可以在所有郵件客戶端顯示; 網頁格式較美觀, 但有些郵件軟體不支援, 這時可能會直接顯示原始 HTML 碼, 空白或純文字亂碼等.
解決辦法是採用 multipart/alternative 內容格式, 也就是同時傳送純文字內容 text_body 與網頁格式內容 html_body, 當收信端收到這種 multipart/alternative 格式信件時, 若支援 HTML 就會顯示網頁格式之內容 html_body; 反之則顯示純文字格式內容 text_body.
修改 send_mail() 函式加入 text_body 與 html_body 參數並取消 mode 參數 :
# smtp_mail_test_4.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, text_body='', html_body=''):
if cc_addr is None:
cc_addr=[] # 預設空串列
# 登入 Google SMTP 伺服器
smtp=smtplib.SMTP('smtp.gmail.com', 587)
smtp.ehlo()
smtp.starttls() # 啟動 TLS 安全傳輸
smtp.login(account, password) # 登入 Google SMTP 伺服器
# 建立郵件內容
content=MIMEMultipart()
content['Subject']=subject
content['From']=from_addr
content['To']=', '.join(to_addr)
content['Cc']=', '.join(cc_addr)
# 添加郵件正文 (雙模)
content.attach(MIMEText(text_body, 'plain', 'utf-8')) # 純文字
if html_body:
content.attach(MIMEText(html_body, 'html', 'utf-8')) # 網頁格式
# 合併收件人和副本收件人
all_recipients=to_addr + cc_addr
status=smtp.sendmail(from_addr, all_recipients, content.as_string())
if status == {}:
print('郵件傳送成功!')
else:
print('郵件傳送失敗!')
smtp.quit()
# 使用範例
account='mygmail@gmail.com' # Gmail 帳號
password='azfkqbjbftodjucd' # Google 應用程式密碼 (這是樣本)
subject='郵件測試' # 主旨
from_addr='mygmail@gmail.com' # 寄件人
to_addr=['myhinet@ms5.hinet.net'] # 收件人
cc_addr=['tony@xxx.com.tw'] # 副本收件人(無設為 None)
text_body='您好, 這是測試信' # 純文字信件內容
html_body='<b><i>您好, 這是測試信<i><b>' # 網頁格式信件內容
send_mail(account, password, subject, from_addr, to_addr, cc_addr, text_body, html_body)
結果如下 :
5. 傳送有附檔之信件 :
傳送附檔需要 open() 函式以二進位模式讀取附檔, 用 email.mime.application.MIMEApplication 類別傳給 MIMEApplication() 後加入信件本體中, 修改 send_mail() 函式添加 attachments 參數如下 :
# smtp_mail_test_5.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
import os
def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, text_body='', html_body=None, attachments=None):
if cc_addr is None:
cc_addr=[] # 預設空串列
if attachments is None: # 可變資料不宜做參數
attachments=[] # 應付迴圈需求
# 登入 Google SMTP 伺服器
smtp=smtplib.SMTP('smtp.gmail.com', 587)
smtp.ehlo()
smtp.starttls() # 啟動 TLS 安全傳輸
smtp.login(account, password) # 登入 Google SMTP 伺服器
# 建立郵件內容
content=MIMEMultipart()
content['Subject']=subject
content['From']=from_addr
content['To']=', '.join(to_addr)
content['Cc']=', '.join(cc_addr)
# 添加郵件正文 (雙模)
if not text_body and not html_body: # text_body 與 html_body 均未傳
text_body='(No message content)' # 預設內容
content.attach(MIMEText(text_body, 'plain', 'utf-8')) # 純文字
if html_body:
content.attach(MIMEText(html_body, 'html', 'utf-8')) # 網頁格式
# 加入附件
for filepath in attachments:
if os.path.isfile(filepath):
filename=os.path.basename(filepath)
with open(filepath, 'rb') as f:
part=MIMEApplication(f.read(), Name=filename)
part.add_header('Content-Disposition', 'attachment', filename=filename)
content.attach(part)
else:
print(f'找不到附件:{filepath}')
# 合併收件人和副本收件人
all_recipients=to_addr + cc_addr
status=smtp.sendmail(from_addr, all_recipients, content.as_string())
if status == {}:
print('郵件傳送成功!')
else:
print('郵件傳送失敗!')
smtp.quit()
# 使用範例
account='mygmail@gmail.com' # Gmail 帳號
password='azfkqbjbftodjucd' # Google 應用程式密碼 (這是樣本)
subject='郵件測試' # 主旨
from_addr='mygmail@gmail.com' # 寄件人
to_addr=['myhinet@ms5.hinet.net'] # 收件人
cc_addr=['tony@xxx.com.tw'] # 副本收件人(無設為 None)
text_body='您好, 這是測試信' # 純文字信件內容
html_body='<b><i>您好, 這是測試信<i><b>' # 網頁格式信件內容
attachments=['cat1.jpg', 'cat2.jpg'] # 附檔
send_mail(account, password, subject, from_addr, to_addr, cc_addr, text_body, html_body, attachments)
藍色字的是為了傳送附檔所增加的部分, 結果如下 :
6. 處理主旨的 Unicode 與 Text+HTML 雙模問題 :
在上面的測試中主旨含有中文 Unicode 都能順利傳送且在收信端正常顯示中文, 其實這些信件並不符合 RFC 標準格式 (因為 RFC 標頭只允許 ASCII 字元), 但因為目前多數的郵件客戶端會試圖去猜測編碼方式, 通常會自動用 UTF-8 來解釋非 ASCII 字元, 所以大都能正常顯示信件內容. 其實比較保險, 符合 RFC 規範的做法是用 email.header.Header 類別來處理主旨的 Unicode 問題, 只要將主旨內容傳給 Header() 建構式即可.
其次, 我將上面的函式提交給 AI 檢查, 它建議雙模應該採取兩層結構, 把 text/plain 和 text/html 包在 MIMEMultipart('alternative') 中, 再讓它作為內容的一部分附加到 MIMEMultipart('mixed') 外層, 讓彼此在 multipart/alternative 中互為備案 (fallback 機制), 這樣才算是符合 RFC 標準, 否則部分收信端可能只會讀第一個文字段落而忽略 HTML 部分.
# smtp_mail_test_6.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.header import Header
import os
def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, text_body='', html_body=None, attachments=None):
if cc_addr is None:
cc_addr=[] # 預設空串列
if attachments is None: # 可變資料不宜做參數
attachments=[] # 應付迴圈需求
# 登入 Google SMTP 伺服器
smtp=smtplib.SMTP('smtp.gmail.com', 587)
smtp.ehlo()
smtp.starttls() # 啟動 TLS 安全傳輸
smtp.login(account, password) # 登入 Google SMTP 伺服器
# 建立郵件內容
content=MIMEMultipart('mixed') # 外層 mixed
content['Subject']=Header(subject, 'utf-8') # 處理主旨 Unicode
content['From']=from_addr
content['To']=', '.join(to_addr)
content['Cc']=', '.join(cc_addr)
# 添加郵件正文 (雙模)
alt_part=MIMEMultipart('alternative') # 內層:alternative(純文字 + HTML)
alt_part.attach(MIMEText(text_body or '(No message content)', 'plain', 'utf-8'))
if html_body:
alt_part.attach(MIMEText(html_body, 'html', 'utf-8'))
content.attach(alt_part) # 將內層加入外層
# 加入附件
for filepath in attachments:
if os.path.isfile(filepath):
filename=os.path.basename(filepath)
with open(filepath, 'rb') as f:
part=MIMEApplication(f.read(), Name=filename)
part.add_header('Content-Disposition', 'attachment', filename=filename)
content.attach(part)
else:
print(f'找不到附件:{filepath}')
# 合併收件人和副本收件人
all_recipients=to_addr + cc_addr
status=smtp.sendmail(from_addr, all_recipients, content.as_string())
if status == {}:
print('郵件傳送成功!')
else:
print('郵件傳送失敗!')
smtp.quit()
# 使用範例
account='mygmail@gmail.com' # Gmail 帳號
password='azfkqbjbftodjucd' # Google 應用程式密碼 (這是樣本)
subject='郵件測試' # 主旨
from_addr='mygmail@gmail.com' # 寄件人
to_addr=['myhinet@ms5.hinet.net'] # 收件人
cc_addr=['tony@xxx.com.tw'] # 副本收件人(無設為 None)
text_body='您好, 這是測試信👋' # 純文字信件內容
html_body='<b><i>您好, 這是測試信<i><b>🌈' # 網頁格式信件內容
attachments=['cat1.jpg'] # 附檔
send_mail(account, password, subject, from_addr, to_addr, cc_addr, text_body, html_body, attachments)
黃底色為新增或修改的部分, 因為已經處理了 Unicode, 所以不論中文或 emoji 圖示均可傳送, 結果如下 :
7. 在 HTML 內容中內嵌圖片 :
上面範例是透過附件檔案方式傳送圖片, 使用夾帶附件的方式可用來傳送任何檔案, 但如果想看到附件內容須點擊開啟附件檔. 如果想在信件內容中直接顯示圖片, 則必須以 HTML 格式用 img 標籤內嵌圖片來傳送信件, 且須使用 email.mime.image,MIMEImage 類別來包裝 open() 讀取的圖片 byte 資料後加入信件標頭中, 將 send_mail() 函式改寫如下 :
# smtp_mail_test_7.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.header import Header
from email.mime.image import MIMEImage
import os
def send_mail(account,
password,
subject,
from_addr,
to_addr,
cc_addr=None,
text_body='',
html_body=None,
attachments=None,
inline_images=None):
if cc_addr is None:
cc_addr=[] # 預設空串列
if attachments is None: # 可變資料不宜做參數
attachments=[] # 應付迴圈需求
if inline_images is None: # 無內嵌圖片
inline_images={}
# 登入 Google SMTP 伺服器
smtp=smtplib.SMTP('smtp.gmail.com', 587)
smtp.ehlo()
smtp.starttls() # 啟動 TLS 安全傳輸
smtp.login(account, password) # 登入 Google SMTP 伺服器
# 建立郵件內容
content=MIMEMultipart('mixed') # 外層 mixed
content['Subject']=Header(subject, 'utf-8') # 處理主旨 Unicode
content['From']=from_addr
content['To']=', '.join(to_addr)
content['Cc']=', '.join(cc_addr)
# 添加郵件正文 (雙模)
alt_part=MIMEMultipart('alternative') # 內層:alternative(純文字 + HTML)
alt_part.attach(MIMEText(text_body or '(No message content)', 'plain', 'utf-8'))
# 若含有 inline 圖片使用 multipart/related 包住 HTML
if html_body:
related_part=MIMEMultipart('related')
related_part.attach(MIMEText(html_body, 'html', 'utf-8'))
for cid, img_path in inline_images.items():
if os.path.isfile(img_path):
with open(img_path, 'rb') as f:
img=MIMEImage(f.read())
img.add_header('Content-ID', f'<{cid}>')
img.add_header('Content-Disposition', 'inline', filename=os.path.basename(img_path))
related_part.attach(img)
else:
print(f'找不到內嵌圖片:{img_path}')
alt_part.attach(related_part) # 將 related 加入 alt
content.attach(alt_part) # 將 alt 加入信件內容
# 加入附件
for filepath in attachments:
if os.path.isfile(filepath):
filename=os.path.basename(filepath)
with open(filepath, 'rb') as f:
part=MIMEApplication(f.read(), Name=filename)
part.add_header('Content-Disposition', 'attachment', filename=filename)
content.attach(part)
else:
print(f'找不到附件:{filepath}')
# 合併收件人和副本收件人
all_recipients=to_addr + cc_addr
status=smtp.sendmail(from_addr, all_recipients, content.as_string())
if status == {}:
print('郵件傳送成功!')
else:
print('郵件傳送失敗!')
smtp.quit()
# 使用範例
account='mygmail@gmail.com' # Gmail 帳號
password='azfkqbjbftodjucd' # Google 應用程式密碼 (這是樣本)
subject='郵件測試' # 主旨
from_addr='mygmail@gmail.com' # 寄件人
to_addr=['myhinet@ms5.hinet.net'] # 收件人
cc_addr=['tony@xxx.com.tw'] # 副本收件人(無設為 None)
text_body='您好, 這是測試信👋' # 純文字信件內容
html_body='''
<html>
<body>
<h3>您好!</h3>
<p>這是內嵌圖片的測試:</p>
<img src="cid:cat1" width="300">
</body>
</html>
'''
attachments=None # 無附檔
inline_images={"cat1": "cat1.jpg"} # 內嵌圖片 (鍵與 img 之 src 對應)
send_mail(
account,
password,
subject,
from_addr,
to_addr,
cc_addr,
text_body,
html_body,
attachments,
inline_images
)
此處藍色與黃色為改寫或增加的部分, 結果如下 :
最後這一版 send_mail() 就是集大成的版本了 (Unicode+純文字網頁雙模+可傳附件檔+可內嵌圖片), 我們可以同時夾帶附件檔與內嵌圖片. 但如果信件內容只是英數字而已的話, 其實用最上面那個範例中的簡易版 send_mail() 即可.











沒有留言 :
張貼留言