layout: post
title: 電子郵件系列(二)
subtitle: SMTP發(fā)送郵件
date: 2018-03-03
author: Simon
header-img: img/post-bg-git-learn.jpg
catalog: true
tags:
- MIME
- 電子郵件
- SMTP
郵件發(fā)送與接收的流程
SMTP是發(fā)送郵件的協(xié)議,是MUA(Mail User Agent)發(fā)送到MTA(Mail Transfer Agent)和MTA之間傳輸?shù)膮f(xié)議。
編寫程序來發(fā)送和接收郵件,本質(zhì)上就是以下這樣的過程:
發(fā)件人->MUA->MTA->若干MTA->MDA->收件人
其中,你是新浪的用戶,就會(huì)先發(fā)到新浪的MTA上,經(jīng)過若干MTA,會(huì)到達(dá)收件人的郵件服務(wù)商的MTA上,這個(gè)MTA會(huì)把郵件投遞到最終目的地MDA(Mail Delivery Agent)郵件投遞代理,Email到達(dá)MDA后,會(huì)靜靜地躺在郵件服務(wù)商的服務(wù)器上,等待用戶開機(jī)聯(lián)網(wǎng)通過MUA從MDA把郵件取到本地。
幾種郵件類型
純文本郵件/HTML文本郵件
發(fā)送純文本郵件:
先構(gòu)造郵件消息(用到email庫):
構(gòu)造一個(gè)最簡單的純文本郵件:
from email.mime.text import MIMEText
msg = MIMEText('send by simon...', 'plain', 'utf-8')
構(gòu)造MIMEText對(duì)象時(shí),第一個(gè)參數(shù)就是郵件正文,第二個(gè)是子格式,plain表示傳入的是純文本,另一個(gè)值是html。
charset值為utf-8,保證多語言兼容性。此值也將被添加到Content-Type頭中去。
此外,若charset值設(shè)定,則Content-Transfer-Encoding會(huì)被設(shè)定為對(duì)應(yīng)值,如utf-8對(duì)應(yīng)base64。關(guān)于Content-Transfer-Encoding內(nèi)容在上一篇電子郵件系列(一)中已經(jīng)詳細(xì)介紹過。
當(dāng)我們print這個(gè)msg會(huì)發(fā)現(xiàn)其信件內(nèi)容(不含header)已經(jīng)是base64格式了。所以,當(dāng)指定了charset后,會(huì)指定Content-Type-Encoding,信件內(nèi)容相應(yīng)的由字符編碼為bytes,其本質(zhì)是二進(jìn)制,之后再將二進(jìn)制轉(zhuǎn)化為base64。
接下來,發(fā)送郵件(用到smtplib庫):
import smtplib
from_addr = '1005121394@qq.com'
to_addr = 'simonkindle@126.com'
password = 'zotggqxvhhktbfig'
smtp_server = 'smtp.qq.com'
server_port = 465
server = smtplib.SMTP_SSL(smtp_server, server_port)
server.set_debuglevel(1) #打印出所有和SMTP服務(wù)器交互的信息
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
對(duì)于QQ郵箱來說,其要求SMTP發(fā)送郵件時(shí)必須使用SSL加密,所以,使用SSL加密端口465,正常情況下不要求SSL加密的使用25端口,同時(shí)創(chuàng)建smtp對(duì)象時(shí)使用的是smtp.SMTP函數(shù)。
sendmail的第二個(gè)參數(shù)to_addrs接收的是一個(gè)list,可以傳入多個(gè)收件人地址。第三個(gè)是郵件內(nèi)容,as_astring函數(shù)會(huì)將格式化的message對(duì)象(包含MIMEHeader以及內(nèi)容)全部轉(zhuǎn)化為str,這樣才能在網(wǎng)絡(luò)上傳輸(當(dāng)然,網(wǎng)絡(luò)上傳輸?shù)氖沁@些str的字節(jié)流bytes格式,這一過程未在代碼中體現(xiàn),個(gè)人猜測應(yīng)該在sendmail中完成。)
發(fā)送HTML格式郵件:
只需要在構(gòu)建message時(shí)將傳入的文本格式改為html,并在傳入html格式的文本即可。例如:
msg = MIMEText('<html><body><h1>Hello</h1>' +
'<p>send by <a +
'</body></html>', 'html', 'utf-8')
以上郵件我們還沒有為其添加郵件頭,也就是主題,發(fā)件人,收件人,這會(huì)造成一些問題,比如會(huì)提示不在收件人中,或收件人無
[圖片上傳失敗...(image-df844d-1520222326027)]
[圖片上傳失敗...(image-f7aa6e-1520222326027)]
尤其是,當(dāng)你用網(wǎng)易郵箱發(fā)郵件時(shí),會(huì)提示錯(cuò)誤,原因就是你的sendmail函數(shù)中的發(fā)收件人與郵件頭中的不匹配。
添加郵件頭:
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()
我們定義了一個(gè)_format_addr格式化郵件地址,不能直接傳入name <addr@example.com>,因?yàn)閚ame如果包含中文,需要對(duì)name進(jìn)行編碼。
但是,subject經(jīng)測試是可以直接傳入中文字符的。
同時(shí)支持HTML和plain格式的文本郵件
如果我們發(fā)送HTML郵件,收件人通過瀏覽器或者Outlook之類的軟件是可以正常瀏覽郵件內(nèi)容的,但是,如果收件人使用的設(shè)備太古老,查看不了HTML郵件怎么辦?
可以在發(fā)送HTML文本的同時(shí)再附加一個(gè)純文本,如果收件人的設(shè)備不支持查看HTML格式郵件,可以顯示純文本(兩個(gè)只能顯示一個(gè))
我們可以創(chuàng)建一個(gè)MIMEMultipart類型對(duì)象,subtype子類型是'alternative',在上一篇文章中我們已經(jīng)講過,這代表純文本和HTML文本混合的內(nèi)容。
創(chuàng)建之后再向其中添加兩個(gè)MIMTText對(duì)象即可。
代碼如下:
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'))
發(fā)送附件
如何發(fā)送帶附件比如像圖片或mp3這種二進(jìn)制文件的郵件呢?
同樣,我們可以先構(gòu)造一個(gè)MIMEMultipart對(duì)象,向其中添加MIMEText對(duì)象作為郵件正文,再向其中添加MIMEBase作為附件即可。
msg = MIMEMultipart()
msg.attach(MIMEText('你好', 'plain', 'utf-8'))
# 添加附件就是添加一個(gè)MIMEBase對(duì)象,注意文件要以二進(jìn)制形式打開。
with open('F://my pictures//beauty//222.JPG', 'rb') as f:
mime = MIMEBase('image', 'jpeg', filename='222.JPG')
# 添加文件頭,此filename會(huì)顯示在郵件附件中
mime.add_header('Content-Disposition', 'attachment', filename='222.JPG')
# 定義圖片ID,以便在文本中引用
mime.add_header('Content-ID', '<0>')
# 讀入附件內(nèi)容
mime.set_payload(f.read())
# Encode the message's payload in Base64. 并添加Content-Transfer-Encoding頭信息
encoders.encode_base64(mime)
msg.attach(mime)
將圖片嵌入郵件正文中
如何把圖片附件嵌入郵件正文中(郵件服務(wù)商一般不允許使用HTML圖片外鏈,防止有害信息):
要把圖片嵌入到郵件正文中,我們只需按照發(fā)送附件的方式,先把郵件作為附件添加進(jìn)去,然后,在HTML中通過引用src="cid:0"就可以把附件作為圖片嵌入了。如果有多個(gè)圖片,給它們依次編號(hào),然后引用不同的cid:x即可。
msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
'<p><img src="cid:0"></p>' +
'</body></html>', 'html', 'utf-8'))
再次發(fā)送,即可在正文中看到圖片了。
加密郵件
使用標(biāo)準(zhǔn)的25端口連接SMTP服務(wù)器時(shí),使用的是明文傳輸,發(fā)送郵件的整個(gè)過程可能會(huì)被竊聽。要更安全地發(fā)送郵件,可以加密SMTP會(huì)話,實(shí)際上就是先創(chuàng)建SSL安全連接,然后再使用SMTP協(xié)議發(fā)送郵件
比如騰訊qq郵箱要求SMTP服務(wù)必須要SSL加密。端口號(hào)是465。
我們上文所用的smtp.SMTP_SSL(server_addr, server_port)就是加密方式
非加密方式使用的函數(shù)是smtp.SMTP(server_addr, 25)。
總結(jié)
對(duì)于非ASCII編碼的純文本,由輸入的字符到網(wǎng)絡(luò)傳輸這一過程中的編碼變化為:
str->bytes->base64->str->bytes
其中,前三步由MIMEText函數(shù)完成,base64->str由as_string函數(shù)完成。最后一步不在代碼中體現(xiàn)。
對(duì)于二進(jìn)制文件來說:
binary code->base64->str->bytes
其中,binary code由open函數(shù)以rb模式讀入,二進(jìn)制轉(zhuǎn)到base64由encoders.encode_base64函數(shù)完成,之后與純文本相同,由as_string完成message格式到str的轉(zhuǎn)化。