The Flask Mega-Tutorial, Part XI: Email Support
Posted by
on under(Great news! There is a new version of this tutorial!)
This is the eleventh article in the series in which I document my experience writing web applications in Python using the Flask microframework.
The goal of the tutorial series is to develop a decently featured microblogging application that demonstrating total lack of originality I have decided to call microblog
.
NOTE: This article was revised in September 2014 to be in sync with current versions of Python and Flask.
Here is an index of all the articles in the series that have been published to date:
- Part I: Hello, World!
- Part II: Templates
- Part III: Web Forms
- Part IV: Database
- Part V: User Logins
- Part VI: Profile Page And Avatars
- Part VII: Unit Testing
- Part VIII: Followers, Contacts And Friends
- Part IX: Pagination
- Part X: Full Text Search
- Part XI: Email Support (this article)
- Part XII: Facelift
- Part XIII: Dates and Times
- Part XIV: I18n and L10n
- Part XV: Ajax
- Part XVI: Debugging, Testing and Profiling
- Part XVII: Deployment on Linux (even on the Raspberry Pi!)
- Part XVIII: Deployment on the Heroku Cloud
Recap
In the most recent installments of this tutorial we've been looking at improvements that mostly had to do with our database.
Today we are letting our database rest for a bit, and instead we'll look at another important function that most web applications have: the ability to send emails to its users.
In our little microblog
application we are going to implement one email related function, we will send an email to a user each time he/she gets a new follower. There are several more ways in which email support can be useful, so we'll make sure we design a generic framework for sending emails that can be reused.
Configuration
Luckily for us, Flask already has an extension that handles email called Flask-Mail, and while it will not take us 100% of the way, it gets us pretty close.
Back when we looked at unit testing, we added configuration for Flask to send us an email should an error occur in the production version of our application. That same information is used for sending application related emails.
Just as a reminder, what we need is two pieces of information:
- the email server that will be used to send the emails, along with any required authentication
- the email address(es) of the admins
This is what we did in the previous article (file config.py
):
# email server
MAIL_SERVER = 'your.mailserver.com'
MAIL_PORT = 25
MAIL_USERNAME = None
MAIL_PASSWORD = None
# administrator list
ADMINS = ['you@example.com']
It goes without saying that you have enter the details of an actual email server and administrator above before the application can actually send emails. We are not going to enhance the server setup to allow those that require an encrypted communication through TLS or SSL. For example, if you want the application to send emails via your gmail account you would enter the following:
# 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 that the username and password are read from environment variables. You will need to set MAIL_USERNAME
and MAIL_PASSWORD
to your Gmail login credentials. Putting sensitive information in environment variables is safer than writing down the information on a source file.
We also need to initialize a Mail
object, as this will be the object that will connect to the SMTP server and send the emails for us (file app/__init__.py
):
from flask_mail import Mail
mail = Mail(app)
Let's send an email!
To learn how Flask-Mail works we'll just send an email from the command line. So let's fire up Python from our virtual environment and run the following:
>>> from flask_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)
....
The snippet of code above will send an email to the list of admins that are configured in config.py
. The sender will be the first admin in the list. The email will have text and HTML versions, so depending on how your email client is setup you may see one or the other. Note that we needed to create an app_context
to send the email. Recent releases of Flask-Mail require this. An application context is created automatically when a request is handled by Flask. Since we are not inside a request we have to create the context by hand just so that Flask-Mail can do its job.
Pretty, neat. Now it's time to integrate this code into our application!
A simple email framework
We will now write a helper function that sends an email. This is just a generic version of the above test. We'll put this function in a new source file that will be dedicated to our email support functions (file app/emails.py
):
from flask_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)
Note that Flask-Mail support goes beyond what we are using. Bcc lists and attachments are available, for example, but we won't use them in this application.
Follower notifications
Now that we have the basic framework to send an email in place, we can write the function that sends out the follower notification (file 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))
Do you find any surprises in here? Our old friend the render_template
function is making an appearance. If you recall, we used this function to render all the HTML templates from our views. Like the HTML from our views, the bodies of email messages are an ideal candidate for using templates. As much as possible we want to keep logic separate from presentation, so emails will also go into the templates
folder along with our views.
So we now need to write the templates for the text and HTML versions of our follower notification email. Here is the text version (file 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
For the HTML version we can do a little bit better and even show the follower's avatar and profile information (file 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>
Note the _external=True
argument to url_for
in the above templates. By default, the url_for
function generates URLs that are relative to the domain from which the current page comes from. For example, the return value from url_for("index")
will be /index
, while in this case we want http://localhost:5000/index
. In an email there is no domain context, so we have to force fully qualified URLs that include the domain, and the _external
argument is just for that.
The final step is to hook up the sending of the email with the actual view function that processes the "follow" (file 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))
Now you can create two users (if you haven't yet) and make one follow the other to see how the email notification works.
So that's it? Are we done?
We could now pat ourselves in the back for a job well done and take email notifications out of our list of features yet to implement.
But if you played with the application for some time and paid attention you may have noticed that now that we have email notifications when you click the follow
link it takes 2 to 3 seconds for the browser to refresh the page, whereas before it was almost instantaneous.
So what happened?
The problem is that Flask-Mail sends emails synchronously. The web server blocks while the email is being sent and only returns its response back to the browser once the email has been delivered. Can you imagine what would happen if we try to send an email to a server that is slow, or even worse, temporarily offline? Not good.
This is a terrible limitation, sending an email should be a background task that does not interfere with the web server, so let's see how we can fix this.
Asynchronous calls in Python
What we really want is for the send_email
function to return immediately, while the work of sending the email is moved to a background process.
Turns out Python already has support for running asynchronous tasks, actually in more than one way. The threading
and multiprocessing
modules can both do this.
Starting a thread each time we need to send an email is much less resource intensive than starting a brand new process, so let's move the mail.send(msg)
call into thread (file 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()
The send_async_email
function now runs in a background thread. Because it is a separate thread, the application context required by Flask-Mail will not be automatically set for us, so the app
instance is passed to the thread, and the application context is set up manually, like we did above when we sent an email from the Python console.
If you test the 'follow' function of our application now you will notice that the web browser shows the refreshed page before the email is actually sent.
So now we have asynchronous emails implemented, but what if in the future we need to implement other asynchronous functions? The procedure would be identical, but we would need to duplicate the threading code for each particular case, which is not good.
We can improve our solution by implementing a decorator. With a decorator the above code would change to this:
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)
Much nicer, right?
The code that allows this magic is actually pretty simple. We will put it in a new source file (file 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
And now that we indirectly have created a useful framework for asynchronous tasks we can say we are done!
Just as an exercise, let's consider how this solution would look using processes instead of threads. We do not want a new process started for each email that we need to send, so instead we could use the Pool
class from the multiprocessing
module. This class creates a specified number of processes (which are forks of the main process) and all those processes wait to receive jobs to run, given to the pool via the apply_async
method. This could be an interesting approach for a busy site, but we will stay with the threads for now.
Final words
The source code for the updated microblog
application is available below:
Download microblog-0.11.zip.
I've got a few requests for putting this application up on github or similar, which I think is a pretty good idea. I will be working on that in the near future. Stay tuned.
Thank you again for following me on this tutorial series. I look forward to see you on the next chapter.
Miguel
-
#76 Mara said
Hi Miguel!
Thank you so much for this tutorial!But I'm getting this error:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "/home/microblog/flask/local/lib/python2.7/site-packages/flask_mail.py", line 491, in send
with self.connect() as connection:
File "/home/microblog/flask/local/lib/python2.7/site-packages/flask_mail.py", line 144, in enter
self.host = self.configure_host()
File "/home/microblog/flask/local/lib/python2.7/site-packages/flask_mail.py", line 158, in configure_host
host = smtplib.SMTP(self.mail.server, self.mail.port)
File "/usr/lib/python2.7/smtplib.py", line 249, in init
(code, msg) = self.connect(host, port)
File "/usr/lib/python2.7/smtplib.py", line 309, in connect
self.sock = self._get_socket(host, port, self.timeout)
File "/usr/lib/python2.7/smtplib.py", line 284, in _get_socket
return socket.create_connection((port, host), timeout)
File "/usr/lib/python2.7/socket.py", line 571, in create_connection
raise err
socket.error: [Errno 111] Connection refusedBtw, I'm using a virtual machine for Linux. Is that a problem?
Thanks so much. Hope to hear from you soon. -
#77 Miguel Grinberg said
@Mara: your VM is apparently unable to establish a connection to the SMTP server.
-
#78 Fernando França said
Excellent tutorial series. Thanks for share this knowledge.
-
#79 tOlorun said
Hi Miguel
Hi everyoneI need help
trying to use the @async decoration so that some functions can run in the background
but i get the following error
Exception in thread Thread-2:
Traceback (most recent call last):
File "/usr/lib/python2.7/threading.py", line 801, in bootstrap_inner
self.run()
File "/usr/lib/python2.7/threading.py", line 754, in run
self.__target(self.__args, *self.__kwargs)
File "/media/tolorun/mmxv/104.131.174.54/edc/app/api_v1/aggrgtr.py", line 36, in log
alog.createdBy, alog.details, alog.stage = g.user.id, log[1], log[0]
File "/usr/local/lib/python2.7/dist-packages/werkzeug/local.py", line 343, in __getattr
return getattr(self._get_current_object(), name)
File "/usr/local/lib/python2.7/dist-packages/werkzeug/local.py", line 302, in _get_current_object
return self.__local()
File "/usr/local/lib/python2.7/dist-packages/flask/globals.py", line 27, in _lookup_app_object
raise RuntimeError('working outside of application context')
RuntimeError: working outside of application contextplease help me
thanks in advance -
#80 Miguel Grinberg said
@tOlorun: you need to create an app context to send email or other tasks that require access to the Flask application, as shown above in this article.
-
#81 Anonymous said
Why do you need an extension to send e-mail? I can't see anything this package does over just using out of the box email module?
-
#82 Miguel Grinberg said
@Anonymous: You do not "need" an extension to send an email, you can do so with the email support in the standard library. The main benefit of using Flask-Mail though, is that email configuration is given as part of the Flask configuration. If you have several configurations, then switching between them does not need to be done by hand.
-
#83 Amit Kumar said
Hey,
I am getting KeyError: 'mail'
and it seems to be an issue with release, i even tried to downgrade but the issue with flask_mail remains.
Please help -
#84 Miguel Grinberg said
@Amit: the text of the error alone isn't sufficient to diagnose the problem. I need to complete stack trace.
-
#85 spetty said
I dont really understand decorators.py, can you explain how the @async and the async(f) we define in decorators.py are linked and how async(f) works?
like others have said, amazing tutorial. I've run into some problems (probably caused by updates to packages since your wrote this) but i've managed to work around them.
-
#86 Miguel Grinberg said
@spetty: decorators are a standard feature of Python. The @async decorator and the async(f) function are not just linked, they are the same thing. Decorators in Python are defined as functions. You can find more information about decorators in the Python docs, and there are also lots of tutorials about how to make them.
-
#87 Chris Yoon said
Thanks again Miguel. Great tutorial.
For people having trouble with the python 2.xx command line mail send test, once you set the os.environ on your environment, you have to first "import os" so that print os.environ.get('mail_username') returns the string that you saved. I'm using powershell and the command was '$env:mail_username = "example@mail.com"' You can verify if the env is saved by typing the following in python: "print os.environ.get('mail_username'). I hope that helps. I spent an hour stuck on this AFTER I changed google security settings.
Cheers!
-
#88 Bos Hieu said
Hi everyone,
Yesterday, I tested send_mail() method and I had a bug like #46.
So, I read all comments in this blogs. And I found out three ways to fix this bug:
1. Use celery like this post 'http://blog.miguelgrinberg.com/post/celery-and-the-flask-application-factory-pattern'
2. Use 'context' instead like #49
3. Use 'current_app._get_current_object()' instead like #50I have tested all and I fixed this bug.
But I really don't understand why '2' and '3' can fix this bug.Anyone can explain why?
Thank you so much!
Bos Hieu,
-
#89 Reznov Ammar said
I was wondering how to make a user notification using Flask, so for example:
When a user make a comment or send another user a message, when the user entering his admin panel he will find a red point for example or a notification that there is a message have been sent to him so when he click on that the point disappear , please any why how to do that ?
-
#90 Miguel Grinberg said
@Reznov: In my PyCon 2014 tutorial "Flask By Example" I showed an application in which the administrator receives notifications when users post comments that need to be reviewed. Take a look at that tutorial on youtube, or the project at GitHub for details.
-
#91 Sam said
Thank you Miguel for your dedicated work. Nicely done!
-
#92 divya said
Hi Miguel, You tutorial has been so helpful in getting up to speed with flask and python. I am new to both. I have a question with regard to flask-mail. My server is behind a proxy and i couldnt find a straightforward way to specify proxy information to smtp or socks library via flask. Do you have any suggestions? Thanks Divya
-
#93 Miguel Grinberg said
@divya: I would configure your proxy in the system, so that all processes that are running in it use it.
-
#94 Rob said
Hey Miguel,
I'm totally stuck on the part where I need to set environment variables. That would be a great area to expand in the future. For now, I can get it to work if I plug in my actual username and password to "config.py", so I know mail is working.
Now, I'm trying to setup a different file, called secure.py in the same directory where the only code would be:
export MAIL_USERNAME='actual_username'
export MAIL_PASSWORD='actual_password'It threw a SyntaxError at me that looks like this:
Traceback (most recent call last):
File "C:...\run.py", line 2, in <module>
from app import app
File "C:...\app__init__.py", line 6, in <module>
from config import basedir, ADMINS, MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, \
File "C:...\config.py", line 1, in <module>
import os, secure
File "C:...\secure.py", line 1
export MAIL_USERNAME = 'actual_username'
^
SyntaxError: invalid syntaxI researched it and saw that, since I am using a Windows PC, I need to use 'set' instead of 'export'. So I change it to set and...
Traceback (most recent call last):
File "C:...\run.py", line 2, in <module>
from app import app
File "C:...\app__init__.py", line 6, in <module>
from config import basedir, ADMINS, MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, \
File "C:...\config.py", line 1, in <module>
import os, secure
File "C:...\secure.py", line 1
set MAIL_USERNAME = 'actual_username'
^
SyntaxError: invalid syntaxSame error. I tried setting it via console, and got the same error. What's going on? Why would = not be valid syntax?
-
#95 Miguel Grinberg said
@Rob: the export and/or set commands are not Python statements, they need to be typed on your command-line prompt. I suggest you familiarize with the basic operation of the command prompt and how environment variables work.
-
#96 Rob said
@Miguel: Yep, I'm an idiot. Just realizing it was an OS-level command cleared things up, and I found the appropriate syntax. Thanks!
-
#97 vaishali said
My mail_setting is:
mail_settings = {
"MAIL_SERVER" : 'smtp.orange.fr',
"MAIL_PORT" : 465,
"MAIL_USE_TLS" : False,
"MAIL_USE_SSL" : True,
"MAIL_USERNAME" : "vaishali.chaudhari.ext@orange.com",
"MAIL_PASSWORD" : "pass",
"DEBUG" : True,
}app.config.update(mail_settings)
mail = Mail()
from pprint import pprint
pprint(app.config)
mail.init_app(app)
with app.app_context():
msg = Message(subject="Welcome in ioc-manager application",
sender="vaishali@orange.com",
recipients=["vaishali.chaudhari@orange.com"],
body="This is test email")
mail.send(msg)I am getting following error:
socket.error: [Errno 10060] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond
Can you suggest me why this error is coming? Even with server smtp.gmail.com its a same error
-
#98 Miguel Grinberg said
@vaishali: I don't know. Maybe a firewall rule prevents outgoing connections on the email ports. It's difficult for me to say, try the development SMTP server, that will confirm that the application is working okay.
-
#99 Luciano said
Hi Miguel,
great job with this tutorials... however i get this error and i cannot figure out how to solve it.Traceback (most recent call last):
File "<input>", line 1, in <module>
File "C:\Users\lucia\PycharmProjects\provaemail1\venv\lib\site-packages\flask_mail.py", line 438, in send
with self.connect() as connection:
File "C:\Users\lucia\PycharmProjects\provaemail1\venv\lib\site-packages\flask_mail.py", line 106, in enter
self.host = self.configure_host()
File "C:\Users\lucia\PycharmProjects\provaemail1\venv\lib\site-packages\flask_mail.py", line 121, in configure_host
host = smtplib.SMTP(self.mail.server, self.mail.port)
File "C:\Python27\Lib\smtplib.py", line 256, in init
(code, msg) = self.connect(host, port)
File "C:\Python27\Lib\smtplib.py", line 317, in connect
self.sock = self._get_socket(host, port, self.timeout)
File "C:\Python27\Lib\smtplib.py", line 292, in _get_socket
return socket.create_connection((host, port), timeout)
File "C:\Python27\Lib\socket.py", line 575, in create_connection
raise err
error: [Errno 10061] Impossibile stabilire la connessione. Rifiuto persistente del computer di destinazionethis is my code:
from flask import Flask
from flask_mail import Mail
import osapp = Flask(name)
<h1>email server</h1>
app.config['SECRET_KEY'] = 'hard-to-guess'
mail = Mail(app)MAIL_SERVER = 'smtp.gmail.com'
<h1>administrator list</h1>
MAIL_PORT = 465
MAIL_USERNAME = os.environ.get('my_username@gmail.com')
MAIL_PASSWORD = os.environ.get('my_password')ADMINS = [my_username@gmail.com', 'another_mail@live.com']
if name == 'main':
app.run()Please, what am I mistaking?
-
#100 Miguel Grinberg said
@Luciano: the mail server you are connecting to is refusing the connection. I notice you are using "smtp.gmail.com", while I use "smtp.googlemail.com". Any reason for changing that?