第6章:邮件-Flask Web 开发(第2版)

许多应用程序都需要在某些事件发生时通知用户,最常用的通信方式是电子邮件。本章你将学习到如何在 Flask 应用中发送邮件。

使用 Flask-Mail 对邮件进行支持

Python 标准库里的 smtplib 包可以用来发送邮件,Flask-Mail 扩展对其进行包装,并将其很好地集成到 Flask 中。通过 pip 来安装该扩展:

(vent) $ pip install flask-mail

该扩展连接到简单邮件传输协议(SMTP)服务器并将邮件传递给它以进行发送。如果没有给出配置,Flask-Mail 连接到本机的 25 端口来发送邮件,这个过程不进行身份验证。下表列出了该扩展需要配置的一些键。

默认值 说明
MAIL_SERVER localhost 邮件服务器的主机名或IP地址
MAIL_PORT 25 邮件服务器的端口
MAIL_USE_TLS False 是否启用 TLS
MAIL_USE_SSL False 是否启用 SSL
MAIL_USERNAME None 邮箱账号的用户名
MAIL_PASSWORD None 邮件账号的密码

在开发过程中,连接到外部 SMTP 服务器可能会更方便。例如,示例6-1显示了如何配置应用程序通过 Google Gmail 帐户发送电子邮件。

# 示例 6-1. hello.py: Flask-Mail 的 Gmail 配置
import os
# ...
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')

Flask-Mail 的初始化,如示例 6-2 所示。

# 示例 6-2. hello.py: Flask-Mail 初始化
from flask_mail import Mail
mail = Mail(app)

需要对邮件的用户和密码设置环境变量。在 Linux/MacOS,使用下面的命令进行设置:

(venv) $ export MAIL_USERNAME=<Gmail username>
(venv) $ export MAIL_PASSWORD=<Gmail password>

Windows 这样设置:

(venv) $ set MAIL_USERNAME=<Gmail username>
(venv) $ set MAIL_PASSWORD=<Gmail password>

在 Python Shell 中发送邮件

为了测试邮箱配置,可以启动一个 Shell 会话,并发送一封测试邮件。(将 [email protected] 替换为你的邮箱地址):

(venv) $ flask shell
>>> from flask_mail import Message
>>> from hello import mail
>>> msg = Message('test email', sender='[email protected]',
...     recipients=['[email protected]'])
>>> msg.body = 'This is the plain text body'
>>> msg.html = 'This is the <b>HTML</b> body'
>>> with app.app_context():
...     mail.send(msg)
...

注意,Flask-Mail 的 send() 方法需要使用 current_app,所以必须在一个激活的应用程序上下文中执行。

将邮件集成到应用程序中

为了避免每次手动创建电子邮件,将应用程序的电子邮件发送功能的公共部分抽象为函数是一个好主意。额外地,这个函数可以使用 Jinja2 模板对邮件正文进行渲染,以获取最大的灵活性。

# 示例 6-3. hello.py: 邮件支持
from flask_mail import Message

app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <[email protected]>'

def send_email(to, subject, template, **kwargs):
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    mail.send(msg)

该函数依赖于两个应用程序的配置键,这两个键定义了邮件主题的前缀字符串以及将用作发件人的邮箱地址。send_email() 函数需要目标地址、邮件主题、邮件正文的模板以及一些关键字参数列表。模板名不需要加上扩展名,这样就可以使用纯文本和HTML两种模式的模板了。将调用者传入的关键字参数传递给模板,以便在模板中使用这些变量来生成邮件正文。index() 视图函数可以很容易地扩展为,在表单收到新名称时向管理员发送电子邮件。

# 示例 6-4. hello.py: 邮件范例
# ...
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
# ...
@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username=form.name.data)
            db.session.add(user)
            session['known'] = False
            if app.config['FLASKY_ADMIN']:
                send_email(app.config['FLASKY_ADMIN'], 'New User',
                           'mail/new_user', user=user)
        else:
            session['known'] = True
        session['name'] = form.name.data
        form.name.data = ''
        return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name'),
                           known=session.get('known', False))

邮件正文两种格式的模板放在 templates/email 目录下。

如果你从 Github 克隆了示例代码,可以使用 git checkout 6a 签出本示例。

异步发送邮件

在发送邮件时,mail.send() 函数会让浏览器进入几秒钟“假死”状态。为了解决这个问题,可以把发送邮件函数移到后台线程中。

# 示例 6-5. hello.py: 异步发送邮件
from threading import Thread

def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

def send_email(to, subject, template, **kwargs):
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr

该实现突显了一个有趣的问题。许多 Flask 扩展都假定运行在已激活的应用程序上下文和/或请求上下文中。如前所述,Flask-Mail 的 send() 函数需要使用 current_app,因此它需要应用程序上下文处于活动状态。但是,由于上下文与线程相关联,因此当 mail.send() 函数在不同的线程中执行时,需要使用 app.app_context() 人为地创建应用程序上下文。app 实例通过参数传递给这个线程,所以上下文能够顺利创建。

如果你从 Github 克隆了示例代码,可以使用 git checkout 6b 签出本示例。

如果您现在运行应用程序,您会注意到它的响应速度更快,但请记住,对于发送大量电子邮件的应用程序,执行专门用于发送电子邮件的作业比为每封电子邮件启动新线程更合适。例如,可以将 send_async_email() 函数发送到一个 Celery 任务队列中执行。

本章完成了大多数 Web 应用程序必备功能的概述。现在的问题是,hello.py 脚本越来越臃肿了,开始变得难以使用。下一章,你将学会如何构建一个大型应用。

本文并非《Flask Web Development(2nd edition)》的全文翻译。而只是在阅读该书时,对自认为重点的内容进行记录以及思考。由于水平有限,本文所述内容难免出现不足或错误。请通过购买正版图书来进行更系统地学习,原书地址。本站将依照本系列文章录制视频教程,届时本文未作记录的部分以及项目实际开发中所需要的知识都会在视频教程中进行补充。

flask Flask Web Development(2nd edition) 2018-03-30 21:40 2872600