Day-18: 電子郵件

  假設要從**@163.com發送郵件到**@sina.com,會通過下面幾個過程:html

  • 首先,你得使用郵件代理軟件(也就是MUA:Mail User Agent),例如Outlook,Foxmail。填寫你的Email地址和密碼,發送郵件。
  • Email從MUA發出後會到達163的服務器,也就是MTA:Mail Transfer Agent。以後再由網易的服務商傳到新浪的服務商MTA,中間可能還會通過其餘幾個MTA。
  • Email到達新浪的MTA後,會被投遞到最終目的地MDA:Mail Delivery Agent——郵件投遞代理,並長期保存在這個電子郵箱中。
  • 最後,得收件人經過MUA將郵件獲取獲得。

  在發郵件時,MUA和MTA之間的協議是SMTP:Simple Mail Transfer Protocol。python

  收郵件時,MUA和MDA使用的協議有POP:Post Office Protocol,版本號爲3,俗稱POP3;另外一種是,IMAP: Internet Message Protocol,它的優勢是不斷能夠取郵件,還能夠直接操做MDA上存儲的郵件,好比從收件箱一道垃圾箱。瀏覽器

  在Python中,收發郵件,只要作到如下兩點:安全

  1. 編寫MUA把郵件發到MTA;
  2. 編寫MUA從MDA上收郵件。
  • STMP發送郵件

  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() # 結束

  可是,效果不是很理想:編碼

  1. 郵件沒主題;
  2. 沒有收件人的名字。

  這是由於顯示主題、收件人、發件人等信息是記錄在所發的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收取文件

  使用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入門整理後的筆記

相關文章
相關標籤/搜索