Python发送邮件(多收件、多抄送、多表格、多附件)

1. 前言

利用 smtplib 发送邮件的基础教程也不少,本文不多加阐述,完整代码贴在文后,代码中注释已详细说明,这里就大概说说需要注意的要点。

2. 要点

  • 确认邮箱使用的协议与加密方式

搞清楚邮箱服务商所提供的协议,是否支持 stmp,是否只支持 SSL。印象中,腾讯企业邮箱或者QQ邮箱都是使用 SSL 加密,此时我们必须使用 SMTP_SSL

  • 确认邮箱提供 IMAP/SMTP 服务的地址与端口

跟以往不同的是,用非官方途径利用官方的邮箱服务器发送邮件,就要拿到该邮箱提供的服务器地址与端口,并在邮箱设置里开启 IMAP/SMTP 服务。例如腾讯企业邮箱的地址便是 smtp.exmaill.qq.com,端口号 465

  • 邮箱安全模式

若邮箱开启了安全模式,如腾讯企业邮箱,客户端专用密码将代替邮箱的登录密码。则此时我们利用邮箱 IMAP/SMTP 服务的登录密码,需要填客户端专用密码而非邮箱的登录密码。

  • 发送 html 格式内容,例如 html 表格

将容纳正文的容器 MIMEText() 指定格式为 html 即可。此时邮件正文将以 html 格式展现。我们可以利用 html 实现表格的构造,涉及到 html 语言,可参考 将 DataFrame 转为 html 格式文本。

  • 邮件收件人发件人标题中文乱码

邮件中的字符串采用 RFC 格式,至于这个格式怎么编码就交给 smtplib,而我们要做的是,将字符串在编码成 RFC 前解码为 utf-8。即代码中 Header().encode 函数。

  • 邮件正文中文乱码

MIMEText() 指定编码 utf-8

  • 附件文件中文名乱码

需给容纳附件的容器添加信息头时,指定 filename 参数将标题名编码成 gbk

  • SMTPServerDisconnected: Connection unexpectedly closed: The read operation timed out

虽然报错,但邮件确实是发送成功了,好像没什么异常情况,这点我好像与别人不一样。所以可以取巧,使用

try:
    ...
except smtplib.SMTPServerDisconnected:
    pass

捕捉异常后 pass 即可。

而真正的做法是, smtplib.SMTP_SSL() 函数添加参数 timeout = 10。此处考虑若发送多个大文件的情况,数值大小看个人情况考虑,数值单位为秒。报错的具体原因个人猜测是附件还未发送成功,serverquit 导致的报错。

3. 利用 smtplib 模拟发件过程

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

def send_email(from_addr, to_addr, mailhost, password, msg_subject, msg_text,
               cc_addr=None, text_type='plain', file_path=None):
    """
    from_addr: dict, 发件人的邮箱昵称和地址, 例如 {'吴刚':'[email protected]'}
    to_addr: dict, 收件人的邮箱昵称和地址, 例如 {'a':'[email protected]', 'b':'[email protected]'}
    mailhost: tuple or list, 邮箱服务器地址和端口, 例如 ('smtp.exmail.qq.com', 465)
    password: str, 密码或授权码
    msg_subject: str, 邮件的标题
    msg_text: str, 邮件的正文
    cc_addr: dict, 抄送人的邮箱昵称和地址, 例如 {'小明': '[email protected]'}
    text_type: str, 发送邮件的文本格式, default 'plain' ,普通文本用此即可,发送html表格
                    则使用 'html'
    file_path: str, 附件文件路径
    """
    msg = MIMEMultipart()   # 发送附件需要建立容器 MIMEMultipart,
                            # 如不需要发送附件,可直接建立容器 MIMEText
    msg['From'] = _format_addr(from_addr)   # 发送人
    msg['To'] = _format_addr(to_addr)   # 收件人
    if cc_addr is not None:
        msg['Cc'] = _format_addr(cc_addr)   # 抄送人
    msg['Subject'] = Header(msg_subject, 'utf-8').encode()  # 邮件主题
    msg.attach(MIMEText(msg_text, text_type, 'utf-8'))  # 指定文本格式为简洁,编码 utf-8
    if file_path is not None:
        msg = files(msg, file_path) # 读取附件

    smtp_server, port = mailhost  # 邮箱服务器以及端口号
    # 使用 smtp 协议以及 ssl 加密方式
    server = smtplib.SMTP_SSL(smtp_server, int(port), timeout=10.0)
    server.set_debuglevel(0)  # 设置 debug 级别 1:打印 0: 不打印
    server.connect(smtp_server)  # 连接服务器
    server.login(get_val(from_addr)[0], password)  # 发件人账号登录

    # 发送邮件
    if cc_addr is not None:
        server.sendmail(get_val(from_addr)[0],
                        get_val(to_addr) + get_val(cc_addr),
                        msg.as_string())
    else:
        server.sendmail(get_val(from_addr)[0], get_val(to_addr),
                        msg.as_string())
    server.quit()  # 退出

def get_val(dic):
    """字典取值,返回列表"""
    val = list(dic.values())
    return val

def _format_addr(dic):
    """规范地址格式处理, 支持多个收件人"""
    addr_list = []
    for name, addr in dic.items():
        addr_list.append(formataddr((Header(name, 'utf-8').encode(), addr)))
    return ','.join(addr_list)

def files(msg, file_path):
    """
    msg : object
        邮件主体容器
    file_path : str or list
        附件的文件路径
    """
    if isinstance(file_path, str):
        file_path = [file_path]

    for path in file_path:
        # 添加附件就是加上一个MIMEBase,从本地读取一个文件:
        with open(path, 'rb') as f:
            # 创建收纳附件的容器 mime
            mime = MIMEBase('application', 'octet-stream')
            # 构造附件
            basename = os.path.basename(path) # 注意:此时 basename 为 utf-8 编码

            # 加上必要的头信息
            # basename 转码 gbk, 否则附件带有中文名会有乱码
            mime.add_header('Content-Disposition', 'attachment',
                            filename=('gbk', '', basename))
            mime.add_header('Content-ID', '<0>')
            mime.add_header('X-Attachment-Id', '0')
            mime.set_payload(f.read())  # 把附件的内容读进 mime
            encoders.encode_base64(mime)  # 用Base64编码
            msg.attach(mime)  # 添加到 MIMEMultipart 中
    return msg

if __name__ == "__main__":
    from_addr = {'我是发件人': '[email protected]'}  # 发送人昵称和地址
    to_addr = {'收件人1': '[email protected]', '收件人2': '[email protected]'}  # 收件人昵称地址
    mailhost = ('smtp.exmail.qq.com', 465)
    password = 'mypassword'
    msg_subject = '我是邮件标题'
    exc_msg = '我是邮件正文内容'
    file_path = ["测试附件1.xlsx", "测试附件2.xlsx"]
    send_email(from_addr, to_addr, mailhost, password, msg_subject,
                exc_msg, cc_addr=cc_addr, file_path=file_path)

你可能感兴趣的:(Python发送邮件(多收件、多抄送、多表格、多附件))