Python中使用SMTP发送邮件以及POP收取邮件
- 假设我们自己的电子邮件地址是from@163.com,对方的电子邮件地址是to@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后,就会保存在新浪的某个服务器上,存放在某个文件或特殊的数据库里,我们将这个长期保存邮件的地方称之为电子邮箱。对方要取到邮件,必须通过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是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。Python对SMTP支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件。
首先我们先构造纯文本的邮件:(网易邮箱 —>qq邮箱)
- 参数1:邮件正文(hello,world)
- 参数2:MIME的subtype,传入‘plain’,最终的MIME就是’text/plain’
- 参数3:代表编码
发送普通邮件
# 输入Email地址和口令: from_addr = input('From: ') #这里的密码一定是授权码,163邮箱原始密码不行。 password = input('Password: ') # 输入SMTP服务器地址:这里我们用smtp.163.com smtp_server = input('SMTP server: ') # 输入收件人地址: to_addr = input('To: ') #用来格式化邮件地址 from email.header import Header from email.utils import parseaddr, formataddr 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)) #设置发件人,收件人姓名和邮件主题 msg['From'] = _format_addr(u'发件的人A <%s>' % from_addr) msg['To'] = _format_addr(u'收件的人B <%s>' % to_addr) msg['Subject'] = Header(u'测试邮件……', 'utf-8').encode() import smtplib server = smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25 server.set_debuglevel(1)#打印出和SMTP服务器交互的所有信息 server.login(from_addr, password)#登录服务器 #发送邮件,这里第二个参数是个列表,可以有多个收件人 #邮件正文是一个str,as_string()把MIMEText对象变成str server.sendmail(from_addr, [to_addr], msg.as_string()) server.quit()
发送HTML邮件
#只需要修改这一行代码,把正文换成html格式文本,plain换成html,其他不变 msg = MIMEText('<html><body><h1>Hello</h1>' + '<p>send by <a href="http://blog.csdn.net/csdn15698845876">csdn</a>...</p>' + '</body></html>', 'html', 'utf-8')
#-*-encoding:utf-8 -*- from email.mime.text import MIMEText from email.header import Header from email.utils import parseaddr, formataddr import smtplib from_addr = raw_input('From: ') password = raw_input('Password: ') smtp_server = raw_input('SMTP server: ') to_addr = raw_input('To: ') 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)) msg = MIMEText('<html><body><h1>Hello</h1>' + '<p>send by <a href="http://blog.csdn.net/csdn15698845876">csdn</a>...</p>' + '</body></html>', 'html', 'utf-8') msg['From'] = _format_addr(u'张康 <%s>' % from_addr) msg['To'] = _format_addr(u'朋友 <%s>' % to_addr) msg['Subject'] = Header(u'HTML邮件', '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()
发送带附件的邮件
带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase对象即可。
#-*-encoding:utf-8 -*- from email import encoders from email.header import Header from email.mime.text import MIMEText from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.utils import parseaddr, formataddr import smtplib from_addr = raw_input('From: ') password = raw_input('Password: ') to_addr = raw_input('To: ') smtp_server = raw_input('SMTP server: ') # 邮件对象: 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)) msg = MIMEMultipart() msg['From'] = _format_addr(u'发件的人A <%s>' % from_addr) msg['To'] = _format_addr(u'收件的人B <%s>' % to_addr) msg['Subject'] = Header(u'带附件的邮件', 'utf-8').encode() # 邮件正文是MIMEText: msg.attach(MIMEText('send with file...', 'plain', 'utf-8')) # 添加附件就是加上一个MIMEBase,从本地读取一个图片: with open('1.jpg', 'rb') as f: # 设置附件的MIME和文件名,这里是jpg类型: mime = MIMEBase('image', 'jpg', filename='1.jpg') # 加上必要的头信息: mime.add_header('Content-Disposition', 'attachment', filename='1.jpg') 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.set_debuglevel(1) server.login(from_addr, password) server.sendmail(from_addr, [to_addr], msg.as_string()) server.quit()
示例:从QQ邮箱发送到网易邮箱
由于从qq邮箱到网易邮箱需要SSL协议,所以代码有一点变化,而且QQ邮箱需要开启IMAP/SMTP服务,登录密码需要使用授权码。
开启IMAP/SMTP服务服务流程:qq邮箱——设置——账户
这里已经开启了,没有开启的点击开启,然后按照流程去操作会得到一个授权码。
# -*- encoding: utf-8 -*- from email.header import Header from email.mime.text import MIMEText from email.utils import parseaddr, formataddr import smtplib from_addr=raw_input('From:') password=raw_input('Password:') to_addr=raw_input('To:') smtp_server=raw_input('SMTP server:')#这里是smtp.qq.com 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)) msg=MIMEText('hello,send by qq mail','plain','utf-8') msg['From'] = _format_addr(u'qq邮箱 <%s>' % from_addr) msg['To'] = _format_addr(u'网易邮箱 <%s>' % to_addr) msg['Subject'] = Header(u'发件人A……', 'utf-8').encode() server=smtplib.SMTP_SSL(smtp_server, 465)#SSL协议端口465 server.set_debuglevel(1) server.login(from_addr,password) server.sendmail(from_addr,[to_addr],msg.as_string()) server.quit()
收取邮件
收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是POP协议,目前版本号是3,俗称POP3。Python内置一个poplib模块,实现了POP3协议,可以直接用来收邮件。
POP3协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。
要把POP3收取的文本变成可以阅读的邮件,还需要用email模块提供的各种类来解析原始文本,变成可阅读的邮件对象。
所以,收取邮件分两步:
- 用poplib把邮件的原始文本下载到本地;
- 用email解析原始文本,还原为邮件对象。
通过POP3下载邮件
import poplib # 输入邮件地址, 口令和POP3服务器地址: email = raw_input('Email: ') password = raw_input('Password: ') pop3_server = raw_input('POP3 server: ') # 连接到POP3服务器:例如pop.163.com 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是个乱的,还没有真正的解析 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) """ 但是这个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
上述过程完整代码如下:
# -*- 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: ') server = poplib.POP3(pop3_server) #server.set_debuglevel(1) print(server.getwelcome()) # 认证: server.user(email) server.pass_(password) print('Messages: %s. Size: %s' % server.stat()) 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()