- SMTP 是发送邮件的协议,Python 内置对 SMTP 的支持,可以发送纯文本邮件、HTML 邮件以及带附件的邮件。
- Python 对 SMTP 支持有
smtplib
和email
两个模块,email
负责构造邮件,smtplib
负责发送邮件。
0、发送邮件时使用到的密码/秘钥的获取方式
(1)qq 邮箱
- 在该页面中往下拉,找到如下所示的界面,随后点击“生成授权码”即可根据操作生成 password:
1、发送纯文本邮件
import smtplib
from email.mime.text import MIMEText
# jy: 构造 MIMEText 对象时, 第一个参数就是邮件正文, 第二个参数是 MIME 的 subtype,
# 传入 'plain' 表示纯文本, 最终的 MIME 就是 'text/plain', 最后一定要用 utf-8
# 编码保证多语言兼容性;
msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
# jy: 发送方 Email 地址
from_addr = "929277335@qq.com"
# jy: 发送方 Email 地址口令(类似 Password, 如 qq 邮箱需在邮箱设置中获取)')
password = "xxxxxxxxxxxxxx"
# jy: 收件人邮箱地址
to_addr = "121123953@qq.com"
# jy: SMTP 服务器地址(SMTP server), 不同邮箱对应的 SMTP server 不同;
# 可百度搜索查找(如谷歌邮箱对应的为: smtp.gmail.com), 以下为 qq 的;
smtp_server = 'smtp.qq.com'
# jy: SMTP 协议默认端口是 25; 经测试, 使用 25 或 587 均可;
#server = smtplib.SMTP(smtp_server, 587)
#server = smtplib.SMTP(smtp_server, 25)
server = smtplib.SMTP(smtp_server)
# jy: 用 set_debuglevel(1) 可以打印出和 SMTP 服务器交互的所有信息; SMTP 协议就是简单的
# 文本命令和响应;
server.set_debuglevel(1)
# jy: login() 方法用来登录 SMTP 服务器
server.login(from_addr, password)
# jy: sendmail() 方法就是发邮件, 由于可以一次发给多个人, 所以传入一个 list, 邮件正文是
# 一个 str, as_string() 把 MIMEText 对象变成 str;
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
- 邮件没有主题
- 收件人的名字没有显示为友好的名字,比如Mr Green <green@example.com>
- 明明收到了邮件,却提示不在收件人中(以上没有这种情况)
- 这是因为邮件主题、如何显示发件人、收件人等信息并不是通过 SMTP 协议发给 MTA,而是包含在发给 MTA 的文本中的,所以必须把 From、To 和 Subject 添加到 MIMEText 中,才是一封完整的邮件:
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):
"""
格式化一个邮件地址;
注意: 不能简单地传入 "name <addr@example.com>",因为如果包含中文, 需要通过 Header
对象进行编码;
"""
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# jy: 发送方 Email 地址
from_addr = "929277335@qq.com"
# jy: 发送方 Email 地址口令(类似 Password, 如 qq 邮箱需在邮箱设置中获取)')
password = "xxxxxxxx"
# jy: 收件人邮箱地址
to_addr = "121123953@qq.com"
# jy: SMTP 服务器地址(SMTP server), 不同邮箱对应的 SMTP server 不同;
# 可百度搜索查找(如谷歌邮箱对应的为: smtp.gmail.com), 以下为 qq 的;
smtp_server = 'smtp.qq.com'
msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
msg['From'] = _format_addr('user_test <%s>' % from_addr)
# jy: msg['To'] 接收的是字符串而不是 list, 如果有多个邮件地址, 用 "," 分隔即可;
msg['To'] = _format_addr('administrator <%s>' % to_addr)
msg['Subject'] = Header('该邮件来自 python 代码发送', '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()
- 收件人的名字很可能不是我们传入的 administrator,因为很多邮件服务商在显示邮件时,会把收件人名字自动替换为用户注册的名字,但是其他收件人名字的显示不受影响。
- 如果查看 Email 的原始内容,可以看到如下经过编码的邮件头(经过 Header 对象编码的文本,包含utf-8 编码信息和 Base64 编码的文本。如果手动构造这样的编码文本,显然比较复杂):
From: =?utf-8?b?UHl0aG9u54ix5aW96ICF?= <xxxxxx@163.com>
To: =?utf-8?b?566h55CG5ZGY?= <xxxxxx@qq.com>
Subject: =?utf-8?b?5p2l6IeqU01UUOeahOmXruWAmeKApuKApg==?=
2、发送 HTML 邮件
- 如果要发送 HTML 邮件,则在构造 MIMEText 对象时,把 HTML 字符串传进去,再把第二个参数由
plain
变为html
:
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):
"""
格式化一个邮件地址;
注意: 不能简单地传入 "name <addr@example.com>",因为如果包含中文, 需要通过 Header
对象进行编码;
"""
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# jy: 发送方 Email 地址
from_addr = "929277335@qq.com"
# jy: 发送方 Email 地址口令(类似 Password, 如 qq 邮箱需在邮箱设置中获取)')
password = "xxxxxxxx"
# jy: 收件人邮箱地址
to_addr = "121123953@qq.com"
# jy: SMTP 服务器地址(SMTP server), 不同邮箱对应的 SMTP server 不同;
# 可百度搜索查找(如谷歌邮箱对应的为: smtp.gmail.com), 以下为 qq 的;
smtp_server = 'smtp.qq.com'
msg = MIMEText('<html><body><h1>Hello</h1>' +
'<p>send by <a href="https://www.yuque.com/it-coach/python">Python</a>...</p>' +
'</body></html>', 'html', 'utf-8')
msg['From'] = _format_addr('user_test <%s>' % from_addr)
# jy: msg['To'] 接收的是字符串而不是 list, 如果有多个邮件地址, 用 "," 分隔即可;
msg['To'] = _format_addr('administrator <%s>' % to_addr)
msg['Subject'] = Header('该邮件来自 python 代码发送', '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()
3、邮件附带附件
- 带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个
MIMEMultipart
对象代表邮件本身,然后往里面加上一个MIMEText
作为邮件正文,再继续往里面加上表示附件的MIMEBase
对象即可:
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import parseaddr, formataddr
import smtplib
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# jy: 发送方 Email 地址
from_addr = "929277335@qq.com"
# jy: 发送方 Email 地址口令(类似 Password, 如 qq 邮箱需在邮箱设置中获取)')
password = "xxxxxxxx"
# jy: 收件人邮箱地址
to_addr = "121123953@qq.com"
# jy: SMTP 服务器地址(SMTP server), 不同邮箱对应的 SMTP server 不同;
# 可百度搜索查找(如谷歌邮箱对应的为: smtp.gmail.com), 以下为 qq 的;
smtp_server = 'smtp.qq.com'
# 邮件对象
msg = MIMEMultipart()
msg['From'] = _format_addr('user_test <%s>' % from_addr)
msg['To'] = _format_addr('administrator <%s>' % to_addr)
msg['Subject'] = Header('该邮件来自 python 代码发送', 'utf-8').encode()
# 邮件正文是 MIMEText
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
# 添加附件就是加上一个 MIMEBase, 从本地读取一个图片:
with open('./test.jpg', '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)
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()
4、将图片嵌入到邮件正文
- 如果要把一个图片嵌入到邮件正文中怎么做?直接在 HTML 邮件中链接图片地址行不行?
- 不行,大部分邮件服务商都会自动屏蔽带有外链的图片,因为不知道这些链接是否指向恶意网站。
- 要把图片嵌入到邮件正文中,只需按照发送附件的方式,先把邮件作为附件添加进去,然后在 HTML 中通过引用
src="cid:0"
就可以把附件作为图片嵌入了。如果有多个图片,给它们依次编号,然后引用不同的cid:x
即可。 - 把上面代码加入
MIMEMultipart
的MIMEText
从plain
改为html
,然后在适当的位置引用图片:
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import parseaddr, formataddr
import smtplib
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# jy: 发送方 Email 地址
from_addr = "929277335@qq.com"
# jy: 发送方 Email 地址口令(类似 Password, 如 qq 邮箱需在邮箱设置中获取)')
password = "xxxxxxxx"
# jy: 收件人邮箱地址
to_addr = "121123953@qq.com"
# jy: SMTP 服务器地址(SMTP server), 不同邮箱对应的 SMTP server 不同;
# 可百度搜索查找(如谷歌邮箱对应的为: smtp.gmail.com), 以下为 qq 的;
smtp_server = 'smtp.qq.com'
# 邮件对象
msg = MIMEMultipart()
msg['From'] = _format_addr('user_test <%s>' % from_addr)
msg['To'] = _format_addr('administrator <%s>' % to_addr)
msg['Subject'] = Header('该邮件来自 python 代码发送', 'utf-8').encode()
# 邮件正文是 MIMEText
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
'<p><img src="cid:0"></p>' +
'</body></html>', 'html', 'utf-8'))
# 添加附件就是加上一个 MIMEBase, 从本地读取一个图片:
with open('./test.jpg', '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)
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()
5、同时支持 HTML 和 Plain 格式
- 如果我们发送 HTML 邮件,收件人通过浏览器或者 Outlook 之类的软件是可以正常浏览邮件内容的,但是,如果收件人使用的设备太古老,查看不了 HTML 邮件怎么办?
- 办法是在发送HTML的同时再附加一个纯文本,如果收件人无法查看 HTML 格式的邮件,就可以自动降级查看纯文本邮件。
- 利用
MIMEMultipart
就可以组合一个 HTML 和 Plain,要注意指定subtype
是alternative
:
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import parseaddr, formataddr
import smtplib
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# jy: 发送方 Email 地址
from_addr = "929277335@qq.com"
# jy: 发送方 Email 地址口令(类似 Password, 如 qq 邮箱需在邮箱设置中获取)')
password = "xxxxxxxx"
# jy: 收件人邮箱地址
to_addr = "121123953@qq.com"
# jy: SMTP 服务器地址(SMTP server), 不同邮箱对应的 SMTP server 不同;
# 可百度搜索查找(如谷歌邮箱对应的为: smtp.gmail.com), 以下为 qq 的;
smtp_server = 'smtp.qq.com'
# 邮件对象
msg = MIMEMultipart('alternative')
msg['From'] = _format_addr('user_test <%s>' % from_addr)
msg['To'] = _format_addr('administrator <%s>' % to_addr)
msg['Subject'] = Header('该邮件来自 python 代码发送', 'utf-8').encode()
# 邮件正文是 MIMEText
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
'<p><img src="cid:0"></p>' +
'</body></html>', 'html', 'utf-8'))
# 添加附件就是加上一个 MIMEBase, 从本地读取一个图片:
with open('./test.jpg', '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)
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()
6、加密 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 服务器,则需要对你的网络设置做必要的调整。
7、小结
- 使用 Python 的smtplib发送邮件十分简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头,就可以顺利发出。
- 构造一个邮件对象就是一个 Messag 对象,如果构造一个 MIMEText 对象,就表示一个文本邮件对象,如果构造一个 MIMEImage 对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipart 对象,而 MIMEBase 可以表示任何对象。它们的继承关系:
Message
+- MIMEBase
+- MIMEMultipart
+- MIMENonMultipart
+- MIMEMessage
+- MIMEText
+- MIMEImage
- 这种嵌套关系就可以构造出任意复杂的邮件。详细参考
email.mime
文档:
8、POP3 收取邮件
- SMTP 用于发送邮件,如果要收取邮件呢?
- 收取邮件就是编写一个 MUA 作为客户端,从 MDA 把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是 POP 协议,目前版本号是 3,俗称 POP3。
- Python 内置一个
poplib
模块,实现了 POP3 协议,可以直接用来收邮件。 - 注意:POP3 协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和 SMTP 协议很像,SMTP 发送的也是经过编码后的一大段文本。
- 要把 POP3 收取的文本变成可以阅读的邮件,还需要用
email
模块提供的各种类来解析原始文本,变成可阅读的邮件对象。 - 所以,收取邮件分两步:
- 第一步:用
poplib
把邮件的原始文本下载到本地 - 第二部:用
email
解析原始文本,还原为邮件对象
(1)通过 POP3 下载邮件
import poplib
# 输入邮件地址, 口令和POP3服务器地址:
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 server: ')
# 连接到 POP3 服务器:
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息:
server.set_debuglevel(1)
# 可选: 打印 POP3 服务器的欢迎文字:
print(server.getwelcome().decode('utf-8'))
# 身份认证:
server.user(email)
server.pass_(password)
# stat() 返回邮件数量和占用空间:
print('Messages: %s. Size: %s' % server.stat())
# list() 返回所有邮件的编号:
resp, mails, octets = server.list()
# 可以查看返回的列表类似 [b'1 82923', b'2 2184', ...]
print(mails)
# 获取最新一封邮件, 注意索引号从 1 开始:
index = len(mails)
resp, lines, octets = server.retr(index)
# lines 存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件:
msg = Parser().parsestr(msg_content)
# 可以根据邮件索引号直接从服务器删除邮件:
# server.dele(index)
# 关闭连接:
server.quit()
- 用 POP3 获取邮件其实很简单,要获取所有邮件,只需要循环使用
retr()
把每一封邮件内容拿到即可。真正麻烦的是把邮件的原始内容解析为可以阅读的邮件对象。
(2)解析邮件
- 解析邮件的过程和上一节构造邮件正好相反,因此,先导入必要的模块:
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import poplib
- 只需要一行代码就可以把邮件内容解析为
Message
对象:
msg = Parser().parsestr(msg_content)
- 但是这个
Message
对象本身可能是一个MIMEMultipart
对象,即包含嵌套的其他MIMEBase
对象,嵌套可能还不止一层。 - 所以需要递归地打印出
Message
对象的层次结构:
# indent 用于缩进显示:
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))
- 邮件的 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):
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
- 把上面的代码整理好,就可以来试试收取一封邮件。先往自己的邮箱发一封邮件,然后用浏览器登录邮箱,看看邮件收到没,如果收到了,我们就来用 Python 程序把它收到本地:
+OK Welcome to coremail Mail Pop3 Server (163coms[...])
Messages: 126. Size: 27228317
From: Test <xxxxxx@qq.com>
To: Python爱好者 <xxxxxx@163.com>
Subject: 用POP3收取邮件
part 0
--------------------
part 0
--------------------
Text: Python可以使用POP3收取邮件……...
part 1
--------------------
Text: Python可以<a href="...">使用POP3</a>收取邮件……...
part 1
--------------------
Attachment: application/octet-stream
- 从打印的结构可以看出,这封邮件是一个
MIMEMultipart
,它包含两部分:第一部分又是一个MIMEMultipart
,第二部分是一个附件。而内嵌的MIMEMultipart
是一个alternative
类型,它包含一个纯文本格式的MIMEText
和一个 HTML 格式的MIMEText
。
(3)小结
- 用 Python 的
poplib
模块收取邮件分两步:第一步是用 POP3 协议把邮件获取到本地,第二步是用email
模块把原始邮件解析为Message
对象,然后,用适当的形式把邮件内容展示给用户即可。