在Python的Flask架構下收發電子郵件的教程
這篇文章主要介紹了在Python的Flask架構下收發電子郵件的教程,主要用到了Flask中的Flask-mail工具,需要的朋友可以參考下
簡述
在大多數此類教程中都會不遺餘力的介紹如何使用資料庫。今天我們對資料庫暫且不表,而是來關注另一個在web應用中很重要的特性:如何推送郵件給使用者。
在某個輕量級應用中我們可能會添加一個如下的郵件服務功能:當使用者有了新的粉絲後,我們發送一封郵件通知使用者。有很多方法可以實現這個特性,而我們希望提供出一種可複用的通用架構來處理。
Flask-Mail介紹
對於我們來說是幸運的,現在已經有很多外部外掛程式來處理郵件,雖說不能百分百按照我們的想法去處理,但已經相當接近了。
在虛擬環境中安裝 Flask-Mail是相當簡單的。Windows以外的使用者可以利用以下命令來安裝:
?
1 |
flask/bin/pip install flask-mail |
Windows使用者的安裝稍有不同,因為Flask-Mail所使用的一些模組不能再Windows系統上運行,你可以使用以下命令:
?
1 |
flask\Scripts\pip install --no-deps lamson chardet flask-mail |
配置:
回想一下前文中單元測試部分的案例,我們通過添加配置支援了一個這樣的功能:當應用的某個版本測試出錯時可以郵件通知我們。從這個例子就可以看出如何配置使用郵件支援。
再次提醒大家,我們需要設定兩個方面的內容:
郵件伺服器資訊
使用者郵箱地址
如下正是前文中所用到的配置
?
1 2 3 4 5 6 7 8 9 10 |
# email server MAIL_SERVER = 'your.mailserver.com' MAIL_PORT = 25 MAIL_USE_TLS = False MAIL_USE_SSL = False MAIL_USERNAME = 'you' MAIL_PASSWORD = 'your-password' # administrator list ADMINS = ['you@example.com'] |
其中並沒有設定切實可用的郵件伺服器和郵箱。現在我們通過一個例子來看如何使用gmail郵箱賬戶來發送郵件:
?
1 2 3 4 5 6 7 8 9 10 |
# email server MAIL_SERVER = 'smtp.googlemail.com' MAIL_PORT = 465 MAIL_USE_TLS = False MAIL_USE_SSL = True MAIL_USERNAME = 'your-gmail-username' MAIL_PASSWORD = 'your-gmail-password' # administrator list ADMINS = ['your-gmail-username@gmail.com'] |
另外我們也可以初始化一個Mail對象來串連SMTP郵件伺服器,發送郵件:
?
1 2 |
from flask.ext.mail import Mail mail = Mail(app) |
發個郵件試試!
為了瞭解flask-mail如何工作的,我們可以從命令列發一封郵件看看。進入python shell並執行如下的指令碼:
?
1 2 3 4 5 6 7 |
>>> from flask.ext.mail import Message >>> from app import mail >>> from config import ADMINS >>> msg = Message('test subject', sender = ADMINS[0], recipients = ADMINS) >>> msg.body = 'text body' >>> msg.html = '<b>HTML</b> body' >>> mail.send(msg) |
上面這段代碼會根據inconfig.py中配置的郵箱地址清單,以首個郵箱作為寄件者給所有郵箱發送一封郵件。郵件內容會以文本和html兩種格式呈現,而你能看到哪種格式取決於你的郵件用戶端。
多麼簡單小巧!你完全可以現在就把它整合到你的應用中。
郵件架構
我們現在可以編寫一個協助函數來發送郵件。這是以上測試中一個通用版的測試。我們把這個函數放進一個新的原檔案中用作郵件支援(fileapp/emails.py):
?
1 2 3 4 5 6 7 8 |
from flask.ext.mail import Message from app import mail def send_email(subject, sender, recipients, text_body, html_body): msg = Message(subject, sender, recipients) msg.body = text_body msg.html = html_body mail.send(msg) |
Flask-Mail的郵件支援超出了我們目前的使用範圍,像密件副本和附件的功能並不會在此應用中得以使用。
Follower 提醒
現在,我們已經有了發郵件的基本架構,我們可以寫發送follower提醒的函數了 (fileapp/emails.py):
?
1 2 3 4 5 6 7 8 9 10 11 |
from flask import render_template from config import ADMINS def follower_notification(followed, follower): send_email("[microblog] %s is now following you!" % follower.nickname, ADMINS[0], [followed.email], render_template("follower_email.txt", user = followed, follower = follower), render_template("follower_email.html", user = followed, follower = follower)) |
你在這裡找到任何驚喜了嗎?我們的老朋友render_template函數有一次出現了。
如果你還記得,我們使用這個函數在views渲染模版. 就像在views裡寫html不好一樣,使用郵件模版是理想的選擇。我們要可能的將邏輯和表現分開,所以email模版也會和其它試圖模版一起放到在模版檔案夾裡.
所以,我們需要為follower提醒郵件寫純文字和網頁版的郵件模版,下面這個是純文字的版本 (fileapp/templates/follower_email.txt):
?
1 2 3 4 5 6 7 8 9 |
Dear {{user.nickname}}, {{follower.nickname}} is now a follower. Click on the following link to visit {{follower.nickname}}'s profile page: {{url_for("user", nickname = follower.nickname, _external = True)}} Regards, The microblog admin |
下面這個是網頁版的郵件,效果會更好(fileapp/templates/follower_email.html):
?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<p>Dear {{user.nickname}},</p> <p><a href="{{url_for("user", nickname = follower.nickname, _external = True)}}">{{follower.nickname}}</a> is now a follower.</p> <table> <tr valign="top"> <td><img src="{{follower.avatar(50)}}"></td> <td> <a href="{{url_for('user', nickname = follower.nickname, _external = True)}}">{{follower.nickname}}</a><br /> {{follower.about_me}} </td> </tr> </table> <p>Regards,</p> <p>The <code>microblog</code> admin</p> |
註解:模版中的url_for函數的 _external = True 參數的意義.預設情況下,url_for 函數產生url是相對我們的網域名稱的。例如,url_for("index")函數傳回值是/index, 但是,發郵件是我們想要http://localhost:5000/index這種url,email中是沒有網域內容的,所以,我們必須強制產生帶網域名稱的url,_external argument就是幹這個工作的。
最後一步是處理“follow”過程,即觸發寄件提醒時的視圖函數,(fileapp/views.py):
?
1 2 3 4 5 6 7 8 9 |
from emails import follower_notification @app.route('/follow/<nickname>') @login_required def follow(nickname): user = User.query.filter_by(nickname = nickname).first() # ... follower_notification(user, g.user) return redirect(url_for('user', nickname = nickname)) |
現在你可以建立兩個使用者(如果還沒有使用者的話)嘗試著用讓一個使用者follow另一個使用者,理解寄件提醒是怎樣工作的。
就是這樣嗎?我們做完了嗎?
我們可能心底裡很興奮完成了這項工作並且把寄件提醒功能同未自動完成清單裡刪除。
但是,如果你現在測試下應用,你會發現當你單擊follow連結的時候,頁面會2到3秒才會響應,瀏覽器才會重新整理,這在之前是沒有的。
發生了什麼?
問題是,Flask-Mail 使用同步模式寄送電子郵件。 從電子郵件發送開始,直到電子郵件交付後,給瀏覽器發回其響應,在整個過程中,Web伺服器會一直阻塞。如果我們試圖寄送電子郵件到一個伺服器是緩慢的,甚至更糟糕的,暫時處於離線狀態,你能想象會發生什麼嗎?很不好。
這是一個可怕的限制,寄送電子郵件應該是背景工作且不會干擾Web伺服器,讓我們看看我們如何能夠解決這個問題。
Python中執行非同步呼叫
我們想send_email 函數發完郵件後立即返回,需要讓發郵件移動到後台進程來非同步執行。
事實上python已經對非同步任務提供了支援,但實際上,還可以用其他的方式,比如線程和多進程模組也可以實現非同步任務。
每當我們需要發郵件的時候,啟動一個線程來處理,比啟動一個全新的進程節省資源。所以,讓我們將mail.send(msg)調用放到另一個線程中。(fileapp/emails.py):
?
1 2 3 4 5 6 7 8 9 10 11 |
from threading import Thread def send_async_email(msg): mail.send(msg) def send_email(subject, sender, recipients, text_body, html_body): msg = Message(subject, sender = sender, recipients = recipients) msg.body = text_body msg.html = html_body thr = threading.Thread(target = send_async_email, args = [msg]) thr.start() |
如果你測試‘follow‘函數,現在你會發現瀏覽器在發送郵件之前會重新整理。
所以,我們已經實現了非同步發送,但是,如果未來在別的需要非同步功能的地方難道我們還需要在實現一遍嗎?
過程都是一樣的,這樣就會在每一種情況下都有重複代碼,這樣非常不好。
我們可以通過 decorator改進代碼。使用裝飾器的代碼是這樣的:
?
1 2 3 4 5 6 7 8 9 10 11 |
from decorators import async @async def send_async_email(msg): mail.send(msg) def send_email(subject, sender, recipients, text_body, html_body): msg = Message(subject, sender = sender, recipients = recipients) msg.body = text_body msg.html = html_body send_async_email(msg) |
更好了,對不對?
實現這種方式的代碼實際上很簡單,建立一個新源檔案(fileapp/decorators.py):
?
1 2 3 4 5 6 7 |
from threading import Thread def async(f): def wrapper(*args, **kwargs): thr = Thread(target = f, args = args, kwargs = kwargs) thr.start() return wrapper |
現在我們對非同步任務建立了個有用的架構(framework), 我們可以說已經完成了!
僅僅作為一個練習,讓我們思考一下為什麼這個方法會看上去使用了進程而不是線程。我們並不想每當我們需要發送一封郵件時就有一個進程被啟動,所以我們能夠使用thePoolclass而不用themultiprocessingmodule。這個類會建立指定數量的進程(這些都是主進程的子進程),並且這些子進程會通過theapply_asyncmethod送到進程池,等待接受任務去工作。這可能對於一個繁忙的網站會是一個有趣的途徑,但是我們目前仍將維持現線上程的方式。