没有看不到的消息,只有不想回的人。
电子邮件是最古老的互联网应用之一。在移动互联网和即时通讯没爆发前,邮件是人们沟通的主要形式。
和HTTP网页服务一样,电子邮件也有自己的协议:
SMTP:用来发邮件的协议。POP3:用来收邮件的协议,数据单向从邮箱传递给客户端。IMAP:是POP3的扩展,可以浏览摘要后再选择下载邮件,也可以从本地同步数据给邮箱。
Python提供了4个标准模块处理邮件:
smtplib,用于发邮件。poplib,用POP3协议收邮件。imaplib,用IMAP协议同步客户端和邮箱数据。email,用于解析邮件内容结构。
20年前,只要你能连到互联网,就能提供邮箱服务。那时候的互联网,除了门户网站,就是电子邮件。
随着准入门槛的提高,目前邮箱服务主要由电信运营商和几个互联网巨头提供,以Web版和App客户端为主,一般都需要独立授权码才能开启三方客户端访问其SMTP/IMAP/POP3接口。


邮件主要的2个用途:正式沟通、自动通知。
- “正式沟通”指较为正式的事项确认,工作中较常见。
- “自动通知”指主动发送或者当条件出发时自动发送邮件。
“自动通知”的应用范围较广,如订阅频道后定期推送内容,代码构建失败后通知成员,每日收集情报后汇总发送给有关人员等等。
随着互联网办公软件成熟,群组功能在企业内应用也更加广泛,企业微信和钉钉是国内2个最主流的企业办公服务软件。
它们都提供了机器人功能,可以自动通知消息到所在群组成员,比邮件及时,也更灵活。

所以我们可以把这类应用归为自动通知场景:
- 自动发送邮件:如定时周报或条件触发后自动邮件通知。
- 归档整理邮件:自动下载邮件,提取内容保存,如附件。
- 机器人通知:钉钉和企业微信群内自动通知相关成员。
发送邮件
用Python发邮件主要分3步:
- 构造邮件内容
- 账号登陆授权
- 发送邮件消息
其中有几个注意点:
- 登陆密码是授权码,而非邮箱Web版客户端的登陆密码。
- 有些服务商会选用自定义端口并要求用SSL加密后发邮件。
- 不同服务商在协议实现支持上可能会有不同,有些服务商会禁止通过协议访问邮箱数据。此外用非官方客户端访问时,会出现’system busy’等提示,估计资源分配不如官方版本。
- 真正发送者和目标在
sendmail()方法中指定,邮件中的From和To只是内容一部分。
尤其是最后一点,早先成了不少电子邮件欺诈者的帮凶,理论上的“发件人”你可以写任何名字。
比如下面标示的邮箱地址都是“假的”。

目前一些服务商会对邮件内容做敏感信息过滤。
发送纯文本邮件
# 纯文本邮件import smtplibfrom email.mime.text import MIMEText# 构造邮件msg = MIMEText('Hello Python1024!', 'plain', 'utf-8')msg['From'] = '程一初 <chengyichu@qq.com>'msg['To'] = '程一初1 <chengyichu1@qq.com>, 程一初2 <chengyichu2@qq.com>'msg['Subject'] = '来自Python1024'from_addr = 'ichengplus6@qq.com'# 密码是授权码password = '<你的授权码>'to_addr = '2700866814@qq.com'smtp_server = 'smtp.qq.com'# QQ邮箱的SMTP服务需SSL加密,端口为465server = smtplib.SMTP_SSL(smtp_server)# 显示发送过程server.set_debuglevel(1)# 登陆验证server.login(from_addr, password)# 发送邮件server.sendmail(from_addr, [to_addr], msg.as_string())# 退出server.quit()
发送HTML邮件
import smtplibfrom email.mime.text import MIMEText# 构造邮件msg = MIMEText('<html><body><h1>Python1024</h1>' +'<p>由<a href="https://www.yuque.com/yichu">程一初</a> 发送。</p></body></html>', 'html', 'utf-8')msg['From'] = '程一初 <ichengplus6@qq.com>'msg['To'] = '程一初1 <chengyichu1@qq.com>, 程一初2 <chengyichu2@qq.com>'msg['Subject'] = '来自Python1024'from_addr = 'ichengplus6@qq.com'# 密码是授权码password = '<你的授权码>'to_addr = '2700866814@qq.com'smtp_server = 'smtp.qq.com'server = smtplib.SMTP_SSL(smtp_server, 465)server.login(from_addr, password)server.sendmail(from_addr, [to_addr], msg.as_string())server.quit()
发送带附件邮件
import pathlibimport smtplibfrom email.mime.text import MIMETextfrom email.mime.multipart import MIMEMultipartfrom email.mime.image import MIMEImagefrom email.mime.application import MIMEApplicationpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/009email')file_path = path.joinpath('avatar.jpg')zip_path = path.joinpath('avatar.jpg.zip')# 构造邮件msg = MIMEMultipart()msg['From'] = '程一初 <ichengplus6@qq.com>'msg['To'] = '程一初1 <chengyichu1@qq.com>, 程一初2 <chengyichu2@qq.com>'msg['Subject'] = '来自Python1024'from_addr = 'ichengplus6@qq.com'msg.attach(MIMEText('<html><body>请查收附件</body></html>', 'html', 'utf-8'))# 图像文件在邮箱Web客户端中可以预览with open(file_path, 'rb') as f:mime = MIMEImage(f.read())mime.add_header('Content-Disposition', 'attachment',filename=file_path.name)mime.add_header('Content-ID', '<image1>')msg.attach(mime)# 其他应用文件with open(zip_path, 'rb') as f:mime = MIMEApplication(f.read())mime.add_header('Content-Disposition', 'attachment',filename=zip_path.name)msg.attach(mime)password = '<你的授权码>'to_addr = '2700866814@qq.com'smtp_server = 'smtp.qq.com'server = smtplib.SMTP_SSL(smtp_server, 465)server.login(from_addr, password)server.sendmail(from_addr, [to_addr], msg.as_string())server.quit()
收邮件
Python收邮件可以选择使用POP3或IMAP两种协议。
poplib:用POP3协议收取MIME内容。smtplib:用IMAP获取邮箱内容,也可以同步数据给邮箱,数据可以双向传递。
从邮箱收到数据后,可以用email模块按邮件标准格式解析。
用POP3自动下载邮件
import pathlibimport poplibfrom email.parser import Parserfrom email.header import Header, decode_headerpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/009email')out_path = path.joinpath('009email_pop3')def download_msg(msg, idx, path=out_path):"""解析邮件并保存到文件"""for part in msg.walk():# 遍历邮件内容if not part.is_multipart():# 有文件名即为附件filename = part.get_filename()# 获取内容并解码content = part.get_payload(decode=True)if filename:# 获取信息头中文件名h = decode_header(Header(filename))filename = str(h[0][0], encoding='utf-8')file_path = path.joinpath(f'mail_{idx}_attach_{filename}')else:file_path = path.joinpath(f'mail_{idx}_text')with open(path.joinpath(file_path), 'wb') as f:f.write(content)user = '2700866814@qq.com'password = '<你的授权码>'pop3_svr = 'pop.qq.com'svr = poplib.POP3_SSL(pop3_svr)svr.set_debuglevel(1)print(svr.getwelcome().decode('utf-8'))# 认证登陆svr.user(user)svr.pass_(password)# 查看邮箱内邮件数、占用空间print('邮件 {} 封, 占用 {} 字节。'.format(*svr.stat()))# 邮件列表resp, mails, octets = svr.list()print(mails)# 获取最新邮件, 注意索引从1开始index = len(mails)resp, lines, octets = svr.retr(index)# 获取的邮件内容按行合并msg_content = b'\r\n'.join(lines).decode('utf-8')# 获取MIME对象msg = Parser().parsestr(msg_content)# 下载邮件内容download_msg(msg, index)svr.quit()
用IMAP自动下载邮件
import reimport pathlibimport imaplibimport emailfrom email.parser import BytesFeedParserfrom email.header import Header, decode_header, make_headerpath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/009email')out_path = path.joinpath('009email_imap')def download_msg(msg, idx, path=out_path):# <同上>passuser = '2700866814@qq.com'password = '<你的授权码>'imap_svr = 'imap.qq.com'imaplib.Debug = 4svr = imaplib.IMAP4_SSL(imap_svr)svr.login(user, password)print(svr.welcome)# 列出所有邮箱rcode, mbox_list = svr.list()# 用正则表达式解析信息list_pattern = re.compile(r'.(?P<flags>.*?). "(?P<delimiter>.*)" (?P<name>.*)')for mbox in mbox_list:flags, delimiter, mbox_name = list_pattern.match(mbox.decode('utf-8')).groups()print(mbox_name.strip())# 检查某个邮箱状态,如邮件数、未读邮件等rcode, res = svr.status('INBOX','(MESSAGES RECENT UNSEEN)',)print(res)# 选择一个邮箱rcode, res = svr.select('INBOX', readonly=True)print(f'共有 {int(res[0])} 封邮件')# 查询未读邮件,返回邮件IDrcode, msg_ids = svr.search('(UNSEEN)')print(msg_ids)# QQMail的IMAP不支持按主题搜索,搜索功能有限rcode, msg_ids = svr.search('(SUBJECT "python1024")',)print(rcode, msg_ids)for m in msg_ids[0].split():# 根据邮件ID获取邮件,按RFC822格式rcode, msg_data = svr.fetch(m, '(RFC822)')for part in msg_data:if isinstance(part, tuple):msg = email.message_from_bytes(part[1])download_msg(msg, str(m, encoding='utf-8'), out_path)
用IMAP协议同步信息到邮箱
import timeimport imaplibfrom email.message import Messageuser = '2700866814@qq.com'password = '<你的授权码>'imap_svr = 'imap.qq.com'imaplib.Debug = 4svr = imaplib.IMAP4_SSL(imap_svr)svr.login(user, password)# 创建一个邮箱rcode, res = svr.create('python1024')print(rcode, res)# 构造一条邮件信息msg = Message()msg.set_unixfrom('pymotw')msg['Subject'] = 'Python1024上传主题'msg['From'] = 'chengyichu@qq.com'msg['To'] = 'ichengplus6@qq.com'msg.set_payload('这是Python1024上传的信息')# 上传信息svr.append('python1024', '',imaplib.Time2Internaldate(time.time()),str(msg).encode('utf-8'))rcode, res = svr.select('python1024')print(f'有{res[0]}封邮件。')rcode, res = svr.search('ALL')msg_id = res[0].split()[-1]# 设置新信息未读状态svr.store(msg_id, '-FLAGS', '(\Seen)')rcode, res = svr.fetch(msg_id, '(FLAGS)')print(res)# 可以把消息复制到其他邮箱svr.copy(msg_id, 'INBOX')
机器人应用
这里所谓的机器人,是指钉钉、企业微信中参与聊天的机器人账号。
其本质是一个restful接口的账号,通过接口控制账号行为。
目前这类机器人主要应用场景有:
- 快速传达信息到社群内,如技术运维、运营分析、情报获取等信息。
- 打造自动社群体验流程,如自助客服、新业务体验等。
在钉钉和企业微信中,一般由管理员创建机器人:
- 钉钉群和企业微信内部群支持机器人主动发送信息。
- 企业微信外部群机器人只能被动回应关键词,不能主动发信息。
下面主要介绍钉钉和企业微信内部群的机器人使用方式。
创建机器人流程:
- 创建钉钉/企业微信内部群。
- 在群设置中,添加机器人。
- 记录下调用机器人的web地址。
调用机器人的接口需要发送HTTP请求,常用的Python模块如urllib、requests。requests模块安装:pip install requests。
发送消息流程就2步:
- 构建消息内容数据
- 使用
requests发送post请求。
注意点:
- 为HTTP请求增加头部信息,指明请求内容为
json数据。 - 提交请求时,需要用
json.dumps()方法对数据编码。 - 机器人发送消息有频率限制:20条/分钟
钉钉机器人使用
import jsonimport requeststoken = '<钉钉TOKEN,在学习群共享>'keyword = 'python1024'robot_url = f'https://oapi.dingtalk.com/robot/send?access_token={token}'HEADERS = {'Content-Type': 'application/json'}# 发送文本消息txt_msg = {'msgtype': 'text','text': {'content': '欢迎加入Python1024学习群。'},'at': {'atMobiles': ['138xxxxxxxx',],"isAtAll": True}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(txt_msg))print(r.json())# 发送链接信息link_msg = {'msgtype': 'link','link': {'text': '欢迎加入Python1024,一起探索效率提升。','title': '程一初的语雀空间','picUrl': 'https://cdn.nlark.com/yuque/0/2019/png/265643/1550131528833-avatar/b9063fa5-24b2-4360-aaae-8374fa12c8aa.png','messageUrl': 'https://www.yuque.com/yichu/'}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(link_msg))print(r.json())# 发送Markdown消息md_msg = {'msgtype': 'markdown','markdown': {'title': '欢迎加入Python1024','text': ' [视频处理的实用工具](https://www.yuque.com/yichu/selflearning/as6xzv)'},'at': {"isAtAll": True}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(md_msg))print(r.json())# 卡片消息card_msg = {'msgtype': 'actionCard','actionCard': {'title': '欢迎加入Python1024','text': ' [视频处理的实用工具](https://www.yuque.com/yichu/selflearning/as6xzv)','btnOrientation': '0','singleTitle': '打开空间','singleURL': 'https://www.yuque.com/yichu/'}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(card_msg))print(r.json())# 选项卡片消息cardm_msg = {'msgtype': 'actionCard','actionCard': {'title': '欢迎加入Python1024','text': ' [视频处理的实用工具](https://www.yuque.com/yichu/selflearning/as6xzv)','btnOrientation': '0','btns': [{'title': 'Python自学手册','actionURL': 'https://www.yuque.com/yichu/selflearning/ux59c6'},{'title': 'Python自动办公','actionURL': 'https://www.yuque.com/yichu/'}]}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(cardm_msg))print(r.json())# 发送信息流卡片消息free_card_msg = {'msgtype': 'feedCard','feedCard': {'links': [{'title': '程一初的语雀空间','messageURL': 'https://www.yuque.com/yichu/','picURL': 'https://cdn.nlark.com/yuque/0/2019/png/265643/1550131528833-avatar/b9063fa5-24b2-4360-aaae-8374fa12c8aa.png'},{'title': 'Python1024自动办公系列','messageURL': 'https://www.yuque.com/yichu/selflearning/ux59c6','picURL': 'https://cdn.nlark.com/yuque/0/2020/png/265643/1597933041598-ccd75182-aa75-447c-a1f5-7a2685acf88d.png'}]}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(free_card_msg))print(r.json())
企业微信内部群机器人使用
import base64import hashlibimport pathlibimport jsonimport requestspath = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/009email')file_path = path.joinpath('avatar.jpg.zip')img_path = path.joinpath('avatar.jpg')token = '<企业微信TOKEN,在学习群共享>'robot_url = f'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={token}'HEADERS = {'Content-Type': 'application/json'}# 发送文本消息txt_msg = {'msgtype': 'text','text': {'content': '欢迎加入Python1024学习群。','mentioned_list':['ichengplus6','@all'],'mentioned_mobile_list': ['138XXXXXXX']}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(txt_msg))print(r.json())# 发送Markdown消息md_msg = {'msgtype': 'markdown','markdown': {'content': '1.[<font color="comment">Python基础系列</font>](http://mp.weixin.qq.com/mp/homepage?__biz=MzUxNzE4MDkzNw==&hid=2&sn=7d05116c613d0e41797858d59d61ffb2&scene=18#wechat_redirect)\n\2.[<font color="warning">Python自动办公系列</font>](https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1477393309697392646&__biz=MzUxNzE4MDkzNw==#wechat_redirect)'}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(md_msg))print(r.json())# 发送图片消息,编码前不超过2M,JPG、PNGwith open(img_path, 'rb') as f:img_data = f.read()b64img = base64.b64encode(img_data)md5 = hashlib.md5()md5.update(img_data)img_msg = {'msgtype': 'image','image': {'base64': b64img.decode('utf-8'),'md5': md5.hexdigest()}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(img_msg))print(r.json())# 发送图文消息news_msg = {'msgtype': 'news','news': {'articles': [{'title':'Python基础系列','description': '通往高手的必经之路,看得懂、学得会、用得上。','url': 'http://mp.weixin.qq.com/mp/homepage?__biz=MzUxNzE4MDkzNw==&hid=2&sn=7d05116c613d0e41797858d59d61ffb2&scene=18#wechat_redirect','picurl': 'http://mmbiz.qpic.cn/mmbiz_jpg/kSfot6Ez8OicVK0w0bwOkUjYG7I3Sux1icQjjKO7PPMud4YtqoU767mYcrRZNyOicHJiaEFfAGvcz1GWkYWxHxmPpA/0'},{'title':'Python自动办公系列','description': '涵盖文本、Word、PPT、Excel、图像、音频、视频、邮件、机器人等常见应用。','url': 'https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1477393309697392646&__biz=MzUxNzE4MDkzNw==#wechat_redirect','picurl': 'http://mmbiz.qpic.cn/mmbiz_jpg/kSfot6Ez8O9sMTzMoHTiaTONYfwAO4ibFc7KaIyMM1xedibr2PHs2e9MD9SuS22v3nZL5b80dd3Ynarkm0fibbEuZA/0'}]}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(news_msg))print(r.json())# 发送文件media_id = ''if not media_id:# 上传文件url = f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={token}&type=file'files = [('file', (file_path.name, open(file_path, 'rb')))]r = requests.post(url, files=files)j = r.json()media_id = j['media_id']print(media_id)file_msg = {'msgtype': 'file','file': {'media_id': media_id}}r = requests.post(robot_url, headers=HEADERS, data=json.dumps(file_msg))print(r.json())
总结
本文主要介绍了用Python收发邮件的主要场景,以及钉钉和企业微信机器人的基本使用方法。
在现实的互联网运营中,我们经常会发现一些非官方支持的机器人,如微信机器人、QQ机器人等。
这些都是通过破解官方客户端协议实现,属于非正常使用,受到官方打击和封禁。
哪里有流量红利,哪里就会有更多技术创新,虽然有时创新的技术非官方所愿。
目前,钉钉的功能更丰富,但企业微信背靠10亿级月活的微信,也正在发力。
你认为哪个更有潜力呢?
加入学习群

