Cycoe@Home

利用 Python 完成自动化群发邮件

最近我们 Campus TMC 要过九周年生日,因此要给以前的老会员发邮件邀请函。本来打算一 个一个手动发,后来一看有将近 100 个。本着 Coding for everything 的准则,尝试用 Python 来解决。

Python 中已经实现了 stmp 的相关接口在 smtplib 模块中,邮件结构相关的接口在 email 模块中。

1 生成通讯录

在批量发送邮件之前,一定要有一份结构化的通讯录,不管是用 list 也好,用 dict 存储也好。此处,我是使用 xlrd 模块从 Excel 表格中读取数据,并将其存储在 dict 中。对于邮件来说,收件人的名字可能会出现重名,但邮件地址应该是唯一的,因此选用邮件地址作为字典的键。

def read_contacts(contact_file):
    """ 讀取聯繫人
    """
    contacts = {}

    data = xlrd.open_workbook(contact_file)
    sheet = data.sheet_by_index(0)

    for row in range(sheet.nrows):
        email = sheet.cell(row, 6).value.strip()
        if email == '':
            continue

        if email.find('/'):
            emails = email.split('/')
            for email in emails:
                contacts[email] = {}
                contacts[email][ZH] = sheet.cell(row, 3).value.strip()
                contacts[email][EN] = sheet.cell(row, 1).value.strip()
        else:
            contacts[email] = {}
            contacts[email][ZH] = sheet.cell(row, 3).value.strip()
            contacts[email][EN] = sheet.cell(row, 1).value.strip()

    del sheet
    del data
    return contacts

生成的字典结构为 {key_address: {'zh': value_zh_name, 'en': value_en_name}, ...}。

2 定义邮件内容

第一步是生成邮件对象,代码示例如下:

from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.utils import formataddr

# 定义邮件的主题与内容
SUBJECT = 'My best friend {:name}, I sincerely invite you to participate our anniversary activity'
# 邮件内容支持 HTML
CONTENT = """
My best friend {:name},
<p>I sincerely invite you to participate our anniversary activity.</p>
<p>Here is our post.</p>
<div>
<div align="center"><img src="cid:post"></div>
"""

# 发件人的信息
SENDER_NAME = 'Cycoe'
SENDER_ADDR = 'cycoe@163.com'

contacts = read_contacts(PATH_OF_CONTACTS)

for email in contacts.keys():
    name = contacts[email][EN]
    # 生成邮件对象
    message = MIMEMultipart()
    # 设置主题、发信人、收信人
    message['Subject'] = Header(SUBJECT.format(name))
    message['From'] = formataddr((SENDER_NAME, SENDER_ADDR))
    message['To'] = formataddr((name, email))
    # 内容有点特殊,对于 MIMEMultipart 类型的邮件,
    # 内容是作为 attachment 添加到 message 中
    content = MIMEText(CONTENT.format(name), _subtype='html', _charset='utf-8')
    message.attach(content)

    # 加载附件,POST_PATH 是附件在本机上的路径
    with open(POST_PATH, 'rb') as fp:
        # 此处的 filename 参数是附件在邮件中显示的名字
        post = MIMEImage(fp.read(), _subtype='png', filename='post.png')
        post.add_header('Content-Disposition', 'attachment', filename='post.png')
        # 该文件头非常重要,Content-ID 一定要对应上面内容中的 cid
        # 这样图片就会被引用到正文中
        post.add_header('Content-ID', '<post>')
        message.attach(post)

3 发送邮件

Python 中使用 smtplib 来处理 smtp 协议,示例代码如下:

import smtplib

HOST = 'smtp.163.com'
PORT = 25
SENDER_ADDR = 'cycoe@163.com'
PASSWORD = '*********'


def send_mail(message, email):
    server = smtplib.SMTP()
    # 连接服务器
    try:
        server.connect(HOST, PORT)
    except smtplib.SMTPConnectError as e:
        print(e)
        return False

    # 登陆
    try:
        server.login(SENDER, PASSWORD)
    except smtplib.SMTPAuthenticationError as e:
        print(e)
        return False

    success = False
    # 发送邮件
    try:
        server.sendmail(SENDER_ADDR, [email], msg.as_string())
        print('郵件發送成功!')
        success = True
    except smtplib.SMTPException as e:
        print('郵件發送失敗!')
    finally:
        del message
        server.quit()

    return success

此处有一点需要注意,不要 connect 一次服务器循环发多封邮件,而是每次 connect 发送一封邮件就 quit 重新 connect。否则会被网易服务器当作垃圾邮件而连接错误。

Author: Cycoe (cycoejoo@163.com)
Date: <2020-03-30 Mon 13:22>
Generator: Emacs 28.0.50 (Org mode 9.3)
Built: <2020-05-21 Thu 20:09>