Sending Email with Flask-Mail

Registration, password resets, and notifications all eventually need to reach a user somewhere the browser can't — their inbox. Flask has no email-sending code built in, so Flask-Mail is the extension that fills the gap, wrapping Python's standard SMTP support in a small API that fits naturally alongside everything else this track has wired into Bookshelf's app factory.

Installing and Initializing Flask-Mail

pip install flask-mail

Following the same extensions-module pattern used for Flask-SQLAlchemy and Flask-Login, create an unbound Mail instance and initialize it inside the app factory:

# extensions.py
from flask_mail import Mail

mail = Mail()
# app.py
from flask import Flask
from extensions import db, login_manager, mail

def create_app():
    app = Flask(__name__)
    app.config.from_object('config.Config')

    db.init_app(app)
    login_manager.init_app(app)
    mail.init_app(app)

    return app

Configuring SMTP Settings

Flask-Mail reads its SMTP connection details from app.config, the same configuration object covered in Configuration & Environments:

SettingPurpose
MAIL_SERVERThe SMTP host to connect to, e.g. smtp.gmail.com
MAIL_PORTThe SMTP port, commonly 587 (TLS) or 465 (SSL)
MAIL_USE_TLSWhether to upgrade the connection to TLS — True for most providers on port 587
MAIL_USERNAMEThe account used to authenticate with the SMTP server
MAIL_PASSWORDThe password (or app-specific password/API key) for that account
# config.py
import os

class Config:
    MAIL_SERVER = os.environ.get('MAIL_SERVER')
    MAIL_PORT = int(os.environ.get('MAIL_PORT', 587))
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
Note: MAIL_USERNAME and MAIL_PASSWORD are credentials — exactly the kind of sensitive value that belongs in environment variables, never hardcoded or committed to version control, per Configuration & Environments.

Sending a Plain-Text Email

Flask-Mail's Message class represents one email — subject, recipients, and body — and mail.send() hands it off to the configured SMTP server:

# auth/views.py
from flask_mail import Message
from extensions import mail

def send_welcome_email(user):
    msg = Message(
        'Welcome to Bookshelf',
        recipients=[user.email],
    )
    msg.body = f"Hi {user.username}, thanks for joining Bookshelf! Start by adding your first book."
    mail.send(msg)

msg.body is what most email clients show for a plain-text message — no markup, just readable text.

Sending HTML Email Instead

Setting msg.html instead of (or alongside) msg.body sends a formatted HTML email — most email clients prefer the HTML version when both are present, falling back to the plain-text body only if HTML rendering isn't supported:

# auth/views.py
msg = Message('Welcome to Bookshelf', recipients=[user.email])
msg.body = f"Hi {user.username}, thanks for joining Bookshelf!"  # plain-text fallback
msg.html = f"<p>Hi <strong>{user.username}</strong>, thanks for joining Bookshelf!</p>"
mail.send(msg)

Hardcoding HTML inside a Python string works for a one-liner like this, but it stops scaling the moment an email needs real formatting — a logo, styled buttons, more than a sentence of copy.

Rendering an Email Body from a Template

Since Bookshelf already has a working Jinja2 setup from Jinja2 Templates, the cleaner approach is to write the email as an ordinary template and render it with render_template() — exactly like rendering a page, except the rendered string becomes msg.html instead of an HTTP response:

<!-- templates/email/welcome_email.html -->
<!DOCTYPE html>
<html>
<body>
    <h1>Welcome to Bookshelf, {{ username }}!</h1>
    <p>Your account is ready. Start by creating a shelf and adding your first book.</p>
</body>
</html>
# auth/views.py
from flask import render_template
from flask_mail import Message
from extensions import mail

def send_welcome_email(user):
    msg = Message('Welcome to Bookshelf', recipients=[user.email])
    msg.html = render_template('email/welcome_email.html', username=user.username)
    mail.send(msg)

This keeps email content editable as ordinary HTML/Jinja2, with the same {{ }} variable substitution used everywhere else in Bookshelf's templates, instead of buried inside Python string formatting.

Putting It Together: A Welcome Email After Registration

The natural place to trigger this is right after the registration view from Registration & Password Hashing commits the new user:

# auth/views.py
@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()

    if form.validate_on_submit():
        new_user = User(
            username=form.username.data,
            email=form.email.data,
            password_hash=generate_password_hash(form.password.data),
        )
        db.session.add(new_user)
        db.session.commit()

        send_welcome_email(new_user)  # the function defined above
        login_user(new_user)
        return redirect(url_for('book_list'))

    return render_template('auth/register.html', form=form)

Testing Email Locally Without Sending Real Mail

Firing real emails every time you run a test suite (or click through registration a dozen times while debugging) is slow and, worse, spams whatever inbox you used for testing. Flask-Mail has a config flag built exactly for this:

# in a test setup
app.config['MAIL_SUPPRESS_SEND'] = True  # mail.send() runs, but nothing is actually sent

For manually clicking through the app in development without a real SMTP account, Python's built-in debugging SMTP server is a quick alternative — run python -m smtpd -c DebuggingServer -n localhost:1025 in a terminal, point MAIL_SERVER/MAIL_PORT at localhost:1025, and every "sent" email prints to that terminal instead of leaving your machine.

Production Reality: Transactional Email Services

Raw SMTP through a personal or generic mail account is fine for learning and small projects, but it runs into real problems at production scale — rate limits, and messages landing in spam folders because the sending server has no reputation with mailbox providers. Most production Flask apps instead send through a transactional email service — SendGrid and Mailgun are the two most common — which handle deliverability, bounce tracking, and analytics, and which Flask-Mail can typically still be pointed at via their SMTP credentials with no code changes beyond the config values above.

Coming up next: Security Best Practices pulls together the security threads from this whole section — hashing, CSRF, sessions — into one reference page.