Reputation: 71
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
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:
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
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
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