My Flask Email Verification Flow: How I Verify Emails Step-by-Step

On May 05, 2025
12min read
Ivan Djuric, an author at Mailtrap
Ivan Djuric Technical Content Writer @Mailtrap
Artem Litvinenko Software Engineer

In this tutorial, I will break down my Flask email verification flow. Namely, I’ll show you how to:

  • Authenticate new users that sign up for your app/project [jump ahead]
    • Best for: eCommerce stores, SaaS, etc.
  • Verify users that submit your contact form [jump ahead]
    • Best for: Simple contact forms

And, since there’s a lot of code involved, I’ll also break down my email testing flow, so you can ensure your sending functionality works as expected. 

Disclaimer: Every line code in this article has been prepared by a developer and thoroughly tested before publication. It works with Python 3.7 and above.

Ready to deliver your emails?
Try Mailtrap for Free

Setting up Flask

Before working on your Flask app, I strongly recommend creating and activating a virtual environment, an industry-standard practice that isolates the required dependencies and prevents conflicts.

To create a virtual environment, use the following command:

python -m venv venv

And to activate it, use:

# Windows
venv\Scripts\activate

# Mac/Linux
source venv/bin/activate

Then, regardless of whether you go for user authentication or contact form verification, you will need the following:

  • Flask 3.1.0 or above – The core web framework. 
  • Flask-SQLAlchemy – Flask extension that adds support for SQLAlchemy.
  • Python-dotenv – reads key-value pairs from a .env file and can set them as environment variables.
  • ItsDangerous – For safely passing data and signing verification tokens.
  • Flask-Mail – A Flask extension that lets us send emails with just a few lines of code.
  • An SMTP service – Personally, I’ll be using Mailtrap SMTP throughout the article since it reliably sends emails for me, even on the generous free plan.
  • A database – Needed to store user details and verification statuses. You can use:
    • SQLite – Built-in, suitable for smaller projects.
    • PostgreSQL or MySQL – Recommended for production
    • SQLAlchemy – What I personally use, since I find it an efficient way to manage the database (e.g., user data handling). Plus, it works with databases, prevents SQL injection, and provides an ORM (Object-Relational Mapping).
  • Flash messages flash – For displaying success/error messages in Flask.
  • URL routing url_for– To handle verification links.
  • A frontend (Jinja2 templates) – To display signup forms, verification messages, and success/error pages.

Email verification in Flask

Email verification essentially checks whether there’s a real person behind a user address by sending them an email with a link they need to click on.

Typically, people submit their user’s email addresses when:

  • Registering for websites or apps – User authentication
  • Submitting contact forms – Contact form verification

So, choose your flow based on whether you have a project/app or a contact form. 🙂 

User authentication

The project structure we’re aiming for looks something like this:

/flask_auth_app
│── .env                 # Environment variables (env with SMTP credentials and secret keys)
│── app.py               # Main Flask app
│── config.py            # Configuration settings (loads from .env)
│── models.py            # User model (database)
│── email_utils.py       # Email & token handling
│── templates/
│   ├── signup.html      # Signup form
│   ├── verify_email.html # Email verification template
│   ├── success.html      # Successful verification page
│── static/              # CSS, JS (optional)
│── database.db          # SQLite database (if used)

Now, let’s get down to some coding!

1. Set up config.py

The config.py file contains app settings, such as Flask secret key, database configuration, and other email settings:

import os #  for loading SMTP credentials from environment variables
from dotenv import load_dotenv

# Load environment variables (optional)
load_dotenv()

# Flask App Configuration
SECRET_KEY = os.getenv("SECRET_KEY", "your_default_secret_key")

# Flask-Mail Configuration
MAIL_SERVER = os.getenv("MAIL_SERVER", "smtp.example.com")
MAIL_PORT = int(os.getenv("MAIL_PORT", 587))
MAIL_USE_TLS = True  # Use TLS for most SMTP providers
MAIL_USE_SSL = False  # Use SSL only if required (never enable both)
MAIL_USERNAME = os.getenv("MAIL_USERNAME", "your_email@example.com")
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD", "your_email_password")
MAIL_DEFAULT_SENDER = "no-reply@your_domain"

# SQLAlchemy Database Configuration
SQLALCHEMY_DATABASE_URI = "sqlite:///database.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False

Notes: Don’t forget to replace the placeholders with your actual SMTP credentials.

Additionally, if you’re using MAIL_USE_TLS = True, which is necessary for secure email sending, you shouldn’t set MAIL_USE_SSL, since only one should be enabled at a time. Additionally, MAIL_USE_SSL should be used only if your SMTP provider specifically requires it.

2. Define environment variables in .env file
Copy/paste the following code in your .env file and replace variables with actual Mailtrap credentials:

SECRET_KEY=your_default_secret_key
MAIL_SERVER=live.smtp.mailtrap.io
MAIL_PORT=587
MAIL_USERNAME=api
MAIL_PASSWORD=your_email_password

3. Define the user model in models.py

Next, we need to create a user model using SQLAlchemy, store email, hashed password, and verification status and set up database connection. All of this happens in the user model in models.py, which we need to define:

from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(100), unique=True, nullable=False)
    password = db.Column(db.String(255), nullable=False)  # Store hashed passwords
    verified = db.Column(db.Boolean, default=False)  # Email verification status

    def set_password(self, password):
        self.password = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password, password)

4. Mail initialization in mail.py

The mail.py file is responsible for initializing the mail object which we will later use to perform email sending in Flask:

from flask_mail import Mail

mail = Mail()

5. Token & email handling in email_utils.py

The email_utils.py file is responsible for handling verification token generation and validation, as well as sending the actual verification emails.

Here, we’ll use itsdangerous to generate secure tokens, implement a function to verify tokens, and set up an email-sending logic using Flask-Mail:

import logging
import traceback
from flask_mail import Message
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature
from config import SECRET_KEY
from mail import mail


# Set up logging configuration
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# Token Serializer
s = URLSafeTimedSerializer(SECRET_KEY)

def generate_token(email):
    """Generate a time-limited token for email verification."""
    return s.dumps(email, salt="email-confirm")

def confirm_token(token, expiration=3600):
    """Validate the token and extract the email if valid."""
    try:
        return s.loads(token, salt="email-confirm", max_age=expiration)
    except SignatureExpired:
        logging.warning("Verification token expired.")
        return False  # Token expired
    except BadSignature:
        logging.warning("Invalid verification token.")
        return False  # Token is invalid

def send_verification_email(to, verify_url):
    """Send the verification email with a secure link."""
    try:
        msg = Message(
            subject="Verify Your Account",
            recipients=[to],
            html=f"""
            <p>Click the link below to verify your email:</p>
            <p><a href="{verify_url}">{verify_url}</a></p>
            """
        )
        mail.send(msg)
        logging.info(f"Verification email sent to {to}.")
    except Exception as e:
        logging.error(f"Error sending email to {to}: {str(e)}")
        logging.error(traceback.format_exc())  # Logs full error traceback

6. Configure main app file (e.g., app.py)

Our main application file basically ties everything together, since it initializes Flask, SQLAlchemy, and Flask-Mail. It also creates signup and verification routes and handles user registration and email verification.

Here’s a code snippet example you can copy:

from flask import Flask, render_template, request, redirect, url_for, flash
from sqlalchemy import inspect
from models import db, User
from email_utils import generate_token, confirm_token, send_verification_email
from mail import mail


app = Flask(__name__)
app.config.from_pyfile("config.py")

# Initialize extensions
mail.init_app(app)
db.init_app(app)

# Create database tables if they do not exist
with app.app_context():
    inspector = inspect(db.engine)
    if "user" not in inspector.get_table_names():
        db.create_all()

@app.route("/signup", methods=["GET", "POST"])
def signup():
    if request.method == "POST":
        email = request.form["email"]
        password = request.form["password"]

        if User.query.filter_by(email=email).first():
            flash("Email already registered.", "danger")
            return redirect(url_for("signup"))

        new_user = User(email=email)
        new_user.set_password(password)  # Hash password
        db.session.add(new_user)
        db.session.commit()

        # Generate token & send email
        token = generate_token(email)
        verify_url = url_for("verify_email", token=token, _external=True)
        send_verification_email(email, verify_url)

        return "Please check your email for the verification link."

    return render_template("signup.html")

@app.route("/verify/<token>")
def verify_email(token): 
    email = confirm_token(token)
    if not email:
        return "Invalid or expired verification link."

    user = User.query.filter_by(email=email).first()
    if user and not user.verified:
        user.verified = True
        db.session.commit()
        return "Your account has been verified!"
    else:
        return "Account already verified."

if __name__ == "__main__":
    app.run(debug=True)

7. Create a signup form (e.g. templates/signup.html)

As you’re reading this article, I am pretty sure you already have a signup form, but just in case you don’t, here’s a basic template you can use:

<!DOCTYPE html>
<html>
<head>
    <title>Sign Up</title>
</head>
<body>
    <h2>Sign Up</h2>
    <form method="POST">
        <input type="email" name="email" placeholder="Enter email" required>
        <input type="password" name="password" placeholder="Enter password" required>
        <button type="submit">Sign Up</button>
    </form>
    {% with messages = get_flashed_messages(with_categories=True) %}
        {% if messages %}
            {% for category, message in messages %}
                <p class="{{ category }}">{{ message }}</p>
            {% endfor %}
        {% endif %}
    {% endwith %}
</body>
</html>

8. Create an email template (e.g., templates/verify_email.html)

Then, a little bit more frontend action because we need to create an HTML email template that will contain our verification link.

Here’s a basic one you can tweak according to your liking:

<!DOCTYPE html>
<html>
<head>
    <title>Email Verification</title>
</head>
<body>
    <p>Hi,</p>
    <p>Click the link below to verify your email address:</p>
    <p><a href="{{ verify_url }}">{{ verify_url }}</a></p>
    <p>If you did not sign up, you can ignore this email.</p>
</body>
</html>

9. Run the code and test the authentication logic

Execute the following command:

python app.py

Or, if you’re using Flask’s CLI:

export FLASK_APP=app.py  # Mac/Linux
set FLASK_APP=app.py      # Windows

flask run

Lastly, open http://127.0.0.1:5000/signup in your browser and create an account. Next, check your email for the verification link, click on it, and confirm the success message. For extra points, you can try logging in to verify the authentication flow. 

10. (optional) Debugging in case of errors 

To debug, check for missing dependencies with:

pip freeze

And that’s it, you have successfully implemented user authentication logic! 

If you wish to add additional layers of security to your web app or Flask contact form, consider adding email validation logic to your sign up form to protect yourself against spammers and bots.

Contact form verification

The project structure for contact form verification we’re aiming for looks something like this:

/flask_contact_verification
│── .env                 # Environment variables (SMTP credentials, secret keys)
│── app.py               # Main Flask app
│── config.py            # Configuration settings (loads from .env)
│── email_utils.py       # Email & token handling
│── templates/
│   ├── contact.html      # Contact form
│   ├── verify_email.html # Email verification template
│   ├── success.html      # Message success page
│── static/              # CSS, JS (optional)
│── database.db          # SQLite database (if used)

P.S. Our awesome YouTube team has prepared a short and sweet guide on email verification in Flask, so be sure to check it out! ⬇️

1. Set up config.py

The config.py is the heart of our email-sending configuration and contains the SMTP details Flask needs to send verification messages.

And here’s a code snippet you can copy/paste:

import os
from dotenv import load_dotenv

# Load environment variables (optional)
load_dotenv()

# Flask App Configuration
SECRET_KEY = os.getenv("SECRET_KEY", "your_default_secret_key")

# Flask-Mail Configuration
MAIL_SERVER = os.getenv("MAIL_SERVER", "smtp.example.com")
MAIL_PORT = int(os.getenv("MAIL_PORT", 587))
MAIL_USE_TLS = True  # Use TLS for most SMTP providers
MAIL_USE_SSL = False  # Use SSL only if required (never enable both)
MAIL_USERNAME = os.getenv("MAIL_USERNAME", "your_email@example.com")
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD", "your_email_password")
MAIL_DEFAULT_SENDER = "no-reply@your_domain"

Note: Don’t forget to replace the SMTP placeholders with your own credentials (or define them in .env in the step below). You can even use Gmail for this, but due to its limitations, I recommend Mailtrap again. 🙂

2. Define environment variables

Create an .env file, paste the following code snippet, and insert your Mailtrap credentials:

SECRET_KEY=your_default_secret_key
MAIL_SERVER=live.smtp.mailtrap.io
MAIL_PORT=587
MAIL_USERNAME=api
MAIL_PASSWORD=your_email_password

3. Initialize the mail object

The mail.py file is responsible for initializing the mail object which we will later use to perform email sending in Flask:

from flask_mail import Mail

mail = Mail()

4. Set up email verification logic in email_utils.py

Next, let’s generate a secure token and add our sending logic along with the email content:

import logging
import traceback
import re
from flask_mail import Message
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature
from config import SECRET_KEY
from mail import mail

# Set up logging configuration
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# Token Serializer
s = URLSafeTimedSerializer(SECRET_KEY)

TOKEN_EXPIRATION_SECONDS = 900  # 15 minutes

def generate_token(email):
    """Generate a time-limited token for email verification."""
    return s.dumps(email, salt="email-confirm")

def confirm_token(token, expiration=TOKEN_EXPIRATION_SECONDS):
    """Validate the token and extract the email if valid."""
    try:
        return s.loads(token, salt="email-confirm", max_age=expiration)
    except SignatureExpired:
        logging.warning("Verification token expired. Ask the user to request a new verification email.")
        return False
    except BadSignature:
        logging.warning("Invalid verification token.")
        return False

def send_verification_email(to, verify_url):
    """Send the verification email with a secure link."""
    try:
        msg = Message(
            subject="Verify Your Email",
            recipients=[to],
            html=f"""
            <p>Click the link below to verify your email before your message is processed:</p>
            <p><a href="{verify_url}">{verify_url}</a></p>
            <p>If you did not submit this request, ignore this email.</p>
            """
        )
        mail.send(msg)
        logging.info(f"Verification email sent to {to}.")
    except Exception as e:
        logging.error(f"Failed to send email to {to}: {str(e)}")
        logging.error(f"Traceback for {to}:\n{traceback.format_exc()}")
        logging.warning("Possible causes: incorrect SMTP credentials, network issues, or email provider blocking.")

def is_valid_email(email):
    """Simple email validation function"""
    email_regex = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
    return re.match(email_regex, email)

5. Configure main app file (e.g., app.py)

Our main app file, app.py, boots the Flask app, configures email handling, and sets up routes for both the contact form and verification.

And here’s the code snippet you can copy/paste:

from flask import Flask, render_template, request, redirect, url_for, flash
from email_utils import generate_token, confirm_token, send_verification_email, is_valid_email
from mail import mail

app = Flask(__name__)
app.config.from_pyfile("config.py")

# Initialize Flask-Mail
mail.init_app(app)


@app.route("/contact", methods=["GET", "POST"])
def contact():
    """Handles contact form submission and sends verification email."""
    if request.method == "POST":
        name = request.form["name"]
        email = request.form["email"]
        message = request.form["message"]

        if not name or not email or not message:
            flash("All fields are required!", "danger")
            return redirect(url_for("contact"))

        if not is_valid_email(email):
            flash("Invalid email format. Please enter a valid email.", "danger")
            return redirect(url_for("contact"))

        # Generate token and send verification email
        token = generate_token(email)
        verify_url = url_for("verify_email", token=token, _external=True)
        send_verification_email(email, verify_url)

        flash("A verification email has been sent. Please check your inbox.", "success")
        return redirect(url_for("contact"))

    return render_template("contact.html")

@app.route("/verify/<token>")
def verify_email(token):
    """Handles email verification after clicking the link."""
    email = confirm_token(token)
    if not email:
        flash("Invalid or expired verification link.", "danger")
        return redirect(url_for("contact"))

    flash("Your email has been verified. Your message has been submitted!", "success")
    return redirect(url_for("contact"))

if __name__ == "__main__":
    app.run(debug=True)

6. Create a contact form (e.g., templates/contact.html)

If you’re reading this, you probably have it, but just in case, here’s a simple code for a simple contact form:

<!DOCTYPE html>
<html>
<head>
    <title>Contact Us</title>
</head>
<body>
    <h2>Contact Us</h2>
    <form method="POST">
        <input type="text" name="name" placeholder="Your Name" required>
        <input type="email" name="email" placeholder="Your Email" required>
        <textarea name="message" placeholder="Your Message" required></textarea>
        <button type="submit">Submit</button>
    </form>
    {% with messages = get_flashed_messages(with_categories=True) %}
        {% if messages %}
            {% for category, message in messages %}
                <p class="{{ category }}">{{ message }}</p>
            {% endfor %}
        {% endif %}
    {% endwith %}
</body>
</html>

And if you want to check out how we do it at Mailtrap, be sure to check out the cool video we have on making a contact form in Flask:

7. Create email verification template (e.g., templates/verify_email.html)

For the user email that will hold the verification link, we’ll use the following template:

<!DOCTYPE html>
<html>
<head>
    <title>Email Verification</title>
</head>
<body>
    <p>Hi,</p>
    <p>Click the link below to verify your email before your message is processed:</p>
    <p><a href="{{ verify_url }}">{{ verify_url }}</a></p>
    <p>If you did not submit this request, ignore this email.</p>
</body>
</html>

8. Run the code and test the verification logic

Finally, let’s run the code with:

python app.py

Or, you can also set up Flask’s debug mode:

export FLASK_APP=app.py   # Mac/Linux
set FLASK_APP=app.py       # Windows

flask run

Lastly, open http://127.0.0.1:5000/contact in your browser, fill out the contact form, and once you hit submit, you should see a status update in your console/logs. Then, click the email verification link in the email and confirm the success message.

Email verification as a part of email testing

Now, as you might have noticed for yourself, there’s a lot of code needed for email verification, meaning there’s also a lot of things that can go wrong. And since verification emails are a part of a new user onboarding process, it’s crucial that they look the way you intended and that they’re performing the way they’re supposed to.

Just ask yourself, do you want to send faulty verification emails to new users who sign up for your website or fill out your contact form? I didn’t think so.

That’s why I always recommend email testing, which allows you to make sure:

  • Your emails will pass spam filters and hit the main inbox.
  • You won’t flood your recipients’ inboxes with testing emails.
  • Your domain or IP won’t get blacklisted for spam.

Mailtrap Email Testing, another inseparable part of Mailtrap Email Delivery Platform, with which I can do all of the above.

Namely, with Email Testing, I can inspect the HTML/CSS of my emails and easily remove/fix any faulty lines of code, preview how my messages look on different devices or email clients, and more. 

Then, I can solve a significant amount of potential email deliverability issues by keeping my spam score below 5. 

And if I need designs for verification messages, I can use Mailtrap Email Templates, which I can create, edit, and host on the platform itself. Then, I can easily test them with the Mailtrap Templates API before moving to production once everything is ready to be rolled out.

Once you run your app with this setup, you should get all of your emails in the Mailtrap Email Testing inbox. 

Wrapping up

That was quite a bit of code, wasn’t it? 😅

Fortunately, you can now verify users for both your web application and your contact forms in Flask, which should keep your email list spotless

Happy verification!

Ivan Djuric, an author at Mailtrap
Article by Ivan Djuric Technical Content Writer @Mailtrap

I’m a Technical Content Writer with 5 years of background covering email-related topics in tight collaboration with software engineers and email marketers. I just love to research and share actionable insights with you about email sending, testing, deliverability improvements, and more. Happy to be your guide in the world of emails!

Article by Artem Litvinenko Software Engineer

Software engineer with over 4 years of experience.