Flask学习之十一 邮件支持

英文博客地址:blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xi-email-support

中文翻译地址:http://www.pythondoc.com/flask-mega-tutorial/email.html

开源中国社区:http://www.oschina.net/translate/the-flask-mega-tutorial-part-xi-email-support

 

对于我们这个应用,我们可能想要有这样的功能,当有一个新的关注者的时候,发一封邮件给用户。

幸运地,Flask 已经存在处理邮件的扩展,尽管不是 100% 支持我们想要的功能,但是已经很好了。

在前面我们就已经安装了Flask-Mail,安装命令为:

flask/bin/pip install flask-mail

 

 

一、配置

需要设置两个方面的内容:

  • 邮件服务器信息
  • 用户邮箱地址
# email server
MAIL_SERVER = 'your.mailserver.com'
MAIL_PORT = 25
MAIL_USERNAME = None
MAIL_PASSWORD = None

# administrator list
ADMINS = ['you@example.com']

上面是之前的代码,其中并没有设置切实可用的邮件服务器和邮箱。现在我们通过一个例子来看如何使用gmail邮箱账户来发送邮件:

# email server
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')

# administrator list
ADMINS = ['your-gmail-username@gmail.com']

 Note: We are not going to enhance the server setup to allow those that require an encrypted communication through TLS or SSL.

(大概意思应该是:我们不会提高服务器设置,让那些需要加密通信的通过TLS或者SSL)

上面的代码是要求把用户名和密码写到环境变量中,再从环境变量中读取,这样会比较安全。写入环境变量代码:

 $ export MAIL_USERNAME=<Gmail username>
 $ export MAIL_PASSWORD=<Gmail password>

 不把信息写入环境变量,直接写入源码如下:

# 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 服务器并且发送邮件(文件 app/__init__.py):

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

 

然后试一下发邮件:

>>> from flask.ext.mail import Message
>>> from app import app, mail
>>> from config import ADMINS
>>> msg = Message('test subject', sender=ADMINS[0], recipients=ADMINS)
>>> msg.body = 'text body'
>>> msg.html = '<b>HTML</b> body'
>>> with app.app_context():
...     mail.send(msg)
....

然后发现忧桑地被墙了,FQ了可以上facebook,但还是发不了邮件,暂时找不到解决办法,错误代码:

socket.error: [Errno 101] Network is unreachable

使用gmail不成功我就改QQ邮箱试一下了。

# mail server settings
MAIL_SERVER = 'smtp.qq.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USE_SSL = False
MAIL_USERNAME = 'your-username'
MAIL_PASSWORD = 'your-password'
# administrator list
ADMINS = ['your-username@qq.com']

改成QQ邮箱出现错误:

raise SMTPAuthenticationError(code, resp)
smtplib.SMTPAuthenticationError: (454, 'Authentication failed, please open smtp flag first!')

原因是我没有开启POP3/SMTP服务

解决方法:在QQ邮箱的设置里面,找到账户-》POP3/IMAP/SMTP选择开启POP3/SMTP服务

smtplib.SMTPAuthenticationError: (535, 'Authentication failed')

 上面的错误是说我验证失败了,因为我刚才开启了POP3/SMTP服务,需要有一个独立密码,而我一开始用的是我的QQ密码。

改正了密码之后就可以了。

备注:因为我不断在修改配置文件,所以我就把命令行的代码直接放到一个test.py文件里面了:

#!flask/bin/python
from flask.ext.mail import Message
from app import app, mail
from config import ADMINS
msg = Message('test subject', sender=ADMINS[0], recipients=ADMINS)
msg.body = 'text body'
msg.html = '<b>HTML</b> body'
with app.app_context():
    mail.send(msg)

 

二、简单的邮件框架

我们现在编写一个辅助方法用于发送邮件。这是上面使用的测试代码通用的版本。我们将会把这个函数放入一个新文件,专门用于我们的应用程序的邮件支持(文件 app/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=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    mail.send(msg)

Flask-Mail的邮件支持还有像密件抄送和附件的功能,不过我们这里用不上。

 

三、Follower 提醒

现在我们已经有了发送邮件的基本框架,我们可以编写发送关注提醒的函数(文件 app/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渲染模板,email模版也会和其它试图模版一起放到在模版文件夹里

关注者提醒邮件的文本以及 HTML 版本的模板。这里是文本版本(文件 app/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

对于 HTML 版本,我们可能会做得更好些,甚至会显示出关注者的头像和用户信息(文件 app/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_external = True 参数。默认情况下,url_for 函数生成的 URLs 是与当前页面的域名相关的。例如,url_for(“index”) 返回值将会是 /index,但是在实际视图函数中返回的是 http://localhost:5000/index。在邮件中是不存在域名的内容,因此我们必须要生成完全的包含域名的 URLs,_external 参数就是为这个目的。

最后一步就是把发送邮件整合到实际的视图函数中(文件 app/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))

 效果,这是QQ邮箱发送到雅虎的:

 

四、在 Python 中异步调用

我们发现随着你不断地使用关注的链接,你可能会发现当你点击 follow 链接的时候,浏览器需要等到 2 到 3 秒的时间刷新页面,尽管邮件是正常地收到了。以前这可是瞬间完成的。

问题是,Flask-Mail 使用同步模式发送电子邮件。 从电子邮件发送开始,直到电子邮件交付后,给浏览器发回其响应,在整个过程中,Web服务器会一直阻塞。如果我们试图发送电子邮件到一个服务器是缓慢的,甚至更糟糕的,暂时处于脱机状态,你能想象会发生什么吗?很不好。

我们想send_email 函数发完邮件后立即返回,需要让发邮件移动到后台进程来异步执行。

事实上 Python 已经支持运行异步任务,而且有不止一种方式。threading 以及 multiprocessing 模块都可以达到这个目的。

每次我们需要发送邮件的时候启动一个进程的资源远远小于启动一个新的发送邮件的整个过程,因此把 mail.send(msg) 调用移入线程中(文件 app/emails.py):

from threading import Thread
from app import app

def send_async_email(app, msg):
    with app.app_context():
        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 = Thread(target=send_async_email, args=[app, msg])
    thr.start()

这里还剩下最后一个问题,如果将来我们需要实现其它异步的函数,我们需要为每一个实现异步功能的函数拷贝多线程的代码吗?这并不好。 我们可以通过实现一个 装饰器 来解决这个问题。

上面代码修改为:

from .decorators import async

@async
def send_async_email(app, msg):
    with app.app_context():
        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(app, msg)

然后把多线程代码放到一个新文件(文件 app/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

 

完了之后我因为多次测试出现了这种错误:

SMTPSenderRefused: (503, 'Error: need EHLO and AUTH first !', u'username@qq.com')

 提示错误服务器拒绝了发件人地址,然后我到现在还没找到解决办法,于是乎又换了163邮箱= =

备注,如果用163邮箱,配置要改成:

# mail server settings
MAIL_SERVER = 'smtp.163.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USE_SSL = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
# administrator list
ADMINS = ['your-username@163.com']

TLS要改成True,否则会出现以下错误:

smtplib.SMTPServerDisconnected: Connection unexpectedly closed

 

 

posted @ 2015-02-08 17:31  AminHuang  Views(1948)  Comments(0Edit  收藏  举报