电子邮件
邮件收发流程
-
假设我们自己的电子邮件地址是
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不会直接到达对方的电脑, 对方要取到邮件,必须通过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/pop3地址, 参考自如何开启outlook邮箱的pop3和smtp_常用邮箱客户端设置指南_weixin_39578674的博客-CSDN博客
-
需要邮箱授权码:使用第三方客户端登录时,需要在设置界面打开相应的服务,发送相应短信后,邮箱会生成授权码,在配置邮件客户端时,需要输入密码的地方输入该授权码即可。
# QQ邮箱 QQ邮箱的POP3/SMTP 和IMAP服务器地址如下(收件方式可以是POP3或者IMAP,发件方式为SMTP): POP3: pop.qq.com,使用SSL,端口号995 IMAP: imap.qq.com, 使用SSL,端口号993 SMTP: smtp.qq.com, 使用SSL,端口号465或587 账户名:您的QQ邮箱账户名(如果您是VIP帐号或Foxmail帐号,账户名需要填写完整的邮件地址) 密码:上一步生成的QQ邮箱授权码 电子邮件地址:您的QQ邮箱的完整邮件地址(与账户名相同) # 网易邮箱 网易邮箱有126.com,163.com和yeah.net三个域名,对应的POP3/SMTP 和IMAP服务器地址分别如下(收件方式可以是POP3或者IMAP,发件方式为SMTP): POP3: pop.126.com, pop.163.com ,pop.yeah.net, 使用SSL,端口号995 IMAP: imap.126.com,imap.163.com, imap.yeah.net, 使用SSL,端口号993 SMTP: smtp.126.com, smtp.163.com, smtp.yeah.net, 使用SSL,端口号465或587 账户名:您的网易邮箱账户名 密码:上一步生成的邮箱授权码 电子邮件地址:邮箱的完整邮件地址(与账户名相同)
-
outlook邮箱
微软Outlook邮箱的POP3/SMTP 和IMAP服务器地址如下(收件方式可以是POP3或者IMAP,发件方式为SMTP)需要注意,微软邮箱设置和上述邮箱的协议是有所区别的: POP3: outlook.office365.com,使用TLS,端口号995 IMAP: outlook.office365.com,使用TLS,端口号993 SMTP: smtp.office365.com ,使用 starttls(),端口号587 账户名:您的outlook邮箱账户名 密码:您的outlook邮箱密码 电子邮件地址:邮箱的完整邮件地址
-
SMTP发送邮件
Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。
模块:email
负责构造邮件,smtplib
负责发送邮件。
纯文本 text/plain
from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib
def _format_addr(s): # 用于格式化一个邮件地址, like James Bond <james.bond@movie.com>
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
from_addr = input('From: ') # 输入Email地址和口令
password = input('Password: ')
to_addr = input('To: ') # 输入收件人地址
smtp_server = input('SMTP server: ') # 输入SMTP服务器地址
"""
from_addr = "meifajia@outlook.com"
to_addr = "1819818024@qq.com"
smtp_server = "smtp.office365.com"
"""
msg = MIMEText('hello, test message from PY...', 'plain', 'utf-8')
msg['From'] = _format_addr('国粹 <%s>' % from_addr)
msg['To'] = _format_addr('ZSUS <%s>' % to_addr) # 接收的是字符串而不是list,如果有多个邮件地址,用,分隔即可。注意很多邮件服务商在显示邮件时,会把收件人名字自动替换为用户注册的名字
msg['Subject'] = Header('Testing,犇...', 'utf-8').encode()
server = smtplib.SMTP(smtp_server, 587) # SMTP协议默认端口是25,
server.starttls() # 创建SSL安全连接,加密SMTP
server.set_debuglevel(1) # 打印和SMTP server的交互信息
server.login(from_addr, password) # 登录SMTP server
server.sendmail(from_addr, [to_addr], msg.as_string()) # 可以发送给多人所以用list, as_string将对象变成str
server.quit()
text/html
msg内容变更为
html = '<html><body><h1>hello Guys</h1><p>5rd test is sent by <a href="https://www.baidu.com">baidu</a>...</p><p><img src="cid:0"></p></body></html>'
msg = MIMEText(html, 'html', 'utf-8')
添加附件
带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个MIMEMultipart
对象代表邮件本身,然后往里面加上一个MIMEText
作为邮件正文,再继续往里面加上表示附件的MIMEBase
对象即可。
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
...
# 邮件对象:
msg = MIMEMultipart()
msg['From'] = _format_addr('国粹 <%s>' % from_addr)
msg['To'] = _format_addr('ZSUS <%s>' % to_addr)
msg['Subject'] = Header('Testing,犇...', '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中通过引用src="cid:0"
就可以把附件作为图片嵌入了。如果有多个图片,给它们依次编号,然后引用不同的cid:x
即可。
把上面代码加入MIMEMultipart
的MIMEText
从plain
改为html
,然后在适当的位置引用图片
# cid:0 是指向前面附件中的header信息('Content-ID', '<0>')
html = '<html><body><h1>hello Guys</h1><p> test is sent by <a href="https://www.baidu.com">baidu</a>...</p><p><img src="cid:0"></p></body></html>'
msg.attach(MIMEText(html, 'html', 'utf-8'))
同时支持HTML和Plain格式
如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。
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对象...
Summary
构造一个邮件对象就是一个Messag
对象,如果构造一个MIMEText
对象,就表示一个文本邮件对象,如果构造一个MIMEImage
对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipart
对象,而MIMEBase
可以表示任何对象。它们的继承关系如下:
Message
+- MIMEBase
+- MIMEMultipart
+- MIMENonMultipart
+- MIMEMessage
+- MIMEText
+- MIMEImage
这种嵌套关系就可以构造出任意复杂的邮件。你可以通过email.mime文档查看它们所在的包以及详细的用法。
POP3收邮件
收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取,Python内置一个poplib
模块,实现了POP3协议,可以直接用来收邮件。
收取邮件分两步:
第一步:用poplib
把邮件的原始文本下载到本地;
第二部:用email
解析原始文本,还原为message
对象, 然后用适当的形式把内容展示出来。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"POP3 service"
import poplib
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 Server: ')
"""
email = "meifajia@outlook.com"
password = "xxxx"
pop3_server = "outlook.office365.com"
"""
server = poplib.POP3_SSL(pop3_server, 995) # 启用SSL模式,要去邮件客户端设置开启允许第三方客户端的POP3协议
server.set_debuglevel(1)
print(server.getwelcome().decode('utf-8')) # 打印sever欢迎语
# 身份验证
server.user(email)
server.pass_(password)
# stat()返回邮件数量和占用空间:
print('Messages: %s. Size: %s' % server.stat())
# list()返回所有邮件的编号,注意索引号从1开始:
resp, mails, octets = server.list()
# 可以查看返回的列表类似[b'1 82923', b'2 2184', ...]
# 使用retr获取最新一封邮件:
index = len(mails)
resp, lines, octets = server.retr(index)
# lines存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件的message对象:
msg=Parser().parsestr(msg_content)
# 可以根据邮件索引号直接从服务器删除邮件,此功能需在邮件服务商POP3中开启:
#server.dele(index)
# 关闭连接:
server.quit()
# decode Subject或者地址名中编译过的str
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
# 邮件内容的str也需要decode,还需要检测编码,非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
# 递归打印msg对象, 根据缩进排列内容
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()): # 检测邮件中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('%sAttachement: %s' % (' ' * indent, content_type))
# 处理message对象
print_info(msg)