Flask 入门 - 电子邮件

很多类型的应用程序都需要在特定事件发生时(例如注册)提醒用户, 而常用的通信方法是电子邮件。

使用 Flask-Mail 提供电子邮件支持

1
pip install flask-mail

Flask-Mail SMTP服务器的配置

配  置 默认值 说  明
MAIL_SERVER localhost 电子邮件服务器的主机名或 IP 地址
MAIL_PORT 25 电子邮件服务器的端口
MAIL_USE_TLS False 启用传输层安全(Transport Layer Security,TLS)协议
MAIL_USE_SSL False 启用安全套接层(Secure Sockets Layer,SSL)协议
MAIL_USERNAME None 邮件账户的用户名
MAIL_PASSWORD None 邮件账户的密码

在开发过程中, 如果连接到外部 SMTP 服务器, 则可能更方便。下面使用新浪邮箱账户发送电子邮件:

示例 6-1 hello.py:配置 Flask-Mail 使用新浪邮箱

1
2
3
4
5
6
7
import os
# ...
app.config['MAIL_SERVER'] = 'smtp.sina.com'
app.config['MAIL_PORT'] = 25
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 hello.py:初始化 Flask-Mail

1
2
from flask_mail import Mail
mail = Mail(app)

保存电子邮件服务器用户名和密码的两个环境变量要在环境中定义。如果你在 Linux 或 Mac OS X 中使用 bash, 则这样设置这两个变量:

1
2
(venv) $ export MAIL_USERNAME="ohmydog"
(venv) $ export MAIL_PASSWORD="ohmydog@sina.com"

Windows 系统下可使用 set 命令设置环境变量:

1
2
(venv) $ set MAIL_USERNAME="ohmydog"
(venv) $ set MAIL_PASSWORD="ohmydog@sina.com"

在 Python shell 中发送电子邮件

你可以打开一个 shell 会话, 发送一封测试邮件, 以检查配置是否正确:

1
2
3
4
5
6
7
8
(venv) $ python hello.py shell
>>> from flask_mail import Message
>>> from hello import mail
>>> msg = Message('test subject', sender='xiaoxigu@sina.com', recipients=['baishe@sina.com'])
>>> msg.body = 'text body'
>>> msg.html = '<b>HTML</b> body'
>>> with app.app_context():
mail.send(msg)

注意, Flask-Mail 中的 send() 函数使用 current_app, 因此要在激活的程序上下文中执行。

在程序中集成发送电子邮件功能

为了避免每次都手动编写电子邮件消息, 我们最好把程序发送电子邮件的通用部分抽象出来, 定义为一个函数。这样, 还能使用 Jinja2 模板渲染邮件正文:

示例 6-3 hello.py:电子邮件支持

1
2
3
4
5
6
7
8
9
10
11
from flask_mail import Message

app.config['FLASK_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASK_MAIL_SENDER'] = 'Flasky Admin <flasky@sina.com>'

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

这个函数用到了两个程序特定配置项,分别定义邮件主题的前缀和发件人的地址。 send_email 函数的参数分别为收件人地址、主题、渲染邮件正文的模板和关键字参数列表。指定模板时不能包含扩展名,这样才能使用两个模板分别渲染纯文本正文和富文本正文。调用者将关键字参数传给 render_template() 函数,以便在模板中使用,进而生成电子邮件正文。

index() 视图函数很容易被扩展,这样每当表单接收新名字时,程序都会给管理员发送一封电子邮件。修改方法如示例 6-4 所示。

示例 6-4 hello.py:电子邮件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ...
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(usernme=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))

电子邮件的收件人保存在环境变量 FLASKY_ADMIN 中,在程序启动过程中,它会加载到一个同名配置变量中。我们要创建两个模板文件,分别用于渲染纯文本和 HTML 版本的邮件正文。这两个模板文件都保存在 templates 文件夹下的 mail 子文件夹中,以便和普通模板区分开来。电子邮件的模板中要有一个模板参数是用户,因此调用 send_mail() 函数时要以关键字参数的形式传入用户。

除了前面提到的环境变量 MAIL_USERNAME 和 MAIL_PASSWORD 之外,这个版本的程序还需要使用环境变量 FLASKY_ADMIN 。Linux 和 Mac OS X 用户可使用下面的命令添加:

1
(venv) $ export FLASKY_ADMIN=<your-email-address>

对微软 Windows 用户来说,等价的命令是:

1
(venv) $ set FLASKY_ADMIN=<Gmail username>

设置好这些环境变量后,我们就可以测试程序了。每次你在表单中填写新名字时,管理员都会收到一封电子邮件。

异步发送电子邮件

如果你发送了几封测试邮件,可能会注意到 mail.send() 函数在发送电子邮件时停滞了几秒钟,在这个过程中浏览器就像无响应一样。为了避免处理请求过程中不必要的延迟,我们可以把发送电子邮件的函数移到后台线程中。修改方法如示例 6-5 所示。

示例 6-5 hello.py:异步发送电子邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from threading import Thread

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

def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASK_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASK_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() 人工创建。

现在再运行程序,你会发现程序流畅多了。不过要记住,程序要发送大量电子邮件时,使用专门发送电子邮件的作业要比给每封邮件都新建一个线程更合适。例如,我们可以把执行 send_async_email() 函数的操作发给 Celery 任务队列。