很多類型的應(yīng)用程序都需要在特定事件發(fā)生時(shí)(例如注冊(cè))提醒用戶, 而常用的通信方法是電子郵件。
使用 Flask-Mail 提供電子郵件支持
pip install flask-mail
Flask-Mail SMTP服務(wù)器的配置
配 置 | 默認(rèn)值 | 說(shuō) 明 |
---|---|---|
MAIL_SERVER | localhost | 電子郵件服務(wù)器的主機(jī)名或 IP 地址 |
MAIL_PORT | 25 | 電子郵件服務(wù)器的端口 |
MAIL_USE_TLS | False | 啟用傳輸層安全(Transport Layer Security,TLS)協(xié)議 |
MAIL_USE_SSL | False | 啟用安全套接層(Secure Sockets Layer,SSL)協(xié)議 |
MAIL_USERNAME | None | 郵件賬戶的用戶名 |
MAIL_PASSWORD | None | 郵件賬戶的密碼 |
在開發(fā)過(guò)程中, 如果連接到外部 SMTP 服務(wù)器, 則可能更方便。下面使用新浪郵箱賬戶發(fā)送電子郵件:
示例 6-1 hello.py:配置 Flask-Mail 使用新浪郵箱
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')
千萬(wàn)不要把賬戶密碼直接寫入腳本, 特別是你計(jì)劃開源自己的作品時(shí)。為了保護(hù)賬戶信息, 你需要讓腳本從環(huán)境中導(dǎo)入敏感信息。
Flask-Mail 的初始化方法如下:
示例 6-2 hello.py:初始化 Flask-Mail
from flask_mail import Mail
mail = Mail(app)
保存電子郵件服務(wù)器用戶名和密碼的兩個(gè)環(huán)境變量要在環(huán)境中定義。如果你在 Linux 或 Mac OS X 中使用 bash, 則這樣設(shè)置這兩個(gè)變量:
(venv) $ export MAIL_USERNAME="ohmydog"
(venv) $ export MAIL_PASSWORD="ohmydog@sina.com"
Windows 系統(tǒng)下可使用 set 命令設(shè)置環(huán)境變量:
(venv) $ set MAIL_USERNAME="ohmydog"
(venv) $ set MAIL_PASSWORD="ohmydog@sina.com"
在 Python shell 中發(fā)送電子郵件
你可以打開一個(gè) shell 會(huì)話, 發(fā)送一封測(cè)試郵件, 以檢查配置是否正確:
(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() 函數(shù)使用 current_app
, 因此要在激活的程序上下文中執(zhí)行。
在程序中集成發(fā)送電子郵件功能
為了避免每次都手動(dòng)編寫電子郵件消息, 我們最好把程序發(fā)送電子郵件的通用部分抽象出來(lái), 定義為一個(gè)函數(shù)。這樣, 還能使用 Jinja2 模板渲染郵件正文:
示例 6-3 hello.py:電子郵件支持
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)
這個(gè)函數(shù)用到了兩個(gè)程序特定配置項(xiàng),分別定義郵件主題的前綴和發(fā)件人的地址。 send_email 函數(shù)的參數(shù)分別為收件人地址、主題、渲染郵件正文的模板和關(guān)鍵字參數(shù)列表。指定模板時(shí)不能包含擴(kuò)展名,這樣才能使用兩個(gè)模板分別渲染純文本正文和富文本正文。調(diào)用者將關(guān)鍵字參數(shù)傳給 render_template() 函數(shù),以便在模板中使用,進(jìn)而生成電子郵件正文。
index() 視圖函數(shù)很容易被擴(kuò)展,這樣每當(dāng)表單接收新名字時(shí),程序都會(huì)給管理員發(fā)送一封電子郵件。修改方法如示例 6-4 所示。
示例 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(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))
電子郵件的收件人保存在環(huán)境變量 FLASKY_ADMIN 中,在程序啟動(dòng)過(guò)程中,它會(huì)加載到一個(gè)同名配置變量中。我們要?jiǎng)?chuàng)建兩個(gè)模板文件,分別用于渲染純文本和 HTML 版本的郵件正文。這兩個(gè)模板文件都保存在 templates 文件夾下的 mail 子文件夾中,以便和普通模板區(qū)分開來(lái)。電子郵件的模板中要有一個(gè)模板參數(shù)是用戶,因此調(diào)用 send_mail() 函數(shù)時(shí)要以關(guān)鍵字參數(shù)的形式傳入用戶。
除了前面提到的環(huán)境變量 MAIL_USERNAME 和 MAIL_PASSWORD 之外,這個(gè)版本的程序還需要使用環(huán)境變量 FLASKY_ADMIN 。Linux 和 Mac OS X 用戶可使用下面的命令添加:
(venv) $ export FLASKY_ADMIN=<your-email-address>
對(duì)微軟 Windows 用戶來(lái)說(shuō),等價(jià)的命令是:
(venv) $ set FLASKY_ADMIN=<Gmail username>
設(shè)置好這些環(huán)境變量后,我們就可以測(cè)試程序了。每次你在表單中填寫新名字時(shí),管理員都會(huì)收到一封電子郵件。
異步發(fā)送電子郵件
如果你發(fā)送了幾封測(cè)試郵件,可能會(huì)注意到 mail.send() 函數(shù)在發(fā)送電子郵件時(shí)停滯了幾秒鐘,在這個(gè)過(guò)程中瀏覽器就像無(wú)響應(yīng)一樣。為了避免處理請(qǐng)求過(guò)程中不必要的延遲,我們可以把發(fā)送電子郵件的函數(shù)移到后臺(tái)線程中。修改方法如示例 6-5 所示。
示例 6-5 hello.py:異步發(fā)送電子郵件
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
上述實(shí)現(xiàn)涉及一個(gè)有趣的問(wèn)題。很多 Flask 擴(kuò)展都假設(shè)已經(jīng)存在激活的程序上下文和請(qǐng)求上下文。Flask-Mail 中的 send() 函數(shù)使用 current_app ,因此必須激活程序上下文。不過(guò),在不同線程中執(zhí)行 mail.send() 函數(shù)時(shí),程序上下文要使用 app.app_context() 人工創(chuàng)建。
現(xiàn)在再運(yùn)行程序,你會(huì)發(fā)現(xiàn)程序流暢多了。不過(guò)要記住,程序要發(fā)送大量電子郵件時(shí),使用專門發(fā)送電子郵件的作業(yè)要比給每封郵件都新建一個(gè)線程更合適。例如,我們可以把執(zhí)行 send_async_email() 函數(shù)的操作發(fā)給 Celery 任務(wù)隊(duì)列。