使用 python 发送电子邮件

使用 python 发送电子邮件

使用 Python 来发送邮件是一件很方便的事,有两个模块可以帮助我们完成这项任务。一个是 smtp 模块,用来发送邮件,另一个是 email 模块,用来构造邮件。

一、构造邮件

一封电子邮件,一般来说需要包含以下三部分内容:

  1. 邮件头:包括发件人、收件人、抄送、标题等内容。
  2. 正文:有两种形式,文本或超文本。
  3. 附件:附件有两种形式,一种嵌入超文本正文,另一种出现在附件列表中。

构造一封邮件我们可以按以下的步骤来操作:

1.处理附件

1.1 添加附件列表中的附件。附件可以有各种各样的格式,我们可以根据附件的不同,使用不用的 MIME 来进行包装。如图片可以使用 MIMEImage 来包装,音频文件可以使用 MIMEAudio 来进行包装,当然也可以全部使用 MIMEApplication 来进行包装。

在添加附件的时候,我们为这个 MIME 通过添加Content-Disposition来指定附件的类型以及文件名。

msg.add_header('Content-Disposition','attachment',filename=encode(file.name))

需要注意的是:文件名是中文件的话需要进行转码。转码的方法如下:

def encode(filename):
    return Charset('utf8').header_encode(filename) \
        if any(map(lambda x: ord(x) > 127, filename))else filename

1.2 添加内嵌附件的方法和上面一样,只不过每个文件需要指定一个 cid,以便在超文邮件正文中使用。具体代码如下:

msg.add_header('Content-Disposition', 'inline',filename=encode(file.name))
msg.add_header('Content-ID', f'<{cid}>')
msg.add_header('X-Attachment-Id', cid)

1.3 完整的代码

    def attach(self, filename, cid=None, writer=None):    # 添加附件
        '''
        filename: 文件名
        cid:     内嵌资源编号,如设置则不出现在附件列表中
        writer:   内容生成,如设置,则通过 writer(fn)的形式来获取数据
        '''
        file = Path(filename)
        msg = MIMEBase(*MIMETYPES.get(
            file.lsuffix, 'application/octet-stream').split('/'))
        if callable(writer):
            with io.BytesIO() as fp:
                writer(fp)
                fp.seek(0)
                data = fp.read()
        else:
            data = file.read_bytes()
        msg.set_payload(data)
        encoders.encode_base64(msg)
        msg.add_header('Content-Disposition', 'inline' if cid else 'attachment',
                       filename=encode(file.name))
        if cid:
            msg.add_header('Content-ID', f'<{cid}>')
            msg.add_header('X-Attachment-Id', cid)
            self.inline_attachments.append(msg)
        else:
            self.attachments.append(msg)

2.构建邮件正文

邮件正文使用 MIMEText 来构建,超级简单。不过要注意的是类型有两种,一种是超文本,其 subtypehtml,还有一种是纯文本,subtypeplain。具体代码如下:

subtype = 'html' if body.startswith('<html>') else 'plain'
msg = MIMEText(body, subtype, 'utf-8')

3.邮件合并

通过上面的步骤,邮件的各个部分都已经构建好了,接下来我们要对邮件的各个部分进行合并。合并是通过 MIMEMultipart 来实现的。为了简化处理,我们可以先定义一个合并函数来完成。

def combine(type_='mixed', *subparts):
    return MIMEMultipart(type_, _subparts=subparts)

参数说明:

  1. type_ 是合并类型,可以有以下几种:
    1. related:用来合并邮件正文和内嵌附件。
    2. alternative:用来合并两种纯文本和超文本两种。一般来说,我们的邮件正文要么用超文本,要么用纯文本,所以这种方式基本上用不到。
    3. mixed:用来合并邮件正文和附件。
  2. subparts是需要合并的各个部分。

合并的顺序:

  1. 先合并超文本和内嵌附件,使用 related来合并。
  2. 合并超文本和纯文本,使用 alternative来合并。
  3. 合并邮件正文和附件,使用 mixed来合并。

具体代码如下:

body = self.body
subtype = 'html' if body.startswith('<html>') else 'plain'
msg = MIMEText(body, subtype, 'utf-8')
if self.inline_attachments:
    msg = combine('related', msg, *self.inline_attachments)
if self.attachments:
    msg = combine('mixed', msg, *self.attachments)

4. 添加邮件头

邮件的各部分合并完成后,就需要添加邮件头了。需要注意的是,邮件头的各种地址列表都需要格式化。格式化的方法如下:

from email.utils import getaddresses, formataddr
def fmtaddr(addrs):
    return ';'.join(map(formataddr, getaddresses([addrs])))

完整设置邮件头的代码如下:

msg.add_header('Subject', self.subject)
for name in ('sender', 'to', 'cc', 'bcc'):
    val = getattr(self, name)
    if val:
        msg.add_header(name.capitalize(), fmtaddr(val))

通过上面的步骤,一封完整的邮件就构造好了。

二、发送邮件

发送邮件就超级简单了。连接邮件服务器后进行登录,然后就可以发邮件了。

其代码如下:

import smtplib
with smtplib.SMTP(host)as smtp:
    smtp.login(user,passwd)
    smtp.send_message(msg)

三、要点提示

用 Python 来发送电子邮件,网上的教程很多,但有少存在错误。主要是邮件的客户端太多,不同的邮件客户端的兼容性也不一样,如果不按规范来构造邮件有的客户端也会正确显示。在测试不充分的情况下,就会造成一些误解。

主要注意的事项有以下几个方面:

  1. 收件人及发件人的地址都需要格式化,否则存在中文时会显示不正确。
  2. 列表中的附件如果文件名是中文的,需要对文件名进行转码处理。
  3. 内嵌附件要有 cidContent-Disposition要设置成 inline ,并且和超文本正文合并时,要使用 related来合并。如果不这样操作,有的客户端,比如 outlook 可以在附件列表中隐藏附件,其他的客户端就很难说了。
  4. 合并各个部件的时候,注意顺序不要弄错。

四、完整代码

# 项目:标准程序库
# 模块:发送电子邮件
# 作者:黄涛
# License:GPL
# Email:huangtao.sh@icloud.com
# 创建:2016-10-26 10:25
# 修改:2019-02-14 15:54 对部分代码进行修订


from email.mime.text import MIMEText, Charset
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.utils import getaddresses, formataddr
from email import encoders
from pathlib import Path
import smtplib
import io


def combine(type_: str = 'mixed', *subparts):
    '''合并邮件的各个部分,
    type_可以为以下几个值:
    related:     合并正文和内嵌附件;
    alternative: 合并纯文本正文和超文本正文;
    mixed:       合并正文和附件
    '''
    return MIMEMultipart(type_, _subparts=subparts)


def encode(filename: str) -> str:
    '''对文件进行编码'''
    return Charset('utf8').header_encode(filename) \
        if any(map(lambda x: ord(x) > 127, filename)) else filename


def fmtaddr(addrs: str) -> str:
    '''格式化邮件地址'''
    return ';'.join(map(formataddr, getaddresses([addrs])))


class MailClient(smtplib.SMTP):
    '''构造邮件客户端,使用方法如下:
       client=MailClient(host,user,passwd)
    '''
    config = {}   # 用于配置发送邮件的想着参数,如:host,user,passwd

    def __init__(self, host=None, user=None, passwd=None, *args, **kw):
        host = host or self.config.get('host')
        user = user or self.config.get('user')
        passwd = passwd or self.config.get('passwd')
        super().__init__(host, *args, **kw)
        self.login(user, passwd)

    def Mail(self, *args, **kw):
        m = Mail(*args, client=self, **kw)
        if m.Sender is None:
            m.Sender = self.config.get('sender')
        return m


# 常见附件类型
MIMETYPE = (
    ('.aiff', 'audio/x-aiff'),
    ('.asf', 'video/x-ms-asf'),
    ('.asr', 'video/x-ms-asf'),
    ('.asx', 'video/x-ms-asf'),
    ('.au', 'audio/basic'),
    ('.avi', 'video/x-msvideo'),
    ('.bas', 'text/plain'),
    ('.bin', 'application/octet-stream'),
    ('.bmp', 'image/bmp'),
    ('.c', 'text/plain'),
    ('.css', 'text/css'),
    ('.doc', 'application/msword'),
    ('.docx', 'application/msword'),
    ('.exe', 'application/octet-stream'),
    ('.gif', 'image/gif'),
    ('.gz', 'application/x-gzip'),
    ('.h', 'text/plain'),
    ('.htm', 'text/html'),
    ('.html', 'text/html'),
    ('.ico', 'image/x-icon'),
    ('.jfif', 'image/pipeg'),
    ('.jpe', 'image/jpeg'),
    ('.jpeg', 'image/jpeg'),
    ('.jpg', 'image/jpeg'),
    ('.js', 'application/x-javascript'),
    ('.latex', 'application/x-latex'),
    ('.lha', 'application/octet-stream'),
    ('.m3u', 'audio/x-mpegurl'),
    ('.mid', 'audio/mid'),
    ('.mov', 'video/quicktime'),
    ('.movie', 'video/x-sgi-movie'),
    ('.mp2', 'video/mpeg'),
    ('.mp3', 'audio/mpeg'),
    ('.mpa', 'video/mpeg'),
    ('.mpe', 'video/mpeg'),
    ('.mpeg', 'video/mpeg'),
    ('.mpg', 'video/mpeg'),
    ('.mpv2', 'video/mpeg'),
    ('.pdf', 'application/pdf'),
    ('.png', 'image/png'),
    ('.ppm', 'image/x-portable-pixmap'),
    ('.pps', 'application/vnd.ms-powerpoint'),
    ('.ppt', 'application/vnd.ms-powerpoint'),
    ('.ps', 'application/postscript'),
    ('.pub', 'application/x-mspublisher'),
    ('.qt', 'video/quicktime'),
    ('.rtf', 'application/rtf'),
    ('.rtx', 'text/richtext'),
    ('.sh', 'application/x-sh'),
    ('.svg', 'image/svg+xml'),
    ('.tar', 'application/x-tar'),
    ('.tex', 'application/x-tex'),
    ('.tgz', 'application/x-compressed'),
    ('.tif', 'image/tiff'),
    ('.tiff', 'image/tiff'),
    ('.tr', 'application/x-troff'),
    ('.trm', 'application/x-msterminal'),
    ('.tsv', 'text/tab-separated-values'),
    ('.txt', 'text/plain'),
    ('.ustar', 'application/x-ustar'),
    ('.wav', 'audio/x-wav'),
    ('.xlm', 'application/vnd.ms-excel'),
    ('.xls', 'application/vnd.ms-excel'),
    ('.xlt', 'application/vnd.ms-excel'),
    ('.xlw', 'application/vnd.ms-excel'),
    ('.z', 'application/x-compress'),
    ('.zip', 'application/zip'),
)

MIMETYPES = {suffix: type_ for suffix, type_ in MIMETYPE}


class Mail:
    '''创建电子邮件,使用方法如下:
    mail=Mail(sender,to,subject,body,cc,bcc)
    添加附件:
    mail.attach(filename,cid=None,writer=None)
    发送邮件:
    mail.post(client)
    '''

    def __init__(self, sender=None, to=None, subject=None, body=None,
                 cc=None, bcc=None, client=None):
        '''初始化邮件'''
        self.attachments = []
        self.inline_attachments = []
        self.body = body
        self.Subject = subject
        self.To = to
        self.Sender = sender
        self.Cc = cc
        self.Bcc = bcc
        self.client = client

    @property
    def message(self):
        '''获取邮件的MESSAGE属性'''
        body = self.body
        subtype = 'html' if body.startswith('<html>') else 'plain'  # 设置正文类型
        msg = MIMEText(body, subtype, 'utf-8')             # 构建邮件正文
        if self.inline_attachments:                        # 合并内嵌附件
            msg = combine('related', msg, *self.inline_attachments)
        if self.attachments:                               # 合并附件
            msg = combine('mixed', msg, *self.attachments)
        msg.add_header('Subject', self.Subject)            # 设置标题
        for name in ('Sender', 'To', 'Cc', 'Bcc'):         # 设置收件人及发件人
            val = getattr(self, name)
            if val:
                msg.add_header(name, fmtaddr(val))
        return msg

    def __str__(self):
        return self.message.as_string()

    def add_fp(self, fp, filename):
        self.attach(filename, writer=fp)

    def attach(self, filename, cid=None, writer=None):    # 添加附件
        '''
        filename: 文件名
        cid:     内嵌资源编号,如设置则不出现在附件列表中
        writer:   内容生成,如设置,则通过 writer(fn)的形式来获取数据
        '''
        file = Path(filename)
        msg = MIMEBase(*MIMETYPES.get(
            file.lsuffix, 'application/octet-stream').split('/'))
        if callable(writer):
            with io.BytesIO() as fp:
                writer(fp)
                fp.seek(0)
                data = fp.read()
        else:
            data = file.read_bytes()
        msg.set_payload(data)
        encoders.encode_base64(msg)
        msg.add_header('Content-Disposition', 'inline' if cid else 'attachment',
                       filename=encode(file.name))
        if cid:
            msg.add_header('Content-ID', f'<{cid}>')
            msg.add_header('X-Attachment-Id', cid)
            self.inline_attachments.append(msg)
        else:
            self.attachments.append(msg)

推荐阅读更多精彩内容