在Python的Flask框架下收發(fā)電子郵件的教程

字號:


    這篇文章主要介紹了在Python的Flask框架下收發(fā)電子郵件的教程,主要用到了Flask中的Flask-mail工具,需要的朋友可以參考下
    簡述
    在大多數(shù)此類教程中都會不遺余力的介紹如何使用數(shù)據(jù)庫。今天我們對數(shù)據(jù)庫暫且不表,而是來關(guān)注另一個在web應用中很重要的特性:如何推送郵件給用戶。
    在某個輕量級應用中我們可能會添加一個如下的郵件服務功能:當用戶有了新的粉絲后,我們發(fā)送一封郵件通知用戶。有很多方法可以實現(xiàn)這個特性,而我們希望提供出一種可復用的通用框架來處理。
    Flask-Mail介紹
    對于我們來說是幸運的,現(xiàn)在已經(jīng)有很多外部插件來處理郵件,雖說不能百分百按照我們的想法去處理,但已經(jīng)相當接近了。
    在虛擬環(huán)境中安裝 Flask-Mail是相當簡單的。Windows以外的用戶可以利用以下命令來安裝:
    flask/bin/pip install flask-mail
    Windows用戶的安裝稍有不同,因為Flask-Mail所使用的一些模塊不能再Windows系統(tǒng)上運行,你可以使用以下命令:
    flask\Scripts\pip install --no-deps lamson chardet flask-mail
    配置:
    回想一下前文中單元測試部分的案例,我們通過添加配置支持了一個這樣的功能:當應用的某個版本測試出錯時可以郵件通知我們。從這個例子就可以看出如何配置使用郵件支持。
    再次提醒大家,我們需要設置兩個方面的內(nèi)容:
    郵件服務器信息
    用戶郵箱地址
    如下正是前文中所用到的配置
    # 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']
    其中并沒有設置切實可用的郵件服務器和郵箱?,F(xiàn)在我們通過一個例子來看如何使用gmail郵箱賬戶來發(fā)送郵件:
    # 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郵件服務器,發(fā)送郵件:
    from flask.ext.mail import Mail
    mail = Mail(app)
    發(fā)個郵件試試!
    為了了解flask-mail如何工作的,我們可以從命令行發(fā)一封郵件看看。進入python shell并執(zhí)行如下的腳本:
    >>> 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)
    上面這段代碼會根據(jù)inconfig.py中配置的郵箱地址列表,以首個郵箱作為發(fā)件人給所有郵箱發(fā)送一封郵件。郵件內(nèi)容會以文本和html兩種格式呈現(xiàn),而你能看到哪種格式取決于你的郵件客戶端。
    多么簡單小巧!你完全可以現(xiàn)在就把它集成到你的應用中。
    郵件框架
    我們現(xiàn)在可以編寫一個幫助函數(shù)來發(fā)送郵件。這是以上測試中一個通用版的測試。我們把這個函數(shù)放進一個新的原文件中用作郵件支持(fileapp/emails.py):
    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 提醒
    現(xiàn)在,我們已經(jīng)有了發(fā)郵件的基本框架,我們可以寫發(fā)送follower提醒的函數(shù)了 (fileapp/emails.py):
    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函數(shù)有一次出現(xiàn)了。
    如果你還記得,我們使用這個函數(shù)在views渲染模版. 就像在views里寫html不好一樣,使用郵件模版是理想的選擇。我們要可能的將邏輯和表現(xiàn)分開,所以email模版也會和其它試圖模版一起放到在模版文件夾里.
    所以,我們需要為follower提醒郵件寫純文本和網(wǎng)頁版的郵件模版,下面這個是純文本的版本 (fileapp/templates/follower_email.txt):
    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
    下面這個是網(wǎng)頁版的郵件,效果會更好(fileapp/templates/follower_email.html):
    <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函數(shù)的 _external = True 參數(shù)的意義.默認情況下,url_for 函數(shù)生成url是相對我們的域名的。例如,url_for("index")函數(shù)返回值是/index, 但是,發(fā)郵件是我們想要這種url,email中是沒有域上下文的,所以,我們必須強制生成帶域名的url,_external argument就是干這個工作的。
    最后一步是處理“follow”過程,即觸發(fā)郵件提醒時的視圖函數(shù),(fileapp/views.py):
    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))
    現(xiàn)在你可以創(chuàng)建兩個用戶(如果還沒有用戶的話)嘗試著用讓一個用戶follow另一個用戶,理解郵件提醒是怎樣工作的。
    就是這樣嗎?我們做完了嗎?
    我們可能心底里很興奮完成了這項工作并且把郵件提醒功能同未完成列表里刪除。
    但是,如果你現(xiàn)在測試下應用,你會發(fā)現(xiàn)當你單擊follow鏈接的時候,頁面會2到3秒才會響應,瀏覽器才會刷新,這在之前是沒有的。
    發(fā)生了什么?
    問題是,F(xiàn)lask-Mail 使用同步模式發(fā)送電子郵件。 從電子郵件發(fā)送開始,直到電子郵件交付后,給瀏覽器發(fā)回其響應,在整個過程中,Web服務器會一直阻塞。如果我們試圖發(fā)送電子郵件到一個服務器是緩慢的,甚至更糟糕的,暫時處于脫機狀態(tài),你能想象會發(fā)生什么嗎?很不好。
    這是一個可怕的限制,發(fā)送電子郵件應該是后臺任務且不會干擾Web服務器,讓我們看看我們?nèi)绾文軌蚪鉀Q這個問題。
    Python中執(zhí)行異步調(diào)用
    我們想send_email 函數(shù)發(fā)完郵件后立即返回,需要讓發(fā)郵件移動到后臺進程來異步執(zhí)行。
    事實上python已經(jīng)對異步任務提供了支持,但實際上,還可以用其他的方式,比如線程和多進程模塊也可以實現(xiàn)異步任務。
    每當我們需要發(fā)郵件的時候,啟動一個線程來處理,比啟動一個全新的進程節(jié)省資源。所以,讓我們將mail.send(msg)調(diào)用放到另一個線程中。(fileapp/emails.py):
    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‘函數(shù),現(xiàn)在你會發(fā)現(xiàn)瀏覽器在發(fā)送郵件之前會刷新。
    所以,我們已經(jīng)實現(xiàn)了異步發(fā)送,但是,如果未來在別的需要異步功能的地方難道我們還需要在實現(xiàn)一遍嗎?
    過程都是一樣的,這樣就會在每一種情況下都有重復代碼,這樣非常不好。
    我們可以通過 decorator改進代碼。使用裝飾器的代碼是這樣的:
    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)
    更好了,對不對?
    實現(xiàn)這種方式的代碼實際上很簡單,創(chuàng)建一個新源文件(fileapp/decorators.py):
    from threading import Thread
    def async(f):
    def wrapper(*args, **kwargs):
    thr = Thread(target = f, args = args, kwargs = kwargs)
    thr.start()
    return wrapper
    現(xiàn)在我們對異步任務創(chuàng)建了個有用的框架(framework), 我們可以說已經(jīng)完成了!
    僅僅作為一個練習,讓我們思考一下為什么這個方法會看上去使用了進程而不是線程。我們并不想每當我們需要發(fā)送一封郵件時就有一個進程被啟動,所以我們能夠使用thePoolclass而不用themultiprocessingmodule。這個類會創(chuàng)建指定數(shù)量的進程(這些都是主進程的子進程),并且這些子進程會通過theapply_asyncmethod送到進程池,等待接受任務去工作。這可能對于一個繁忙的網(wǎng)站會是一個有趣的途徑,但是我們目前仍將維持現(xiàn)在線程的方式。