0%

Flask——电子邮件

很多类型的应用程序需要在特定事件发生时提醒用户,而常用的通信方法是电子邮件。虽然Python标准库中的smtplib包可用在Flask程序中发送电子邮件,但包装了smtplib的Flask-Mail扩展能更好地和Flask集成。

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


使用pip安装Flask-Mail:

1
(env) [root@server1 myproject]# pip install flask-mail

Flask-Mail的初始化方法如下:

1
2
from flask.ext.mail import Mail
mail = Mail(app)

Flask-Mail连接到简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)服务器,并把邮件交给这个服务器发送。如果不进行配置,Flask-Mail会连接localhost上的端口25,无需验证即可发送电子邮件。下表列出了可用来设置SMTP服务器的配置。

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服务器,则可能更方便。下面这个例子展示了如何配置程序,以便使用QQ邮箱账户发送电子邮件。

run.py:配置Flask-Mail使用QQ邮箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask
from flask.ext.mail import Mail
from flask.ext.script import Manager
import os

app = Flask(__name__)
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')

mail = Mail(app)
manager = Manager(app)

if __name__ == "__main__":
manager.run()

注意:千万不要把账户密令直接写入脚本,特别是当你计划开源自己的作品时。为了保护账户信息,你需要让脚本从环境中导入敏感信息。

保护电子邮件服务器用户名和密码的两个环境变量要在环境中定义。如果你在Linux或Mac OS X中使用bash,那么可以按照下面的方式设定这两个变量:

1
2
(env) [root@server1 myproject]# export MAIL_USERNAME=<qq-mail username>
(env) [root@server1 myproject]# export MAIL_PASSWORD=<qq-mail password>

微软Windows用户可按照下面的方式设定环境变量:

1
2
(env) $ set MAIL_USERNAME=<qq-mail username>
(env) $ set MAIL_PASSWORD=<qq-mail password>

在Python shell中发送电子邮件


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

注意:发送测试邮件前,请先在你的邮箱设置中打开POP3/SMTP服务,获取授权码,具体步骤请参考http://service.mail.qq.com/cgi-bin/help?subtype=1&&no=1001256&&id=28

1
2
3
4
5
6
7
8
(env) [root@server1 myproject]# python run.py shell
>>> from flask.ext.mail import Message
>>> from run import mail
>>> msg = Message('Hello Koen',sender='you@example.com',recipients=['you@example.com'])
>>> msg.body = 'Hello Koen'
>>> msg.html = '<h1>Hello Koen!</h1>'
>>> with app.app_context():
... mail.send(msg)

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

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


为了避免每次都手动编写电子邮件消息,我们最好把程序发送电子邮件的通用部分抽象出来,定义成一个函数。这么做还有个好处,即该函数可以使用Jinja2模板渲染邮件正文,灵活性极高。

1
2
3
4
5
6
7
8
9
10
11
from flask.ext.mail import Message

app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'

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()函数的参数分别为收件人地址、主题、渲染邮件正文的模板和关键字参数列表。指定模板时不要包含扩展名,这样才能使用两个模板分别渲染纯文本正文和富文本正文。调用者将关键字参数传给render_template()函数,以便在模板中使用,进而生成电子邮件正文。

index()视图函数很容易被扩展,这样每当表单接收新名字时,程序都会给管理员发送一封电子邮件。

run.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(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
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_email()函数时要以关键字参数的形式传入用户。

完整的run.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import os
from flask import Flask, render_template, session, redirect, url_for
from flask.ext.script import Manager, Shell
from flask.ext.bootstrap import Bootstrap
from flask.ext.moment import Moment
from flask.ext.wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.migrate import Migrate, MigrateCommand
from flask.ext.mail import Mail, Message

basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')

manager = Manager(app)
bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)

class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role', lazy='dynamic')

def __repr__(self):
return '<Role %r>' % self.name

class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

def __repr__(self):
return '<User %r>' % self.username

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)

class NameForm(Form):
name = StringField('What is your name?', validators=[Required()])
submit = SubmitField('Submit')

def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

@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
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'),
known=session.get('known', False))


if __name__ == '__main__':
manager.run()

templates/base.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}

{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{ message }}
</div>
{% endfor %}

{% block page_content %}{% endblock %}
</div>
{% endblock %}

{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}

templates/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
{% if not known %}
<p>Pleased to meet you!</p>
{% else %}
<p>Happy to see you again!</p>
{% endif %}
</div>
{{ wtf.quick_form(form) }}
{% endblock %}

templates/mail/new_user.txt

1
User {{ user.username }} has joined.

templates/mail/new_user.html

1
User <b>{{ user.username }}</b> has joined.

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

1
(env) [root@server1 myproject]# export FLASKY_ADMIN=<your-email-address>

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

1
(env) $ set FLASKY_ADMIN=<your-email-address>

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

异步发送电子邮件

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

run.py:异步发送电子邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()人工创建。

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

完整的run.py如下,其他模板文件不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import os
from threading import Thread
from flask import Flask, render_template, session, redirect, url_for
from flask.ext.script import Manager, Shell
from flask.ext.bootstrap import Bootstrap
from flask.ext.moment import Moment
from flask.ext.wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.migrate import Migrate, MigrateCommand
from flask.ext.mail import Mail, Message

basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = '379148058@qq.com'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')

manager = Manager(app)
bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)

class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role', lazy='dynamic')

def __repr__(self):
return '<Role %r>' % self.name

class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

def __repr__(self):
return '<User %r>' % self.username

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

class NameForm(Form):
name = StringField('What is your name?', validators=[Required()])
submit = SubmitField('Submit')

def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

@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
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'),
known=session.get('known', False))

if __name__ == '__main__':
manager.run()

至此,我们已经完成了对大多数Web程序所需功能的概述。

参考书籍:《Flask Web开发——基于Python的Web应用开发实战》


- - - - - - - - - 本文结束啦感谢您阅读 - - - - - - - - -