📜 ⬆️ ⬇️

Flask Mega-Tutorial, Part X: Email Support (Edition 2018)

Miguel grinberg




There


This is the tenth part of the Mask-Tutorial Flask series, in which I will tell you how the application can send emails to your users and how to create a password recovery function with the support of an email address.


Under the spoiler is a list of articles in this series.



Note 1: If you are looking for old versions of this course, this is here .


Note 2: If suddenly you would like to speak in support of my (Miguel) work, or simply do not have the patience to wait for the article for a week, I (Miguel Greenberg) offer the full version of this manual (in English) in the form of an electronic book or video. For more information, visit learn.miguelgrinberg.com .


As a result, after not long torments, after 9 lessons we got an application that works tolerably well with the database, so in this chapter I want to get away from this topic and add another important part that most web applications need, but exactly - sending email.


Why should an application e-mail something to its users? There are many reasons, but one of them is solving the problems associated with authentication. In this chapter, I'm going to add a password reset feature for users who constantly forget their password. When a user requests a password reset, the application sends an email with a specially created link. The user then needs to click this link to access the form in which you can set a new password.


GitHub links for this chapter: Browse , Zip , Diff .


Introduction to Flask-Mail


As for sending email, Flask has an extension called Flask-Mail for this purpose, which will help make this task very simple. As always, it (this extension) is installed using pip:


(venv) $ pip install flask-mail 

References to a password reset should contain a secure token. To generate these tokens, I'm going to use JSON Web Tokens , which also has a popular Python package:


 (venv) $ pip install pyjwt 

The Flask-Mail extension is configured from the app.config object. Remember when in Chapter 7 I added an email configuration to send email every time an error occurred in production? Then I did not tell you about it, but my choice of configuration variables was modeled after Flask-Mail requirements, so there is no need for any additional work, the configuration variables are already in the application.


As with most Flask extensions, you need to create an instance immediately after creating the Flask application. In this case, it is an object of the Mail class:


 # ... from flask_mail import Mail app = Flask(__name__) # ... mail = Mail(app) 

Note translator: It is very important that the string mail = Mail(app) located after app.config.from_object(Config) .

In order to test sending emails, you have the same two options that I mentioned in Chapter 7 . If you want to use an emulated mail server, then Python provides an option to run in the second terminal with the following command:


 (venv) $ python -m smtpd -n -c DebuggingServer localhost:8025 

To configure this server, you need to set two environment variables:


 (venv) $ export MAIL_SERVER=localhost (venv) $ export MAIL_PORT=8025 

If you prefer to send emails "adult", you need to use a real mail server. If you have one, you just need to set the environment variables MAIL_SERVER , MAIL_PORT , MAIL_USE_TLS , MAIL_USERNAME and MAIL_PASSWORD . For particularly lazy, I remind you how to use your Gmail account to send email with the following settings:


 (venv) $ export MAIL_SERVER=smtp.googlemail.com (venv) $ export MAIL_PORT=587 (venv) $ export MAIL_USE_TLS=1 (venv) $ export MAIL_USERNAME=<your-gmail-username> (venv) $ export MAIL_PASSWORD=<your-gmail-password> 

If you are using Microsoft Windows , you need to replace the export with the set in each of the above export instructions.


Remember that the security settings of your Gmail account may prevent an application from sending email through it unless you explicitly allow “less secure applications” access to your Gmail account. You can read about it here , and if you are concerned about the security of your account, you can create a secondary account that you set up to check emails only, or you can temporarily enable the option to allow “less secure applications” access to run your tests and then go back to a safer default.


Using Flask-Mail


To demonstrate how Flask-Mail works, I'll show you how to send an email from the Python shell. To do this, start Python with the flask shell, and then run the following commands:


 >>> from flask_mail import Message >>> from app import mail >>> msg = Message('test subject', sender=app.config['ADMINS'][0], ... recipients=['your-email@example.com']) >>> msg.body = 'text body' >>> msg.html = '<h1>HTML body</h1>' >>> mail.send(msg) 

The code snippet above will send an email to the list of email addresses you specified in the recipients argument. As sender (sender) I use the administrator setting (I added a variable in the ADMINS config ADMINS see chapter 7 ). The letter will have plain text in the HTML version, so depending on how your email client is configured, you can see one or the other option.


In short, it's pretty simple. Now let's integrate emails into the app.


Simple Email Framework


Let's start by writing a helper function that sends an email. In general, it repeats the flask shell exercise from the previous section. I will put this feature in a new module called app/email.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) 

Flask-Mail supports some interesting features that I do not use here. Such as lists Cc and Bcc . Be sure to read the Flask-Mail documentation if you are interested in these parameters.


Password reset request


Let me remind you that the problem we are solving is to provide the user with the opportunity to reset his password. For this, I'm going to add a link to the login page:


 <p> Forgot Your Password? <a href="{{ url_for('reset_password_request') }}">Click to Reset It</a> </p> 

When the user clicks on the Click to Reset It link, a new web form will appear that requests the user's email address as a way to initiate the password reset process. Here is the form class:


 class ResetPasswordRequestForm(FlaskForm): email = StringField('Email', validators=[DataRequired(), Email()]) submit = SubmitField('Request Password Reset') 

And here is the corresponding HTML template:


 {% extends "base.html" %} {% block content %} <h1>Reset Password</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.email.label }}<br> {{ form.email(size=64) }}<br> {% for error in form.email.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %} 

You also need the view function to handle this form:


 from app.forms import ResetPasswordRequestForm from app.email import send_password_reset_email @app.route('/reset_password_request', methods=['GET', 'POST']) def reset_password_request(): if current_user.is_authenticated: return redirect(url_for('index')) form = ResetPasswordRequestForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user: send_password_reset_email(user) flash('Check your email for the instructions to reset your password') return redirect(url_for('login')) return render_template('reset_password_request.html', title='Reset Password', form=form) 

This view function looks a lot like the others that process the form. We start with the fact that the user is not logged in. If the user is logged in, it makes no sense to use the password reset function, but you should redirect the output to the index page.


When the form is submitted and valid, I search for the user via the email provided by the user in the form. If the user is found, send an email with a password reset. For this, the helper function send_password_reset_email() . I will show you this feature below.


After sending the e-mail, I sent a message prompting the user to check the e-mail in his inbox, where he should find the message with instructions and redirection back to the login page. You may notice that this message is displayed anyway. This means that customers cannot use this form to find out if this user is registered or not.


Password reset tokens


Before implementing the send_password_reset_email() function, I need to think of a way to create a link to request a password. This will be the link that will be emailed to the user. When clicking on the link, the user is presented with a page where a new password can be set. The hard part of this plan is to make sure that only valid reset links can be used to reset the account password.


Links will be provided with a token, and this token will be checked before allowing a password change, as proof that the user who requested the email has access to the email address in the account. A very popular token standard for this type of process is JSON Web Token, or JWT. The best part of JWT is that they are self-sufficient. You can email the token to the user, and when the user clicks on the link that returns the token back to the application, you can check it yourself.


To figure out how JWT works? Nothing better than to figure out how to experience this in a Python shell session:


 >>> import jwt >>> token = jwt.encode({'a': 'b'}, 'my-secret', algorithm='HS256') >>> token b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhIjoiYiJ9.dvOo58OBDHiuSHD4uW88nfJikhYAXc_sfUHq1mDi4G0' >>> jwt.decode(token, 'my-secret', algorithms=['HS256']) {'a': 'b'} 

The dictionary {'a': 'b'} is an example of the payload that will be written to the token. To make the token secure, you must provide a secret key to use when creating a cryptographic signature. In this example, I used the string my-secret , but with the application I'm going to use SECRET_KEY from the configuration. The algorithm argument specifies how the token should be generated. The most widely used algorithm is the HS256 .


As you can see, the final token is a long sequence of characters. But do not think that this is an encrypted token. The contents of the token, including the payload, can be easily decoded by any user (don't believe me? Copy the above token, and then enter it in the JWT debugger to view its contents). What makes the token safe is that the payload is signed. If someone tried to fake or manipulate the payload in the token, then the signature will be invalidated, and a secret key is needed to create a new signature. When the token is verified, the contents of the payload are decoded and sent back to the caller. If the token's signature has been confirmed, then the payload can be trusted as authentic.


The payload that I will use for password reset tokens will be in the format {'reset_password': user_id, 'exp': token_expiration} . The exp field is standard for JWT, and if it is present, this indicates the time at which the token expires. If the token has a valid signature, but it has exceeded the expiration time mark, then the signature will be considered invalid. For the password reset function, I'm going to give these tokens 10 minutes of life.


When a user clicks on a link in an email received, the token will be sent back to the application as part of the URL, and first of all the browsing function that processes this URL will check it. If the signature is valid, the user may be identified by an identifier stored in the payload. Further, as soon as the user identification is verified, the application can request a new password and set it in the user account.


Since these tokens belong to users, I am going to write token generation and validation functions as methods in the User model:


 from time import time import jwt from app import app class User(UserMixin, db.Model): # ... def get_reset_password_token(self, expires_in=600): return jwt.encode( {'reset_password': self.id, 'exp': time() + expires_in}, app.config['SECRET_KEY'], algorithm='HS256').decode('utf-8') @staticmethod def verify_reset_password_token(token): try: id = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])['reset_password'] except: return return User.query.get(id) 

The get_reset_password_token() function generates a JWT token as a string. Note that decode('utf-8') required because the jwt.encode() function returns a token as a sequence of bytes, but in an application it is more convenient to have the token as a string.


The verify_reset_password_token() check function is a static method, which means that it can be called directly from a class. A static method is similar to a class method, with the only difference that static methods do not require creating an instance of a class. If simpler, then there is no first argument self. This method takes a token and tries to decode it by calling the jwt.decode() function PyJWT. If the token cannot be verified or expired, an exception will be thrown, in which case I will intercept it to prevent the consequences of the error, and then return None . If the token is valid, then the reset_password key reset_password from the token payload is the user ID, so I can load the user and return it to the page.


Sending email to reset your password


Now that I have tokens, I can generate emails to reset my password. The send_password_reset_email() function depends on the send_email() function that I wrote above.


 from flask import render_template from app import app # ... def send_password_reset_email(user): token = user.get_reset_password_token() send_email('[Microblog] Reset Your Password', sender=app.config['ADMINS'][0], recipients=[user.email], text_body=render_template('email/reset_password.txt', user=user, token=token), html_body=render_template('email/reset_password.html', user=user, token=token)) 

An interesting part of this function is that the text and HTML content for emails are generated from templates using the familiar render_template() function. Templates take a user and a token as arguments, so a personalized email message can be generated. Here is a text template for resetting the password:


   {{ user.username }},   ?  ,   . !   ,    : {{ url_for('reset_password', token=token, _external=True) }}      ,     .  ,  Microblog 

Or like this, decently, in the HTML version of almost the same letter with more decent text:


 <p> {{ user.username }},</p> <p>    <a href="{{ url_for('reset_password', token=token, _external=True) }}">    </a>. </p> <p> ,         :</p> <p>{{ url_for('reset_password', token=token, _external=True) }}</p> <p>     ,    .</p> <p> ,</p> <p> Microblog</p> 

Note that the reset_password route referenced by the url_for() call in these two email templates does not exist yet, it will be added in the next section.


Reset user password


When a user clicks on an email link, the second route associated with this feature will work. Here is the function of viewing the password request:


 from app.forms import ResetPasswordForm @app.route('/reset_password/<token>', methods=['GET', 'POST']) def reset_password(token): if current_user.is_authenticated: return redirect(url_for('index')) user = User.verify_reset_password_token(token) if not user: return redirect(url_for('index')) form = ResetPasswordForm() if form.validate_on_submit(): user.set_password(form.password.data) db.session.commit() flash('Your password has been reset.') return redirect(url_for('login')) return render_template('reset_password.html', form=form) 

In this view function, I first make sure that the user is not logged in, and then I determine who the user is by calling the token verification method in the User class. This method returns the user if the token is valid, or None if not. If the token is not valid, I am redirected to the index home page.


If the token is valid, then I submit to the user a second form in which a new password is requested. This form is processed in the same way as the previous forms, and as a result of successfully submitting the form, I call the set_password() method set_password() to change the password and then redirect to the login page where the user can now log in.


Here is the ResetPasswordForm class:


 class ResetPasswordForm(FlaskForm): password = PasswordField('Password', validators=[DataRequired()]) password2 = PasswordField( 'Repeat Password', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('Request Password Reset') 

And this is the corresponding HTML template:


 {% extends "base.html" %} {% block content %} <h1>Reset Your Password</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.password.label }}<br> {{ form.password(size=32) }}<br> {% for error in form.password.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.password2.label }}<br> {{ form.password2(size=32) }}<br> {% for error in form.password2.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %} 

Now the password reset function is complete. Give it a try.


Asynchronous messages


If you are using a simulated email server that Python provides, you may not have noticed, but sending email slows down the application significantly. All interactions that must occur when sending email make the task slow, it usually takes a few seconds to receive the email, and perhaps more if the addressee’s email server is slow or if there are several recipients.


I would like the send_email() function to be asynchronous. What does it mean? This means that when you call this function, the email sending task is scheduled in the background, freeing send_email() to return immediately, so that the application can continue to work simultaneously with the email being sent.


Python has support for running asynchronous tasks, in fact in more than one way. threading and multiprocessing modules can be executed. Starting a background thread for a sent message is much less resource-intensive than starting a completely new process, so I'm going to go that way:


 from threading import Thread # ... 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 Thread(target=send_async_email, args=(app, msg)).start() 

The send_async_email function now works in a background thread, because it is called via the Thread() class in the last line of send_email() . Sending by e-mail will now be performed in a separate thread, and when the process is completed, the flow is completed and cleared. If you set up and use a real mail server, you will definitely notice an improvement in speed when you click the “Send” button in the password reset request form.


You probably expected that only the msg argument would be sent to the stream, but as you can see in the code, I also send an instance of the app . When working with streams, there is an important aspect of Flask design that needs to be kept in mind. Flask uses contexts to avoid having to pass arguments through functions. I am not going to dwell on this in detail, but I know that there are two types of contexts, the application context and the request context ( application context and request context ). In most cases, these contexts are automatically managed by the infrastructure, but when the application starts user threads, the contexts for these threads may require manual input.


There are many extensions that require the application context to work, because it allows them to find an instance of the Flask application without passing it as an argument. The reason many extensions need to know the application instance is that they have the configuration stored in the app.config object. This is exactly the situation with Flask-Mail. The mail.send() method must access the configuration values ​​for the mail server, and this can be done only by knowing what an app is. An application context created with a call with app.app_context() makes an instance of the application accessible via the current_app variable from Flask.


There


')

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


All Articles