python之email

如果现在我们要给朋友发一个邮件,假设我们自己的电子邮件地址是[email protected],对方的电子邮件地址是[email protected](注意地址都是虚构的哈),现在我们用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后,就静静地躺在新浪的某个服务器上,存放在某个文件或特殊的数据库里,我们将这个长期保存邮件的地方称之为电子邮箱。

同普通邮件类似,Email不会直接到达对方的电脑,因为对方电脑不一定开机,开机也不一定联网。对方要取到邮件,必须通过MUA从MDA上把邮件取到自己的电脑上。

所以,一封电子邮件的旅程就是:

发件人 -> MUA -> MTA -> MTA -> 若干个MTA -> MDA <- MUA <- 收件人

有了上述的基本概念,要编写程序来发送和接收邮件,本质上就是:

1.编写MUA把邮件发到MTA,编写MUA从MDA上收邮件。

发邮件的时候,MUA和MTA使用的协议就是SMTP:Simple Mail Transfer Protocol,后面的MTA到另一个MTA也是用SMTP协议。

收邮件的时候,MUA和MDA使用的协议有两种:POP: Post Office Protocol,目前版本是3。俗称POP3; IMAP: IInternet Message Access Protocol,目前版本是4,不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱等等。

邮件客户端软件在发送邮件时,会让你先配置SMTP服务器,也就是你要发送到哪个MTA上。如果你是163邮箱,你就得填163提供的SMTP服务器地址:smtp.163.com,为了证明你是163的用户,SMTP服务器还会要求你填写邮箱地址和邮箱口令。这样,MUA才能把email通过SMTP协议发送到MTA.

类似的,从MDA收邮件的时候,MDA服务器也要求验证你的邮箱口令,确保不会有人冒充你。

SMTP:SMTP是发送邮件的协议,python内置对SMTP的支持,可以发送纯文本邮件,HTML邮件以及带附件的邮件。

Python对SMTP的支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件。

#-*-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

#编写一个函数_format_addr()来格式化一个邮件地址
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:')

#注意到构造一个MIMEText对象时,第一个参数是邮件的正文,第二个参数是MIME的subtype.
#subtype传入'plian',最终的MIME就是'text/plain',utf-8编码保证多语言的兼容性
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)  #SMTP协议的默认端口是25
server.set_debuglevel(1)   #打印一下和SMTP服务器交互的所有信息
server.login(from_addr,password)  #登录SMTP服务器
#发送邮件,可以群发(所以to_addr为一个list,邮件正文为一个str,所以用as_string把MIMEText对象变成str)
server.sendmail(from_addr, [to_addr], msg.as_string())  
server.quit()
如果我们要发送HTML邮件,而不是普通的纯文本文件怎么办??方法很简单,在构造MIMEText对象时,把HTML字符串传进去,再把第二个参数由plain变成html就可以了。

msg = MIMEText('

Hello

' + '

send by Python...

' + '', 'html', 'utf-8')
如果想在email中加上附件呢?带附件的邮件可以看做包含若干个部分的邮件:文本和各个附件本身。
所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase对象即可:

#-*-coding:utf-8-*-
from email.mime.text import MIMEText
from email import  encoders
from email.utils import parseaddr,formataddr
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
import smtplib

#编写一个函数_format_addr()来格式化一个邮件地址
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
    ))

f_addr = raw_input('from:')
passwd = raw_input('password:')
t_addr = raw_input('to:')
smtp_server = raw_input('smtp server:')
#邮件对象是MIMEMultipart
msg = MIMEMultipart()
msg['From'] = _format_addr(u'陈敏华bigTom <%s>' % f_addr)
msg['To'] = _format_addr(u'管理员 <%s>' % t_addr)
msg['Subject'] = Header(u'试试加个附件哈哈','utf-8').encode()
#邮件正文是MIMEText
msg.attach(MIMEText('

Hello

' + '

' + #把附件中的图片嵌入正文中 '', 'html', 'utf-8')) #添加附件 with open(u'H:/workspace/python/src/我要学python/email/carter.jpg','rb') as f: #设置附件的MIME和文件名 mime = MIMEBase('image','jpg',filename='carter.jpg') #加上必要的头信息 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) server = smtplib.SMTP(smtp_server,25) server.login(f_addr,passwd) server.sendmail(f_addr,t_addr,msg.as_string()) server.quit()

同时支持HTML和Plain格式

如果我们发送HTML邮件,收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的,但是,如果收件人使用的设备太古老,查看不了HTML邮件怎么办?

办法是在发送HTML的同时再附加一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。

利用MIMEMultipart就可以组合一个HTML和Plain,要注意指定subtype是alternative

msg = MIMEMultipart('alternative')
msg['From'] = ...
msg['To'] = ...
msg['Subject'] = ...

msg.attach(MIMEText('hello', 'plain', 'utf-8'))
msg.attach(MIMEText('

Hello

', 'html', 'utf-8')) # 正常发送msg对象...

加密SMTP

使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全地发送邮件,可以加密SMTP会话,实际上就是先创建SSL安全连接,然后再使用SMTP协议发送邮件。

某些邮件服务商,例如Gmail,提供的SMTP服务必须要加密传输。我们来看看如何通过Gmail提供的安全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()方法,就创建了安全连接。后面的代码和前面的发送邮件代码完全一样。

如果因为网络问题无法连接Gmail的SMTP服务器,请相信我们的代码是没有问题的,你需要对你的网络设置做必要的调整。

小结

使用Python的smtplib发送邮件十分简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头,就可以顺利发出。

构造一个邮件对象就是一个Messag对象,如果构造一个MIMEText对象,就表示一个文本邮件对象,如果构造一个MIMEImage对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipart对象,而MIMEBase可以表示任何对象。它们的继承关系如下:

Message
+- MIMEBase
   +- MIMEMultipart
   +- MIMENonMultipart
      +- MIMEMessage
      +- MIMEText
      +- MIMEImage

这种嵌套关系就可以构造出任意复杂的邮件。你可以通过email.mime文档查看它们所在的包以及详细的用法。


POP收邮件:

SMTP用于发送邮件,那如果要收取邮件呢?

收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。最常用的协议是POP3

python内置了一个poplib模块,实现了POP3协议。

注意:POP3收取的文本不是一个可读的邮件本身,要把收取的文本变成可阅读的邮件,还需要用email模块提供的各种类来解析原始文本。

所有,收取邮件分为两步:

一,用poplib把邮件的原始文本下载到本地。

二,用email解析原始文本,还原邮件对象。

我们来试一试:

#-*-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: ')
#连接到POP3服务器
server = poplib.POP3(pop3_server)
#server.set_debuglevel(1)
print(server.getwelcome())
# 认证:
server.user(email)
server.pass_(password)
#stat返回邮件数量和占用空间
print('Messages: %s. Size: %s' % server.stat())
#list返回所有邮件的编号
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()

解析邮件

解析邮件的过程和上一节构造邮件正好相反,因此,先导入必要的模块:

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,因为像CcBcc这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。上面的代码我们偷了个懒,只取了第一个元素。

文本邮件的内容也是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学习笔记)