python之email

来源:互联网 发布:红旗linux证书 编辑:程序博客网 时间:2024/05/28 06:06

如果现在我们要给朋友发一个邮件,假设我们自己的电子邮件地址是me@163.com,对方的电子邮件地址是friend@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后,就静静地躺在新浪的某个服务器上,存放在某个文件或特殊的数据库里,我们将这个长期保存邮件的地方称之为电子邮箱。

同普通邮件类似,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 encodersfrom email.header import Headerfrom email.mime.text import MIMETextfrom email.utils import parseaddr, formataddrimport 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协议的默认端口是25server.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('<html><body><h1>Hello</h1>' +    '<p>send by <a href="http://www.python.org">Python</a>...</p>' +    '</body></html>', 'html', 'utf-8')
如果想在email中加上附件呢?带附件的邮件可以看做包含若干个部分的邮件:文本和各个附件本身。
所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase对象即可:

#-*-coding:utf-8-*-from email.mime.text import MIMETextfrom email import  encodersfrom email.utils import parseaddr,formataddrfrom email.header import Headerfrom email.mime.base import MIMEBasefrom email.mime.multipart import MIMEMultipartimport 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:')#邮件对象是MIMEMultipartmsg = 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()#邮件正文是MIMETextmsg.attach(MIMEText('<html><body><h1>Hello</h1>' +    '<p><img src="cid:0"></p>' +                       #把附件中的图片嵌入正文中    '</body></html>', '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('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))# 正常发送msg对象...

加密SMTP

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

某些邮件服务商,例如Gmail,提供的SMTP服务必须要加密传输。我们来看看如何通过Gmail提供的安全SMTP发送邮件。

必须知道,Gmail的SMTP端口是587,因此,修改代码如下:

smtp_server = 'smtp.gmail.com'smtp_port = 587server = 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 poplibimport emailfrom email.parser import Parserfrom email.header import decode_headerfrom email.utils import parseaddrdef 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 charsetdef decode_str(s):    value, charset = decode_header(s)[0]    if charset:        value = value.decode(charset)    return valuedef 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 emailfrom email.parser import Parserfrom email.header import decode_headerfrom 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


0 0