使用 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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容