假設要從**@163.com發送郵件到**@sina.com,會通過下面幾個過程:html
在發郵件時,MUA和MTA之間的協議是SMTP:Simple Mail Transfer Protocol。python
收郵件時,MUA和MDA使用的協議有POP:Post Office Protocol,版本號爲3,俗稱POP3;另外一種是,IMAP: Internet Message Protocol,它的優勢是不斷能夠取郵件,還能夠直接操做MDA上存儲的郵件,好比從收件箱一道垃圾箱。瀏覽器
在Python中,收發郵件,只要作到如下兩點:安全
SMTP是發送郵件的協議,Python中stmplib和email兩個模塊是對STMP支持的,其中,email負責構造郵件,stmplib負責發送郵件。服務器
首先,得了解構造的郵件對象就是Message對象,而MIMEText對象,就是一個文本郵件對象,若是構造一個MIMEImage對象,就是表示一個做爲附件的圖片,若是要把多個對象組合起來,就用MIMEMultipart對象,而MIMEBase能夠表示任何對象,它們的繼承關係以下:app
Message +- MIMEBase +- MIMEMultipart +- MIMENonMultipart +- MIMEMessage +- MIMEText +- MIMEImage
若是咱們要構造純文本郵件,以下:函數
from email.mime.text import MIMEText msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
構造MIMEText對象時,第一個參數時郵件正文,第二個參數時MIME的subtype,傳入「plain」表示純文本,傳入「html」就表示HTML格式,最終的MIME就是‘text/plain’,最後必定要用utf-8編碼來保證多國語言兼容。學習
郵件構建完成後,經過SMTP發送ui
# 輸入Email地址和口令: from_addr = raw_input('From: ') password = raw_input('Password: ') # 輸入SMTP服務器地址: smtp_server = raw_input('SMTP server: ') # 輸入收件人地址: to_addr = raw_input('To: ') import smtplib server = smtplib.SMTP(smtp_server, 25) # SMTP協議默認端口是25 server.set_debuglevel(1) # 打印出和STMP服務器交互的全部信息 server.login(from_addr, password) # 登陸STMP服務器 server.sendmail(from_addr, [to_addr], msg.as_string()) # 發送郵件 server.quit() # 結束
可是,效果不是很理想:編碼
這是由於顯示主題、收件人、發件人等信息是記錄在所發的message中的,STMP發送所傳入的參數只是保證登陸服務器和肯定目標地址。因此,必須把from、To、subject等信息添加到MIMEText中,才能完整顯示。
# -*- coding: utf-8 -*- 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.encode('utf-8') if isinstance(addr, unicode) else addr)) from_addr = raw_input('From: ') password = raw_input('Password: ') to_addr = raw_input('To: ') smtp_server = raw_input('SMTP server: ') msg = MIMEText('hello, send by Python...', 'plain', 'utf-8') msg['From'] = _format_addr(u'Python愛好者 <%s>' % from_addr) msg['To'] = _format_addr(u'管理員 <%s>' % to_addr) msg['Subject'] = Header(u'來自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和後面備註的郵箱地址。前者因爲可能涉及到中文,因此用Header對象進行編碼(編碼後用於傳輸的文本是包含了utf-8和Base64的文本),注意最後傳出的name和addr都得是utf-8.另外msg['To']接受的不是list而是字符串,因此若是有多個郵件地址傳入,就得用,分隔。
改進後,效果更人性化:
若是要發送HTML郵件,只用將前面構造MIMEText對象時,把HTML字符串傳進去,再把第二個參數有plain換成html就ok了。
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文檔,若是要發送附件,就不能只建立MIMEText對象了,還有還有附件的對象,可是繼承樹種沒有專門的附件對象,用能夠代替任何郵件對象的MIMEBase對象代替。
# 郵件對象: msg = MIMEMultipart() msg['From'] = _format_addr(u'Python愛好者 <%s>' % from_addr) msg['To'] = _format_addr(u'管理員 <%s>' % to_addr) msg['Subject'] = Header(u'來自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)
先建立MIMEMultipart做爲要發送的郵件對象msg,再將要顯示主題,收件人,發件人信息加入到msg中,至於正文和附件就另外建立MIMEText對象和MIMEBase對象分別儲存好內容後再添加到msg中。
若是要將圖片嵌入到正文中,就只能發送html文件,再上述發送附件的基礎上將圖片的連接地址傳入到html文件中的相應位置。
msg.attach(MIMEText('<html><body><h1>Hello</h1>' + '<p><img src="cid:0"></p>' + '</body></html>', 'html', 'utf-8'))
效果以下:
若是咱們發送HTML郵件,而收件人使用的是比較老的MUA,沒法顯示HTML文件,只能顯示純文本文件,那就出問題了。因此,爲了確保不管老舊設備都能顯示,會發送純文本和HTML兩種郵件格式,同時將subtype換成alternative,若是沒法查看HTML,會自動降級成純文本。
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對象...
另外,使用標準的25端口鏈接SMTP服務器時,使用的是明文傳輸,發送郵件的整個過程有被竊聽的風險。要更安全的發送郵件,都是先建立SSL安全鏈接,而後再使用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()方法,就建立了安全鏈接,其餘的都是同樣的。
最後,關於如何構造複雜的郵件內容能夠參考email官方包
使用POP3收取郵件分爲兩步:
第一步:用poplib把郵件的原始文本下載到本地;
第二步:用email解析原始文本,還原爲郵件對象。
將郵件下載到本地:
import poplib # 輸入郵件地址, 口令和POP3服務器地址: email = raw_input('Email: ') password = raw_input('Password: ') pop3_server = raw_input('POP3 server: ') # 鏈接到POP3服務器: server = poplib.POP3(pop3_server) # 能夠打開或關閉調試信息: # server.set_debuglevel(1) # 可選:打印POP3服務器的歡迎文字: print(server.getwelcome()) # 身份認證: server.user(email) server.pass_(password) # stat()返回郵件數量和佔用空間: print('Messages: %s. Size: %s' % server.stat()) # list()返回全部郵件的編號: resp, mails, octets = server.list() # 能夠查看返回的列表相似['1 82923', '2 2184', ...] print(mails) # 獲取最新一封郵件, 注意索引號從1開始: index = len(mails) resp, lines, octets = server.retr(index) # lines存儲了郵件的原始文本的每一行, # 能夠得到整個郵件的原始文本: msg_content = '\r\n'.join(lines) # 稍後解析出郵件: msg = Parser().parsestr(msg_content) # 能夠根據郵件索引號直接從服務器刪除郵件: # server.dele(index) # 關閉鏈接: server.quit()
解析郵件:
先得導入必要的模塊:
import email from email.parser import Parser from email.header import decode_header from email.utils import parseaddr
而後,將郵件內容解析爲Message對象:
msg = Parser().parsestr(msg_content)
若是解析的對象自己是個MIMEMultipart對象,就要遞歸地打印出Message對象的層次結構:
# indent用於縮進顯示: def print_info(msg, indent=0): if indent == 0: # 郵件的From, To, Subject存在於根對象上: for header in ['From', 'To', 'Subject']: value = msg.get(header, '') if value: if header=='Subject': # 須要解碼Subject字符串: value = decode_str(value) else: # 須要解碼Email地址: 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()): # 若是郵件對象是一個MIMEMultipart, # get_payload()返回list,包含全部的子對象: parts = msg.get_payload() for n, part in enumerate(parts): print('%spart %s' % (' ' * indent, n)) print('%s--------------------' % (' ' * indent)) # 遞歸打印每個子對象: print_info(part, indent + 1) else: # 郵件對象不是一個MIMEMultipart, # 就根據content_type判斷: content_type = msg.get_content_type() if content_type=='text/plain' or content_type=='text/html': # 純文本或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和Email中包含的name都是通過編碼後的str,要正常顯示,就必須decode:
def decode_str(s): value, charset = decode_header(s)[0] if charset: value = value.decode(charset) return value
decode_header()返回一個list,可能包含多個郵件地址,可是這裏只取了第一個郵件地址。
另外,郵件的正文內容也是str,還須要檢測編碼,不然,非UTF-8編碼的郵件沒法正常顯示:
def guess_charset(msg): # 先從msg對象獲取編碼: charset = msg.get_charset() if charset is None: # 若是獲取不到,再從Content-Type字段獲取: content_type = msg.get('Content-Type', '').lower() pos = content_type.find('charset=') if pos >= 0: charset = content_type[pos + 8:].strip() return charset
最後,在瀏覽器上和Python編寫的POP3程序地結果:
+OK Welcome to coremail Mail Pop3 Server (163coms[...]) Messages: 126. Size: 27228317 From: Test <xxxxxx@qq.com> To: Python愛好者 <xxxxxx@163.com> Subject: 用POP3收取郵件 part 0 -------------------- part 0 -------------------- Text: Python可使用POP3收取郵件……... part 1 -------------------- Text: Python能夠<a href="...">使用POP3</a>收取郵件……... part 1 -------------------- Attachment: application/octet-stream
注:本文爲學習廖雪峯Python入門整理後的筆記