- 假設我們自己的電子郵件地址是from@163.com,對方的電子郵件地址是to@sina.com(這里的地址虛擬的),現在我們用Outlook或者Foxmail之類的軟件寫好郵件,填上對方的Email地址,點“發送”,電子郵件就發出去了。這些電子郵件軟件被稱為MUA:Mail User Agent——郵件用戶代理。
- Email從MUA發出去,不是直接到達對方電腦,而是發到MTA:Mail Transfer Agent——郵件傳輸代理,就是那些Email服務提供商,比如網易、新浪等等。由於我們自己的電子郵件是163.com,所以,Email首先被投遞到網易提供的MTA,再由網易的MTA發到對方服務商,也就是新浪的MTA。這個過程中間可能還會經過別的MTA。
- Email到達新浪的MTA后,由於對方使用的是@sina.com的郵箱,因此,新浪的MTA會把Email投遞到郵件的最終目的地MDA:Mail Delivery Agent——郵件投遞代理。Email到達MDA后,就會保存在新浪的某個服務器上,存放在某個文件或特殊的數據庫里,我們將這個長期保存郵件的地方稱之為電子郵箱。對方要取到郵件,必須通過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是發送郵件的協議,Python內置對SMTP的支持,可以發送純文本郵件、HTML郵件以及帶附件的郵件。Python對SMTP支持有smtplib和email兩個模塊,email負責構造郵件,smtplib負責發送郵件。
首先我們先構造純文本的郵件:(網易郵箱 —>qq郵箱)
- 參數1:郵件正文(hello,world)
- 參數2:MIME的subtype,傳入‘plain’,最終的MIME就是’text/plain’
- 參數3:代表編碼
發送普通郵件
# 輸入Email地址和口令: from_addr = input('From: ') #這里的密碼一定是授權碼,163郵箱原始密碼不行。 password = input('Password: ') # 輸入SMTP服務器地址:這里我們用smtp.163.com smtp_server = input('SMTP server: ') # 輸入收件人地址: to_addr = input('To: ') #用來格式化郵件地址 from email.header import Header from email.utils import parseaddr, formataddr 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)) #設置發件人,收件人姓名和郵件主題 msg['From'] = _format_addr(u'發件的人A <%s>' % from_addr) msg['To'] = _format_addr(u'收件的人B <%s>' % to_addr) msg['Subject'] = Header(u'測試郵件……', 'utf-8').encode() import smtplib server = smtplib.SMTP(smtp_server, 25) # SMTP協議默認端口是25 server.set_debuglevel(1)#打印出和SMTP服務器交互的所有信息 server.login(from_addr, password)#登錄服務器 #發送郵件,這里第二個參數是個列表,可以有多個收件人 #郵件正文是一個str,as_string()把MIMEText對象變成str server.sendmail(from_addr, [to_addr], msg.as_string()) server.quit()
發送HTML郵件
#只需要修改這一行代碼,把正文換成html格式文本,plain換成html,其他不變 msg = MIMEText('<html><body><h1>Hello</h1>' + '<p>send by <a href="http://blog.csdn.net/csdn15698845876">csdn</a>...</p>' + '</body></html>', 'html', 'utf-8')

#-*-encoding:utf-8 -*- from email.mime.text import MIMEText from email.header import Header from email.utils import parseaddr, formataddr import smtplib from_addr = raw_input('From: ') password = raw_input('Password: ') smtp_server = raw_input('SMTP server: ') to_addr = raw_input('To: ') 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)) msg = MIMEText('<html><body><h1>Hello</h1>' + '<p>send by <a href="http://blog.csdn.net/csdn15698845876">csdn</a>...</p>' + '</body></html>', 'html', 'utf-8') msg['From'] = _format_addr(u'張康 <%s>' % from_addr) msg['To'] = _format_addr(u'朋友 <%s>' % to_addr) msg['Subject'] = Header(u'HTML郵件', '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()
發送帶附件的郵件
帶附件的郵件可以看做包含若干部分的郵件:文本和各個附件本身,所以,可以構造一個MIMEMultipart對象代表郵件本身,然后往里面加上一個MIMEText作為郵件正文,再繼續往里面加上表示附件的MIMEBase對象即可。
#-*-encoding:utf-8 -*- from email import encoders from email.header import Header from email.mime.text import MIMEText from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.utils import parseaddr, formataddr import smtplib from_addr = raw_input('From: ') password = raw_input('Password: ') to_addr = raw_input('To: ') smtp_server = raw_input('SMTP server: ') # 郵件對象: 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)) msg = MIMEMultipart() msg['From'] = _format_addr(u'發件的人A <%s>' % from_addr) msg['To'] = _format_addr(u'收件的人B <%s>' % to_addr) msg['Subject'] = Header(u'帶附件的郵件', 'utf-8').encode() # 郵件正文是MIMEText: msg.attach(MIMEText('send with file...', 'plain', 'utf-8')) # 添加附件就是加上一個MIMEBase,從本地讀取一個圖片: with open('1.jpg', 'rb') as f: # 設置附件的MIME和文件名,這里是jpg類型: mime = MIMEBase('image', 'jpg', filename='1.jpg') # 加上必要的頭信息: mime.add_header('Content-Disposition', 'attachment', filename='1.jpg') 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) 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()
示例:從QQ郵箱發送到網易郵箱
由於從qq郵箱到網易郵箱需要SSL協議,所以代碼有一點變化,而且QQ郵箱需要開啟IMAP/SMTP服務,登錄密碼需要使用授權碼。
開啟IMAP/SMTP服務服務流程:qq郵箱——設置——賬戶
這里已經開啟了,沒有開啟的點擊開啟,然后按照流程去操作會得到一個授權碼。

# -*- encoding: utf-8 -*- from email.header import Header from email.mime.text import MIMEText from email.utils import parseaddr, formataddr import smtplib from_addr=raw_input('From:') password=raw_input('Password:') to_addr=raw_input('To:') smtp_server=raw_input('SMTP server:')#這里是smtp.qq.com 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)) msg=MIMEText('hello,send by qq mail','plain','utf-8') msg['From'] = _format_addr(u'qq郵箱 <%s>' % from_addr) msg['To'] = _format_addr(u'網易郵箱 <%s>' % to_addr) msg['Subject'] = Header(u'發件人A……', 'utf-8').encode() server=smtplib.SMTP_SSL(smtp_server, 465)#SSL協議端口465 server.set_debuglevel(1) server.login(from_addr,password) server.sendmail(from_addr,[to_addr],msg.as_string()) server.quit()
收取郵件
收取郵件就是編寫一個MUA作為客戶端,從MDA把郵件獲取到用戶的電腦或者手機上。收取郵件最常用的協議是POP協議,目前版本號是3,俗稱POP3。Python內置一個poplib模塊,實現了POP3協議,可以直接用來收郵件。
POP3協議收取的不是一個已經可以閱讀的郵件本身,而是郵件的原始文本,這和SMTP協議很像,SMTP發送的也是經過編碼后的一大段文本。
要把POP3收取的文本變成可以閱讀的郵件,還需要用email模塊提供的各種類來解析原始文本,變成可閱讀的郵件對象。
所以,收取郵件分兩步:
- 用poplib把郵件的原始文本下載到本地;
- 用email解析原始文本,還原為郵件對象。
通過POP3下載郵件

import poplib # 輸入郵件地址, 口令和POP3服務器地址: email = raw_input('Email: ') password = raw_input('Password: ') pop3_server = raw_input('POP3 server: ') # 連接到POP3服務器:例如pop.163.com 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是個亂的,還沒有真正的解析 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) """ 但是這個Message對象本身可能是一個MIMEMultipart對象,即包含嵌套的其他MIMEBase對象,嵌套可能還不止一層。所以我們要遞歸地打印出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中包含的名字都是經過編碼后的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): # 先從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
上述過程完整代碼如下:

# -*- coding: utf-8 -*- import poplib import email from email.parser import Parser from email.header import decode_header from email.utils import parseaddr 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 def decode_str(s): value, charset = decode_header(s)[0] if charset: value = value.decode(charset) return value 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)) email = raw_input('Email: ') password = raw_input('Password: ') pop3_server = raw_input('POP3 server: ') server = poplib.POP3(pop3_server) #server.set_debuglevel(1) print(server.getwelcome()) # 認證: server.user(email) server.pass_(password) print('Messages: %s. Size: %s' % server.stat()) resp, mails, octets = server.list() # 獲取最新一封郵件, 注意索引號從1開始: resp, lines, octets = server.retr(len(mails)) # 解析郵件: msg = Parser().parsestr('\r\n'.join(lines)) # 打印郵件內容: print_info(msg) # 慎重:將直接從服務器刪除郵件: # server.dele(len(mails)) # 關閉連接: server.quit()