如果现在我们要给朋友发一个邮件,假设我们自己的电子邮件地址是[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中加上附件呢?带附件的邮件可以看做包含若干个部分的邮件:文本和各个附件本身。
#-*-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邮件,收件人通过浏览器或者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对象...
使用标准的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,因为像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