Jeevs09
Jeevs09

Reputation: 71

Email verification with flask-mail

I am looking to add email verification to my web app using flask-mail, and after reading the documentation, it seems that I must create a Mail instance using:

app = Flask(__name__)
mail = Mail(app)

and then import the app and mail instances.

However, my current code creates the Flask and Mail instances inside a function as below:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager 

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)

    app.config["SECRET_KEY"] = "9OLWxND4o83j4K4iuopO"
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite"

    db.init_app(app)

    login_manager = LoginManager()
    login_manager.login_view = "auth.login"
    login_manager.init_app(app)

    from .models import User

    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app

The above code is in my __init__.py file. I can't import the Mail instance into my other files where I register a user because one hasn't actually been defined, it is only in a function. The base code was from this tutorial: https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login and now I am adding email verification to it. To run the web app, I type db.create_all(app=create_app()) in a Python REPL, which creates my sqlite database, and is the only time the create_all() function is called. And then I type Flask run in my powershell terminal.

Upvotes: 2

Views: 9993

Answers (3)

miksus
miksus

Reputation: 3437

I recently came to the same problem and I decided to solve it by creating a Flask extension to (my) email library. This extension (Flask-Redmail) is pretty similar to Flask-Mail but it is more feature-packed and relies on a well tested and robust library, called Red Mail.

I wrote how I did it here: https://flask-redmail.readthedocs.io/en/latest/cookbook.html#verification-email

In short, what you need to do:

  • Get the email (and the password) the user specified
  • Store the user to your user database as an unverified user
  • Send an email to the user with a unique URL that identifies him/her
  • Create this URL endpoint and set the user to be verified if visited.

In order to achieve these, I suggest to use:

Next, I'll demonstrate how to do it. Create the file for your application (ie. app.py):

from flask import Flask
from flask_redmail import RedMail¨
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager

email = RedMail()
db = SQLAlchemy()
login_manager = LoginManager()

def create_app():
    app = Flask(__name__)
    
    # Configure the sender
    app.config["EMAIL_HOST"] = "localhost"
    app.config["EMAIL_PORT"] = 587
    app.config["EMAIL_USER"] = "[email protected]"
    app.config["EMAIL_PASSWORD"] = "<PASSWORD>"

    # Set some other relevant configurations
    app.config["SECRET_KEY"] = "GUI interface with VBA"
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app_data.db"

    email.init_app(app)
    db.init_app(app)
    login_manager.init_app(app)

    # Import and set the blueprints/routes
    ...

Create the user class and set login to models.py:

from app import db, login_manager
from flask_login import UserMixin

@login_manager.user_loader
def load_user(user_id):
    return User.query.filter_by(id=user_id).first()

class User(UserMixin, db.Model):
    __tablename__ = 'user'

    email = db.Column(db.String, primary_key=True)
    password = db.Column(db.String, nullable=False)
    verified = db.Column(db.Boolean, default=False)

Then to the route, for example as views.py:

from flask import request, current_app, abort, render_template, BluePrint

# Import your custom instances and models
from app import email, db
from models import User

auth_page = Blueprint('auth', __name__)

@auth_page.route("/create-user", methods=["GET", "POST"])
def create_user():
    if request.method == "GET":
        return render_template("create_user.html")
    elif request.method == "POST":
        # Now we create the user

        # Getting form data (what user inputted)
        data = request.form.to_dict()
        email = data["email"]
        password = data["password"]

        # Verifying the user does not exist
        old_user = User.query.filter_by(id=email).first()
        if old_user:
            abort(403)

        # Encrypt the password here (for example with Bcrypt)
        ...

        # Creating the user
        user = User(
            email=email, 
            password=password,
            verified=False
        )
        db.session.add(user)
        db.session.commit()

        # Create a secure token (string) that identifies the user
        token = jwt.encode({"email": email}, current_app.config["SECRET_KEY"])
        
        # Send verification email
        email.send(
            subject="Verify email",
            receivers=email,
            html_template="email/verify.html",
            body_params={
                "token": token
            }
        )

Then we create the email body. Flask-Redmail seeks the HTML templates from the application's Jinja environment by default. Do this simply by creating file templates/email/verify.html:

<h1>Hi,</h1>
<p>
    in order to use our services, please click the link below:
    <be>
    <a href={{ url_for('verify_email', token=token, _external=True) }}>verify email</a>
</p>
<p>If you did not create an account, you may ignore this message.</p>

Finally, we create a route to handle the verification:

@auth_page.route("/vefify-email/<token>")
def verify_email(token):
    data = jwt.decode(token, current_app.config["SECRET_KEY"])
    email = data["email"]

    user = User.query.filter_by(email=email).first()
    user.verified = True
    db.session.commit()

Note that you need to templates/create_user.html and models.py where you store your User class.

Some relevant links:

More about Red Mail:

Upvotes: 4

Gitau Harrison
Gitau Harrison

Reputation: 3517

Like many other extensions, you will need to create a mail object in your __init__.py file after installing flask-mail. Using a structure like the one below, where blueprints are used, you can add email support to your flask app.

project_folder
    | --- app.py
    | --- config.py
    | --- app/
          | --- email.py
          | --- models.py
          | --- __init__.py
          | --- main/
                 | --- __init__.py
                 | --- routes.py
                 | --- email.py
          | --- auth/
                 | --- __init__.py
                 | --- routes.py
          | --- templates/
                 | --- auth/
                        | --- register.html

Create a mail object in the application factory:

# app/__init.py

from flask import Flask
from flask_mail import Mail
# ...

mail = Mail()
# ...


def create_app():
    app = Flask(__name__)
    # ...

    mail.init_app(app)
    # ...

Create an email module which will handle all email support needs of your application as seen below:

# app/email.py

from threading import Thread
from flask import current_app
from flask_mail import Message
from app import mail


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=(current_app._get_current_object(), msg)).start()

Above, I have imported mail we created in the __init__.py file. Since we are using a factory function, I import current_app from flask which will help to access the application's configuration variables. These variables are needed to complete email support. Threading ensures that the application is not slowed down when the execution of email setup is in progress.

I am making an assumption that you want to send an email to a user who has registered. So, in the auth package, you will need to create a helper method to send the email to a user.

# app/auth/email.py

from flask import render_template, current_app
from app.email import send_email


def send_congrats_email(user):
    send_email('[Congrats] You are registered'),
               sender=current_app.config['ADMINS'][0],
               recipients=[user.email],
               text_body=render_template('email/reset_password.txt',
                                         user=user),
               html_body=render_template('email/reset_password.html',
                                         user=user)

In your auth/routes.py, create a view function for registration:

# app/auth/routes.py

from app.auth.email import send_congrats_email

@bp.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        send_congrats_email(user)
        flash('Check your email for our congrats message')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', title='Register',
                           form=form)

Ensure your email configurations are set up in the config module:

import os 
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(basedir, '.env')


class Config(object):
    # Database configuration
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL?ssl=require') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    # Form protection
    SECRET_KEY = os.environ.get('SECRET_KEY')

    # Email configuration
    MAIL_SERVER = os.environ.get('MAIL_SERVER')
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    ADMINS = os.environ.get('ADMINS')

With this, your email message (seen in the templates found in app/templates/email/), will be sent to a newly registered user when the register() view function is invoked.

Upvotes: 0

J&#252;rgen Gmach
J&#252;rgen Gmach

Reputation: 6131

The solution is a two-phase initialization, which almost all Flask extensions support:

from flask import Flask
from flask_mail import Mail

mail = Mail()


def create_app():
    app = Flask(__name__)
    ...
    mail.init_app(app)
    ...

This allows you to import mail from another module.

I just recently implemented email verification, and I followed this old, but still mostly valid tutorial:

http://www.patricksoftwareblog.com/confirming-users-email-address/

Upvotes: 0

Related Questions