Python 電子郵件


  從一台計算機編寫郵件到對方收到郵件。假設我們自己的電子郵件地址是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的文本中的,所以,我們必須把FromToSubject添加到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,郵件正文是一個stras_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,因為像CcBcc這樣的字段可能包含多個郵件地址,所以解析出來的會有多個元素。上面的代碼我們偷了個懶,只取了第一個元素。

  文本郵件的內容也是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

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM