📜 ⬆️ ⬇️

Flask Mega-Tutorial, Part 11: Email Support

This is the eleventh article in the series, where I describe my experience of writing a Python web application using the Flask mic framework.

The purpose of this guide is to develop a fairly functional microblog application, which I decided to call microblog in the absence of originality.


Brief repetition


In the last lessons we dealt mainly with improvements related to our database.
')
Today we will allow our database to rest for a while, and instead look at one very important function that most web applications have: the ability to send email to the user.

In our small application, we are going to implement a similar function; we will send a notification to the user every time someone subscribes to it. There are a few more things for which the sending function may be useful, so we will try to design our function so that it can be reused.

Introduction to Flask-Mail


Fortunately for us, Flask already has an email processing extension, and although it does not perform 100% of the tasks, it is very close to that.

Installing Flask-Mail in our virtual environment is quite simple. Users on systems other than Windows should do:

 flask/bin/pip install flask-mail 

For Windows users, things are a little more complicated, because one of the dependencies Flask-Mail does not work on this OS. On Windows, you need to do the following:

 flask\Scripts\pip install --no-deps lamson chardet flask-mail 


Configuration


Earlier, when we added Unit testing, we added a configuration for Flask in which we indicated an email to which error notifications should be sent in the production version of our application. The same information is used to send mail by the application.

We need to remember that we need the following information:



This is what we did in the previous article ( config.py ):

 # 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'] 


Of course, you will have to enter the actual data in this config in order for the application to actually be able to send you emails. For example, if you want to use the application to send emails via gmail.com, you need to specify the following:

 # 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'] 


We also need to initialize the Mail object, because This will be the object that will connect to the SMTP server and send emails for us (file app/__init__.py ):

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


Let's send an email.

To understand how Flask-Mail works, we need to send an email from the command line. Let's run Python from our virtual environment and type the following:

 >>> 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) .... 


The code snippet above will send a letter to the list of administrators specified in config.py . The sender will be the first administrator from the list. The letter will have a text and HTML version, which you will see depends on the settings of your email client. Please note that we need to create an app_context to send an email. Recent Flask-Mail releases require this. The context is created automatically when the request is processed by Flask.

Since we are not inside the request, we can create the context with our hands.

Now it's time to integrate this code into our application.

Simple email framework


Now we will write an additional function that sends email. This is just a more general version of the above test. We will put this function in a new file that we will highlight for our email-related functions (file 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) 


Please note that Flask-mail supports more than we use. For example, lists of hidden copies and attachments are available, but we will not use them in our application.

Subscription Notifications


Now that we have a basic framework for sending email, we can write a subscriber notification function (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)) 


Saw something unexpected?
Our old friend - the render_template function creates a letter view. If you remember, we used this function to render all the HTML templates from our presentation. Just like our HTML, the body of the letter is an ideal candidate for using templates. We want, as far as possible, to separate the logic from the presentation, so the letters will go in the folder with the templates along with the other view.

So, now we will write templates for text and HTML versions for our notification. This is the text version ( app/templates/follower_email.txt file):
 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 make everything a little more beautiful and show the subscriber’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=""></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> 


Notice the _external = True in the url_for field of our template. By default, the url_for function generates URLs relative to the current page. For example, the code {url_for ("index")} will be /index , while we are expecting http://localhost:5000/index . Email does not have a domain context, so we must provide full URLs that include the domain, and _external will help us with _external .

The final step will be to connect sending an email with a view function that handles "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)) 


You must now create two users (if you have not already done so) and make one subscriber to the other to see how the email notification works. This is what you need? Are we done?

Now we can pat ourselves on the head for a job well done and cross out email notifications from the list of functions that we have to implement.

But if you played with our application, you noticed that now that we’ve implemented email notifications, it takes a few seconds after clicking on “Follow” before the browser refreshes the page. And before that it happened instantly.

So what happened?

The problem is that Flask-Mail sends emails synchronously. The server is blocked until the letter is sent and sends a response to the browser, only when the message is delivered. Can you imagine what will happen if we try to send an email to a slow server, or, even worse, turned off? This is bad.

This is a very scary limitation, sending an email should be a background task that does not interfere with the server, so let's see how we can fix it all.

Asynchronous calls in Python


We want the send_email function send_email end instantly while the job of sending the letter is in the background.

It turns out Python already supports launching asynchronous tasks, even in more than one way. Threading and multiprocessing modules can help us.

Starting a new thread, every time we need to send an email, a much less resource-intensive operation than starting a new process, so let's move the mail.send(msg) call to the stream ( app/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 = Thread(target = send_async_email, args = [msg]) thr.start() 


If you are testing the “Follow” function, you will notice that the browser shows the updated page before the letter is sent.

So, now we have implemented asynchronous mail sending, but what if in the future we need to implement other asynchronous functions? The procedure will remain the same, but we will need to duplicate the code for working with threads in each case, which is not good.

We can implement our solution in the form of a decorator. With the decorator, the code above will change to this one:

 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) 


Much better, isn't it?

The code that makes this magic is actually very simple. We will write it into a new file ( app/decorators.py file):

 from threading import Thread def async(f): def wrapper(*args, **kwargs): thr = Thread(target = f, args = args, kwargs = kwargs) thr.start() return wrapper 


Now that we have accidentally created a good basis for asynchronous tasks, we can say that everything is done!

For the sake of the exercise, let's consider how our solution would change if we used processes instead of threads.
We do not want the new process to start every time we send an email, instead we can use the Pool class from the multiprocessing module. This class creates the required number of processes (which are forks of the main process) and all processes are waiting for tasks that are passed through the apply_async method. This may be useful and interesting for loaded sites, but for now we’ll stop at threads.

Final words


The source code of the updated application is available below:

Download microblog-0.11.zip.

I received several requests to place this application on a github or similar site, I think this is a very good idea. I will work on this in the near future. Stay in touch.

Thank you for following my series of tutorials. Hope to see you in the following parts.

Miguel

Source: https://habr.com/ru/post/234737/


All Articles