【高手过招第二期】使用代码发送邮件失败时的解决思路

背景

之前在某银行分行进行 RPA 项目开发,使用到了一个发邮件的功能。

因为客户的机子上自带有 outlook 软件,使用设计器组件可以直接进行调用,于是发邮件模块就使用了 outlook 组件进行实现。

但是实际在使用了一段时间该组件后,发现 outlook 软件经常罢工,于是决定弃用该组件,该用代码来实现发邮件功能。


邮箱代码

代码如下(代码在廖雪峰老师网站上找的):

from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
import smtplib

'''
    带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,
    所以,可以构造一个MIMEMultipart对象代表邮件本身,
    然后往里面加上一个MIMEText作为邮件正文,
    再继续往里面加上表示附件的MIMEBase对象即可:
'''


def _format_addr(s):
    # 格式化一个邮件地址,注意不能简单地传入name <addr@example.com>,因为如果包含中文,需要通过Header对象进行编码。
    name, addr = parseaddr(s)
    return formataddr((Header(name, 'utf-8').encode(), addr))


from_addr = "user@xx.com"  # 发件地址
password = "password"  # 密码
to_addr = "receiver@xx.com"  # 输入收件人地址,接收的是字符串而不是list,如果有多个邮件地址,用,分隔即可。 
smtp_server = "domain.com" # 设置邮箱服务器

msg = MIMEMultipart()  # 邮件对象:
msg['From'] = _format_addr("Python爱好者<%s>" % from_addr)
msg['To'] = _format_addr("管理员<%s>" % to_addr)
msg['Subject'] = Header('来着SMTP的问候', 'utf-8').encode()

# 邮件正文是MIMEText:
msg.attach(MIMEText('Hello, send by Python..', 'plain', 'utf-8'))

# 连接服务器、登录、发送邮件
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 个参数,分别为用户名、密码、邮箱服务器地址和端口。

  • 邮箱用户名、密码,这个由客户进行提供即可。

  • 服务器地址和端口,我则通过 outlook 软件进行查看,方式如下:【高手过招第二期】使用代码发送邮件失败时的解决思路


常规解决问题思路

  • 出于谨慎考虑,代码进拷贝进客户环境之前,我先在自己的笔记本的设计器上进行了测试。测试通过后,再将代码移植到客户电脑上。

  • 代码上客户环境后,再次进行调试,然后问题就来了。报错大致如下:
    【高手过招第二期】使用代码发送邮件失败时的解决思路

  • 报错日志,重点就是最后一句日志了 — 授权失败
    我边吐槽着这种水土不服的代码,一遍思索客户环境和我个人环境的差异。毫无疑问,肯定是邮箱服务器不一致的导致的。于是开始了以下的调试:

  1. 猜测是不是客户的邮箱服务器有开启了 SSL 安全协议,以上我将连接服务器的代码做了以下修改
    server = smtplib.SMTP_SSL(smtp_server, 465)
    再进行调试后:控制台又提示了以下错误
    [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1108)
    也是登录不上去,但是看这日志基本上也可以排除掉 SSL 协议问题了

  2. 再猜测是不是客户提供的密码有错误,毕竟密码输错对很多人来说是经常的事情,只是这可能性比较小。
    于是我找个客户核对了下,客户提供了他们网页版邮箱地址,我测试登录了下,确认了密码没问题。

  3. 推测客户的企业邮箱可能是用第三方授权码才能登录(QQ 邮箱、网易邮箱在第三方登录时也是用授权码)
    于是乎再找客户了解了下他们使用第三方邮箱软件的登录方式,得到的反馈是,电脑上的 foxmail 和手机上网易邮箱软件,都是可以直接邮箱 + 密码登录。因此,排查此问题。

  4. 看着授权失败的日志发呆了会,我灵光一闪,想到会不会是邮箱还需要设置开启这种 smap 协议,如下图(QQ 邮箱 SMTP 服务需要开启才能用)【高手过招第二期】使用代码发送邮件失败时的解决思路
    于是我又登录上了企业邮箱的网页版,将邮箱的所有设置检查了一遍,并没有发现类似这 SMTP 服务的开关设置。又又又排除掉了一种可能。

  5. 后来死马当活马医,命令行 telnet 下邮箱服务器地址和端口,看看能否正常打通
    telnet 请求回应成功,端口可以正常打通。说明地址没问题。于是乎我开始懵了。。。。


  • 前前后后花了两三个小时,还是没能解决这代码发邮件的问题。
  • 于是乎去找客户反馈这邮箱服务器的问题,打算让客户用回 outlook 组件发邮件,计划对于 outlook 不稳定问题,多加几个容错来处理。
  • 和客户聊着聊着,客户说起了他们邮箱服务有专门的运维大佬在管理,让我可以带上我的代码去咨询下情况。
  • 再见到大佬后,大佬笑着看了我封装的函数,表示我的代码没问题。说着说着他把自己测试用的 python 代码亮了出来,让我参考参考。我一看,也笑了,大家都是同一套模板啊。那邮箱问题的根源应该参数有问题了。

问题处理

  • 大佬在看了一眼几个参数后,直接指出了发送邮件失败的问题要点,是服务器域名和端口的问题。
  • 我在 outlook 软件上查看到的服务器域名和端口,那是他们总部的邮箱服务器。
  • outlook 软件之所以可以直接获得总部邮箱的授权登录,是因为他们的电脑系统是特制版,outlook 可以默认登录当前电脑登录的用户的邮箱,即他们 outlook 不用密码登录,只要电脑用户有登录,打开 outlook 软件该电脑用户的邮箱就可以默认登录。而第三方软件就不行了。
  • 之后大佬又给我提供了他们分行用的邮箱服务器地址和端口,再调试,成功,问题解决!

问题总结

现在大公司都有自家的邮箱服务,邮箱的标准协议功能大家也都是一样的,如邮箱登录、收发件等。如果大家实施过程中,自己写了这类邮箱操作的代码,测试在自己环境可以正常运行,而在客户环境运行不了。可以先排查掉一些低级错误,如邮箱服务器端口能否正常 telnet 通、邮箱是否有打开相关服务协议等。如果这类低级错误问题排查后还是无法解决。最好还是咨询下该公司的邮箱运维同事,看看他们邮箱的使用,是不是有什么特殊的规则,调用时有没有需要注意的事项等。

排除了不可能,剩下的就是真相 。—– 柯南道尔

【高手过招】第 2 期:实施 RPA 项目中,你遇到了哪些棘手的问题?