從一台計算機編寫郵件到對方收到郵件。假設我們自己的電子郵件地址是me@163.com
,對方的電子郵件地址是friend@sina.com
我們在本地的軟件上寫好郵件,點擊發送,郵件就發送出去了,這些電子郵件被稱為MUA(mail user agent)郵件用戶代理
郵件從本地被發送出去后,並不會被直接送到用戶的電腦上,而是被發送到MTA:Mail Transfer Agent 瀏覽器傳輸代理,就是那些Email服務提供商,比如網易、新浪等等,由於我們自己的電子郵件是163.com,所以郵件回被最先送到網易提供的MTA,再由網易的MTA發送到對方的MTA,即新浪的MTA,這中間可能還會經過其他服務商的MTA
當郵件被送到對方郵箱的服務提供商的MTA,因此新浪的MTA會把郵件投遞到郵件的最終目的地:MDA :Mail Delivery Agent 郵件投遞代理。Email到達MDA后,就靜靜地躺在新浪的某個服務器上,存放在某個文件或特殊的數據庫里,我們將這個長期保存郵件的地方稱之為電子郵箱。
Email不會直接到達對方的電腦,因為對方電腦不一定開機,開機也不一定聯網。對方要取到郵件,必須通過MUA從MDA上把郵件取到自己的電腦上。
所以,一封電子郵件的旅程就是:
發件人 -> MUA -> MTA -> MTA -> 若干個MTA -> MDA <- MUA <- 收件人
所以要編寫程序來發送和接受郵件,最基本的兩件事
編寫MUA將郵件送到MTA,編寫MUA從MDA上取郵件
協議
發郵件時,MUA和MTA使用的協議就是SMTP:Simple Mail Transfer Protocol,后面的MTA到另一個MTA也是用SMTP協議。
收郵件時,MUA和MDA使用的協議有兩種:POP:Post Office Protocol,目前版本是3,俗稱POP3;IMAP:Internet Message Access Protocol,目前版本是4,優點是不但能取郵件,還可以直接操作MDA上存儲的郵件,比如從收件箱移到垃圾箱,等等。
郵件客戶端軟件在發郵件時,會讓你先配置SMTP服務器,也就是你要發到哪個MTA上。假設你正在使用163的郵箱,你就不能直接發到新浪的MTA上,因為它只服務新浪的用戶,所以,你得填163提供的SMTP服務器地址:smtp.163.com
,為了證明你是163的用戶,SMTP服務器還要求你填寫郵箱地址和郵箱口令,這樣,MUA才能正常地把Email通過SMTP協議發送到MTA。
類似的,從MDA收郵件時,MDA服務器也要求驗證你的郵箱口令,確保不會有人冒充你收取你的郵件,所以,Outlook之類的郵件客戶端會要求你填寫POP3或IMAP服務器地址、郵箱地址和口令,這樣,MUA才能順利地通過POP或IMAP協議從MDA取到郵件。
最后特別注意,目前大多數郵件服務商都需要手動打開SMTP發信和POP收信的功能,否則只允許在網頁登錄。否則我們就不能在自己的程序上登陸
使用SMTP發送郵件
參考鏈接:https://www.liaoxuefeng.com/wiki/1016959663602400/1017790702398272#0
SMTP是發送郵件的協議,可以發送純文本郵件、HTML郵件以及帶附件的郵件。Python對SMTP的支持有兩個模塊:smtplib和email。email
負責構造郵件,smtplib
負責發送郵件。構造和發送是一起的
構造一個郵件對象就是一個Messag
對象,如果構造一個MIMEText
對象,就表示一個文本郵件對象,如果構造一個MIMEImage
對象,就表示一個作為附件的圖片,要把多個對象組合起來,就用MIMEMultipart
對象,而MIMEBase
可以表示任何對象。它們的繼承關系如下:
Message +- MIMEBase +- MIMEMultipart +- MIMENonMultipart +- MIMEMessage +- MIMEText +- MIMEImage
發送文本
#構造 from email.mime.text import MIMEText msg = MIMEText('hello, send by Python...', 'plain', 'utf-8') #發送 from_addr='gao__king@163.com' smtp_server='smtp.163.com' to_addr='1903843477@qq.com' password=input('enter password:') #這是因為郵件主題、如何顯示發件人、收件人等信息並不是通過SMTP協議發給MTA,而是包含在發給MTA的文本中的,所以,我們必須把From
、To
和Subject
添加到MIMEText
中 msg['Subject']='hello world'#編輯郵件主題 msg['From']=from_addr#發信人 msg['To']=to_addr#收信人 server=smtplib.SMTP(smtp_server,25) server.set_debuglevel(1) server.login(from_addr,password)login()
方法用來登錄SMTP服務器,為了驗證身份,不向所有人提供服務,所以必須提供用戶名和口令 server.sendmail(from_addr,[to_addr],msg.as_string()) server.quit()
注意到構造MIMEText
對象時,第一個參數就是郵件正文,第二個參數是MIME的subtype,傳入'plain'
表示純文本,最終的MIME就是'text/plain'
,最后一定要用utf-8
編碼保證多語言兼容性。
我們用set_debuglevel(1)
就可以打印出和SMTP服務器交互的所有信息
sendmail()
方法就是發郵件,由於可以一次發給多個人,所以傳入一個list
,郵件正文是一個str
,as_string()
把MIMEText
對象變成str
。
作者還介紹了通過header對象來編碼郵件頭
遇到的問題:
smtplib.SMTPAuthenticationError: (535, b’Error: authentication failed’):將發送郵箱開啟POP3/SMTP服務,這時163會讓我們設置客戶端授權碼,把登錄的密碼改成郵箱所設置的授權碼即可。
smtplib.SMTPDataError: (554, b’DT:SPM 163 smtp12…):分兩種情況 a、發送的郵件內容存在test或測試,刪掉即可;b、代碼中缺少msg[‘From’]和msg[‘To’],加上即可。
腳本運行未報錯,但是在接收者郵箱內(一般在垃圾站中)未收到郵件,這時需要查看發送郵箱服務器的端口號有沒有寫上。
發送HTML
在構造MIMEText
對象時,=第二個參數是MIME的subtype,傳入'plain'
表示純文本,‘html’表示HTML
html=r''' <h1>hello</h1> <img src='cid:0'> ''' msg_main_html=MIMEText(html,'html','utf-8')
發送附件
帶附件的郵件可以看做包含若干部分的郵件:文本和各個附件本身,所以,可以構造一個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)
發送圖片
如果要把一個圖片嵌入到郵件正文中怎么做?直接在HTML郵件中鏈接圖片地址行不行?答案是,大部分郵件服務商都會自動屏蔽帶有外鏈的圖片,因為不知道這些鏈接是否指向惡意網站。
要把圖片嵌入到郵件正文中,我們只需按照發送附件的方式,先把郵件作為附件添加進去,然后,在HTML中通過引用src="cid:0"
就可以把附件作為圖片嵌入了。如果有多個圖片,給它們依次編號,然后引用不同的cid:x
即可。
msg.attach(MIMEText('<html><body><h1>Hello</h1>' + '<p><img src="cid:0"></p>' + '</body></html>', 'html', 'utf-8'))
同時支持HTML和Plain格式
並不是說,只能發送文本和只能發送網頁,如果收件人使用的設備太古老,查看不了HTML郵件怎么辦?
辦法是在發送HTML的同時再附加一個純文本,如果收件人無法查看HTML格式的郵件,就可以自動降級查看純文本郵件。
利用MIMEMultipart
就可以組合一個HTML和Plain,要注意指定subtype是alternative
:
text=r'''hello,good''' html=r''' <h1>hello</h1> <img src='cid:0'> ''' msg=MIMEMultipart() msg_main=MIMEText(text,'plain','utf-8') msg_main_html=MIMEText(html,'html','utf-8') msg.attach(msg_main) msg.attach(msg_main_html)
加密SMTP
使用標准的25端口連接SMTP服務器時,使用的是明文傳輸,發送郵件的整個過程可能會被竊聽。要更安全地發送郵件,可以加密SMTP會話,實際上就是先創建SSL安全連接,然后再使用SMTP協議發送郵件。
收取郵件
收取郵件就是編寫一個MUA作為客戶端,從MDA把郵件獲取到用戶的手機或者電腦上。
收取郵件最常用的協議是POP目前的版本是3,俗稱pop3
Python內置了一個實現了pop3協議的模塊poplib,可以用來收取郵件
但直接使用pop協議收取的郵件並不是一個直接閱讀的文本,這點和SMTP發送郵件類似,通過SMTP發送的協議也是一個經過編碼的文本,所以經過pop協議收取的郵件,還需要通過Python的email模塊提供的各種類來解析原始文本,變成可閱讀的文本郵件。
所以,收取郵件分為兩步,第一步是通過POP協議把郵件下載到本地,第二部是通過email模塊把原始文本解析為郵件對象
下載郵件
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()
解析郵件
還原郵件的過程和構造郵件對象的過程剛好相反,先導入模塊
from email.parser import Parser from email.header import decode_header from email.utils import parseaddr
先把郵件解析為Message對象
msg=Parser().parserstr(msg_content)
但是這個Message
對象本身可能是一個MIMEMultipart
對象,即包含嵌套的其他MIMEBase
對象,嵌套可能還不止一層。
我們要打印出他的層次結構,讓人能看的明白
# 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、from等都是通過編碼后的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