没有看不到的消息,只有不想回的人。

电子邮件是最古老的互联网应用之一。在移动互联网和即时通讯没爆发前,邮件是人们沟通的主要形式。

和HTTP网页服务一样,电子邮件也有自己的协议:

  • SMTP:用来发邮件的协议。
  • POP3:用来收邮件的协议,数据单向从邮箱传递给客户端。
  • IMAP:是POP3的扩展,可以浏览摘要后再选择下载邮件,也可以从本地同步数据给邮箱。

Python提供了4个标准模块处理邮件:

  • smtplib,用于发邮件。
  • poplib,用POP3协议收邮件。
  • imaplib,用IMAP协议同步客户端和邮箱数据。
  • email,用于解析邮件内容结构。

20年前,只要你能连到互联网,就能提供邮箱服务。那时候的互联网,除了门户网站,就是电子邮件。

随着准入门槛的提高,目前邮箱服务主要由电信运营商和几个互联网巨头提供,以Web版和App客户端为主,一般都需要独立授权码才能开启三方客户端访问其SMTP/IMAP/POP3接口。

9、邮件和机器人 - 图1

9、邮件和机器人 - 图2

邮件主要的2个用途:正式沟通、自动通知。

  • “正式沟通”指较为正式的事项确认,工作中较常见。
  • “自动通知”指主动发送或者当条件出发时自动发送邮件。

“自动通知”的应用范围较广,如订阅频道后定期推送内容,代码构建失败后通知成员,每日收集情报后汇总发送给有关人员等等。

随着互联网办公软件成熟,群组功能在企业内应用也更加广泛,企业微信和钉钉是国内2个最主流的企业办公服务软件。
它们都提供了机器人功能,可以自动通知消息到所在群组成员,比邮件及时,也更灵活。

9、邮件和机器人 - 图3

所以我们可以把这类应用归为自动通知场景:

  • 自动发送邮件:如定时周报或条件触发后自动邮件通知。
  • 归档整理邮件:自动下载邮件,提取内容保存,如附件。
  • 机器人通知:钉钉和企业微信群内自动通知相关成员。

发送邮件

用Python发邮件主要分3步:

  1. 构造邮件内容
  2. 账号登陆授权
  3. 发送邮件消息

其中有几个注意点:

  • 登陆密码是授权码,而非邮箱Web版客户端的登陆密码。
  • 有些服务商会选用自定义端口并要求用SSL加密后发邮件。
  • 不同服务商在协议实现支持上可能会有不同,有些服务商会禁止通过协议访问邮箱数据。此外用非官方客户端访问时,会出现’system busy’等提示,估计资源分配不如官方版本。
  • 真正发送者和目标在sendmail()方法中指定,邮件中的FromTo只是内容一部分。

尤其是最后一点,早先成了不少电子邮件欺诈者的帮凶,理论上的“发件人”你可以写任何名字。

比如下面标示的邮箱地址都是“假的”。

9、邮件和机器人 - 图4

目前一些服务商会对邮件内容做敏感信息过滤。

发送纯文本邮件

  1. # 纯文本邮件
  2. import smtplib
  3. from email.mime.text import MIMEText
  4. # 构造邮件
  5. msg = MIMEText('Hello Python1024!', 'plain', 'utf-8')
  6. msg['From'] = '程一初 <chengyichu@qq.com>'
  7. msg['To'] = '程一初1 <chengyichu1@qq.com>, 程一初2 <chengyichu2@qq.com>'
  8. msg['Subject'] = '来自Python1024'
  9. from_addr = 'ichengplus6@qq.com'
  10. # 密码是授权码
  11. password = '<你的授权码>'
  12. to_addr = '2700866814@qq.com'
  13. smtp_server = 'smtp.qq.com'
  14. # QQ邮箱的SMTP服务需SSL加密,端口为465
  15. server = smtplib.SMTP_SSL(smtp_server)
  16. # 显示发送过程
  17. server.set_debuglevel(1)
  18. # 登陆验证
  19. server.login(from_addr, password)
  20. # 发送邮件
  21. server.sendmail(from_addr, [to_addr], msg.as_string())
  22. # 退出
  23. server.quit()

发送HTML邮件

  1. import smtplib
  2. from email.mime.text import MIMEText
  3. # 构造邮件
  4. msg = MIMEText('<html><body><h1>Python1024</h1>' +
  5. '<p>由<a href="https://www.yuque.com/yichu">程一初</a> 发送。</p></body></html>', 'html', 'utf-8')
  6. msg['From'] = '程一初 <ichengplus6@qq.com>'
  7. msg['To'] = '程一初1 <chengyichu1@qq.com>, 程一初2 <chengyichu2@qq.com>'
  8. msg['Subject'] = '来自Python1024'
  9. from_addr = 'ichengplus6@qq.com'
  10. # 密码是授权码
  11. password = '<你的授权码>'
  12. to_addr = '2700866814@qq.com'
  13. smtp_server = 'smtp.qq.com'
  14. server = smtplib.SMTP_SSL(smtp_server, 465)
  15. server.login(from_addr, password)
  16. server.sendmail(from_addr, [to_addr], msg.as_string())
  17. server.quit()

发送带附件邮件

  1. import pathlib
  2. import smtplib
  3. from email.mime.text import MIMEText
  4. from email.mime.multipart import MIMEMultipart
  5. from email.mime.image import MIMEImage
  6. from email.mime.application import MIMEApplication
  7. path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/009email')
  8. file_path = path.joinpath('avatar.jpg')
  9. zip_path = path.joinpath('avatar.jpg.zip')
  10. # 构造邮件
  11. msg = MIMEMultipart()
  12. msg['From'] = '程一初 <ichengplus6@qq.com>'
  13. msg['To'] = '程一初1 <chengyichu1@qq.com>, 程一初2 <chengyichu2@qq.com>'
  14. msg['Subject'] = '来自Python1024'
  15. from_addr = 'ichengplus6@qq.com'
  16. msg.attach(MIMEText('<html><body>请查收附件</body></html>', 'html', 'utf-8'))
  17. # 图像文件在邮箱Web客户端中可以预览
  18. with open(file_path, 'rb') as f:
  19. mime = MIMEImage(f.read())
  20. mime.add_header('Content-Disposition', 'attachment',
  21. filename=file_path.name)
  22. mime.add_header('Content-ID', '<image1>')
  23. msg.attach(mime)
  24. # 其他应用文件
  25. with open(zip_path, 'rb') as f:
  26. mime = MIMEApplication(f.read())
  27. mime.add_header('Content-Disposition', 'attachment',
  28. filename=zip_path.name)
  29. msg.attach(mime)
  30. password = '<你的授权码>'
  31. to_addr = '2700866814@qq.com'
  32. smtp_server = 'smtp.qq.com'
  33. server = smtplib.SMTP_SSL(smtp_server, 465)
  34. server.login(from_addr, password)
  35. server.sendmail(from_addr, [to_addr], msg.as_string())
  36. server.quit()

收邮件

Python收邮件可以选择使用POP3IMAP两种协议。

  • poplib:用POP3协议收取MIME内容。
  • smtplib:用IMAP获取邮箱内容,也可以同步数据给邮箱,数据可以双向传递。

从邮箱收到数据后,可以用email模块按邮件标准格式解析。

POP3自动下载邮件

  1. import pathlib
  2. import poplib
  3. from email.parser import Parser
  4. from email.header import Header, decode_header
  5. path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/009email')
  6. out_path = path.joinpath('009email_pop3')
  7. def download_msg(msg, idx, path=out_path):
  8. """解析邮件并保存到文件"""
  9. for part in msg.walk():
  10. # 遍历邮件内容
  11. if not part.is_multipart():
  12. # 有文件名即为附件
  13. filename = part.get_filename()
  14. # 获取内容并解码
  15. content = part.get_payload(decode=True)
  16. if filename:
  17. # 获取信息头中文件名
  18. h = decode_header(Header(filename))
  19. filename = str(h[0][0], encoding='utf-8')
  20. file_path = path.joinpath(f'mail_{idx}_attach_{filename}')
  21. else:
  22. file_path = path.joinpath(f'mail_{idx}_text')
  23. with open(path.joinpath(file_path), 'wb') as f:
  24. f.write(content)
  25. user = '2700866814@qq.com'
  26. password = '<你的授权码>'
  27. pop3_svr = 'pop.qq.com'
  28. svr = poplib.POP3_SSL(pop3_svr)
  29. svr.set_debuglevel(1)
  30. print(svr.getwelcome().decode('utf-8'))
  31. # 认证登陆
  32. svr.user(user)
  33. svr.pass_(password)
  34. # 查看邮箱内邮件数、占用空间
  35. print('邮件 {} 封, 占用 {} 字节。'.format(*svr.stat()))
  36. # 邮件列表
  37. resp, mails, octets = svr.list()
  38. print(mails)
  39. # 获取最新邮件, 注意索引从1开始
  40. index = len(mails)
  41. resp, lines, octets = svr.retr(index)
  42. # 获取的邮件内容按行合并
  43. msg_content = b'\r\n'.join(lines).decode('utf-8')
  44. # 获取MIME对象
  45. msg = Parser().parsestr(msg_content)
  46. # 下载邮件内容
  47. download_msg(msg, index)
  48. svr.quit()

IMAP自动下载邮件

  1. import re
  2. import pathlib
  3. import imaplib
  4. import email
  5. from email.parser import BytesFeedParser
  6. from email.header import Header, decode_header, make_header
  7. path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/009email')
  8. out_path = path.joinpath('009email_imap')
  9. def download_msg(msg, idx, path=out_path):
  10. # <同上>
  11. pass
  12. user = '2700866814@qq.com'
  13. password = '<你的授权码>'
  14. imap_svr = 'imap.qq.com'
  15. imaplib.Debug = 4
  16. svr = imaplib.IMAP4_SSL(imap_svr)
  17. svr.login(user, password)
  18. print(svr.welcome)
  19. # 列出所有邮箱
  20. rcode, mbox_list = svr.list()
  21. # 用正则表达式解析信息
  22. list_pattern = re.compile(r'.(?P<flags>.*?). "(?P<delimiter>.*)" (?P<name>.*)')
  23. for mbox in mbox_list:
  24. flags, delimiter, mbox_name = list_pattern.match(mbox.decode('utf-8')).groups()
  25. print(mbox_name.strip())
  26. # 检查某个邮箱状态,如邮件数、未读邮件等
  27. rcode, res = svr.status('INBOX','(MESSAGES RECENT UNSEEN)',)
  28. print(res)
  29. # 选择一个邮箱
  30. rcode, res = svr.select('INBOX', readonly=True)
  31. print(f'共有 {int(res[0])} 封邮件')
  32. # 查询未读邮件,返回邮件ID
  33. rcode, msg_ids = svr.search('(UNSEEN)')
  34. print(msg_ids)
  35. # QQMail的IMAP不支持按主题搜索,搜索功能有限
  36. rcode, msg_ids = svr.search('(SUBJECT "python1024")',)
  37. print(rcode, msg_ids)
  38. for m in msg_ids[0].split():
  39. # 根据邮件ID获取邮件,按RFC822格式
  40. rcode, msg_data = svr.fetch(m, '(RFC822)')
  41. for part in msg_data:
  42. if isinstance(part, tuple):
  43. msg = email.message_from_bytes(part[1])
  44. download_msg(msg, str(m, encoding='utf-8'), out_path)

IMAP协议同步信息到邮箱

  1. import time
  2. import imaplib
  3. from email.message import Message
  4. user = '2700866814@qq.com'
  5. password = '<你的授权码>'
  6. imap_svr = 'imap.qq.com'
  7. imaplib.Debug = 4
  8. svr = imaplib.IMAP4_SSL(imap_svr)
  9. svr.login(user, password)
  10. # 创建一个邮箱
  11. rcode, res = svr.create('python1024')
  12. print(rcode, res)
  13. # 构造一条邮件信息
  14. msg = Message()
  15. msg.set_unixfrom('pymotw')
  16. msg['Subject'] = 'Python1024上传主题'
  17. msg['From'] = 'chengyichu@qq.com'
  18. msg['To'] = 'ichengplus6@qq.com'
  19. msg.set_payload('这是Python1024上传的信息')
  20. # 上传信息
  21. svr.append('python1024', '',
  22. imaplib.Time2Internaldate(time.time()),
  23. str(msg).encode('utf-8'))
  24. rcode, res = svr.select('python1024')
  25. print(f'有{res[0]}封邮件。')
  26. rcode, res = svr.search('ALL')
  27. msg_id = res[0].split()[-1]
  28. # 设置新信息未读状态
  29. svr.store(msg_id, '-FLAGS', '(\Seen)')
  30. rcode, res = svr.fetch(msg_id, '(FLAGS)')
  31. print(res)
  32. # 可以把消息复制到其他邮箱
  33. svr.copy(msg_id, 'INBOX')

机器人应用

这里所谓的机器人,是指钉钉、企业微信中参与聊天的机器人账号。
其本质是一个restful接口的账号,通过接口控制账号行为。

目前这类机器人主要应用场景有:

  1. 快速传达信息到社群内,如技术运维、运营分析、情报获取等信息。
  2. 打造自动社群体验流程,如自助客服、新业务体验等。

在钉钉和企业微信中,一般由管理员创建机器人:

  • 钉钉群和企业微信内部群支持机器人主动发送信息。
  • 企业微信外部群机器人只能被动回应关键词,不能主动发信息。

下面主要介绍钉钉和企业微信内部群的机器人使用方式。

创建机器人流程:

  1. 创建钉钉/企业微信内部群。
  2. 在群设置中,添加机器人。
  3. 记录下调用机器人的web地址。

调用机器人的接口需要发送HTTP请求,常用的Python模块如urllibrequests
requests模块安装:pip install requests

发送消息流程就2步:

  1. 构建消息内容数据
  2. 使用requests发送post请求。

注意点:

  • 为HTTP请求增加头部信息,指明请求内容为json数据。
  • 提交请求时,需要用json.dumps()方法对数据编码。
  • 机器人发送消息有频率限制:20条/分钟

钉钉机器人使用

  1. import json
  2. import requests
  3. token = '<钉钉TOKEN,在学习群共享>'
  4. keyword = 'python1024'
  5. robot_url = f'https://oapi.dingtalk.com/robot/send?access_token={token}'
  6. HEADERS = {'Content-Type': 'application/json'}
  7. # 发送文本消息
  8. txt_msg = {
  9. 'msgtype': 'text',
  10. 'text': {
  11. 'content': '欢迎加入Python1024学习群。'
  12. },
  13. 'at': {
  14. 'atMobiles': [
  15. '138xxxxxxxx',
  16. ],
  17. "isAtAll": True
  18. }
  19. }
  20. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(txt_msg))
  21. print(r.json())
  22. # 发送链接信息
  23. link_msg = {
  24. 'msgtype': 'link',
  25. 'link': {
  26. 'text': '欢迎加入Python1024,一起探索效率提升。',
  27. 'title': '程一初的语雀空间',
  28. 'picUrl': 'https://cdn.nlark.com/yuque/0/2019/png/265643/1550131528833-avatar/b9063fa5-24b2-4360-aaae-8374fa12c8aa.png',
  29. 'messageUrl': 'https://www.yuque.com/yichu/'
  30. }
  31. }
  32. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(link_msg))
  33. print(r.json())
  34. # 发送Markdown消息
  35. md_msg = {
  36. 'msgtype': 'markdown',
  37. 'markdown': {
  38. 'title': '欢迎加入Python1024',
  39. 'text': '![animate](https://picbed-yichu.oss-cn-shenzhen.aliyuncs.com/picgo/58932a984729f.gif) [视频处理的实用工具](https://www.yuque.com/yichu/selflearning/as6xzv)'
  40. },
  41. 'at': {
  42. "isAtAll": True
  43. }
  44. }
  45. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(md_msg))
  46. print(r.json())
  47. # 卡片消息
  48. card_msg = {
  49. 'msgtype': 'actionCard',
  50. 'actionCard': {
  51. 'title': '欢迎加入Python1024',
  52. 'text': '![animate](https://picbed-yichu.oss-cn-shenzhen.aliyuncs.com/picgo/58932a984729f.gif) [视频处理的实用工具](https://www.yuque.com/yichu/selflearning/as6xzv)',
  53. 'btnOrientation': '0',
  54. 'singleTitle': '打开空间',
  55. 'singleURL': 'https://www.yuque.com/yichu/'
  56. }
  57. }
  58. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(card_msg))
  59. print(r.json())
  60. # 选项卡片消息
  61. cardm_msg = {
  62. 'msgtype': 'actionCard',
  63. 'actionCard': {
  64. 'title': '欢迎加入Python1024',
  65. 'text': '![animate](https://picbed-yichu.oss-cn-shenzhen.aliyuncs.com/picgo/58932a984729f.gif) [视频处理的实用工具](https://www.yuque.com/yichu/selflearning/as6xzv)',
  66. 'btnOrientation': '0',
  67. 'btns': [
  68. {
  69. 'title': 'Python自学手册',
  70. 'actionURL': 'https://www.yuque.com/yichu/selflearning/ux59c6'
  71. },
  72. {
  73. 'title': 'Python自动办公',
  74. 'actionURL': 'https://www.yuque.com/yichu/'
  75. }
  76. ]
  77. }
  78. }
  79. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(cardm_msg))
  80. print(r.json())
  81. # 发送信息流卡片消息
  82. free_card_msg = {
  83. 'msgtype': 'feedCard',
  84. 'feedCard': {
  85. 'links': [
  86. {
  87. 'title': '程一初的语雀空间',
  88. 'messageURL': 'https://www.yuque.com/yichu/',
  89. 'picURL': 'https://cdn.nlark.com/yuque/0/2019/png/265643/1550131528833-avatar/b9063fa5-24b2-4360-aaae-8374fa12c8aa.png'
  90. },
  91. {
  92. 'title': 'Python1024自动办公系列',
  93. 'messageURL': 'https://www.yuque.com/yichu/selflearning/ux59c6',
  94. 'picURL': 'https://cdn.nlark.com/yuque/0/2020/png/265643/1597933041598-ccd75182-aa75-447c-a1f5-7a2685acf88d.png'
  95. }
  96. ]
  97. }
  98. }
  99. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(free_card_msg))
  100. print(r.json())

企业微信内部群机器人使用

  1. import base64
  2. import hashlib
  3. import pathlib
  4. import json
  5. import requests
  6. path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/009email')
  7. file_path = path.joinpath('avatar.jpg.zip')
  8. img_path = path.joinpath('avatar.jpg')
  9. token = '<企业微信TOKEN,在学习群共享>'
  10. robot_url = f'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={token}'
  11. HEADERS = {'Content-Type': 'application/json'}
  12. # 发送文本消息
  13. txt_msg = {
  14. 'msgtype': 'text',
  15. 'text': {
  16. 'content': '欢迎加入Python1024学习群。',
  17. 'mentioned_list':['ichengplus6','@all'],
  18. 'mentioned_mobile_list': ['138XXXXXXX']
  19. }
  20. }
  21. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(txt_msg))
  22. print(r.json())
  23. # 发送Markdown消息
  24. md_msg = {
  25. 'msgtype': 'markdown',
  26. 'markdown': {
  27. '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\
  28. 2.[<font color="warning">Python自动办公系列</font>](https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1477393309697392646&__biz=MzUxNzE4MDkzNw==#wechat_redirect)'
  29. }
  30. }
  31. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(md_msg))
  32. print(r.json())
  33. # 发送图片消息,编码前不超过2M,JPG、PNG
  34. with open(img_path, 'rb') as f:
  35. img_data = f.read()
  36. b64img = base64.b64encode(img_data)
  37. md5 = hashlib.md5()
  38. md5.update(img_data)
  39. img_msg = {
  40. 'msgtype': 'image',
  41. 'image': {
  42. 'base64': b64img.decode('utf-8'),
  43. 'md5': md5.hexdigest()
  44. }
  45. }
  46. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(img_msg))
  47. print(r.json())
  48. # 发送图文消息
  49. news_msg = {
  50. 'msgtype': 'news',
  51. 'news': {
  52. 'articles': [
  53. {
  54. 'title':'Python基础系列',
  55. 'description': '通往高手的必经之路,看得懂、学得会、用得上。',
  56. 'url': 'http://mp.weixin.qq.com/mp/homepage?__biz=MzUxNzE4MDkzNw==&hid=2&sn=7d05116c613d0e41797858d59d61ffb2&scene=18#wechat_redirect',
  57. 'picurl': 'http://mmbiz.qpic.cn/mmbiz_jpg/kSfot6Ez8OicVK0w0bwOkUjYG7I3Sux1icQjjKO7PPMud4YtqoU767mYcrRZNyOicHJiaEFfAGvcz1GWkYWxHxmPpA/0'
  58. },
  59. {
  60. 'title':'Python自动办公系列',
  61. 'description': '涵盖文本、Word、PPT、Excel、图像、音频、视频、邮件、机器人等常见应用。',
  62. 'url': 'https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1477393309697392646&__biz=MzUxNzE4MDkzNw==#wechat_redirect',
  63. 'picurl': 'http://mmbiz.qpic.cn/mmbiz_jpg/kSfot6Ez8O9sMTzMoHTiaTONYfwAO4ibFc7KaIyMM1xedibr2PHs2e9MD9SuS22v3nZL5b80dd3Ynarkm0fibbEuZA/0'
  64. }
  65. ]
  66. }
  67. }
  68. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(news_msg))
  69. print(r.json())
  70. # 发送文件
  71. media_id = ''
  72. if not media_id:
  73. # 上传文件
  74. url = f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={token}&type=file'
  75. files = [('file', (file_path.name, open(file_path, 'rb')))]
  76. r = requests.post(url, files=files)
  77. j = r.json()
  78. media_id = j['media_id']
  79. print(media_id)
  80. file_msg = {
  81. 'msgtype': 'file',
  82. 'file': {
  83. 'media_id': media_id
  84. }
  85. }
  86. r = requests.post(robot_url, headers=HEADERS, data=json.dumps(file_msg))
  87. print(r.json())

总结

本文主要介绍了用Python收发邮件的主要场景,以及钉钉和企业微信机器人的基本使用方法。

在现实的互联网运营中,我们经常会发现一些非官方支持的机器人,如微信机器人、QQ机器人等。
这些都是通过破解官方客户端协议实现,属于非正常使用,受到官方打击和封禁。
哪里有流量红利,哪里就会有更多技术创新,虽然有时创新的技术非官方所愿。

目前,钉钉的功能更丰富,但企业微信背靠10亿级月活的微信,也正在发力。

你认为哪个更有潜力呢?

加入学习群

9、邮件和机器人 - 图5