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:
| Setting | Purpose |
|---|---|
MAIL_SERVER | The SMTP host to connect to, e.g. smtp.gmail.com |
MAIL_PORT | The SMTP port, commonly 587 (TLS) or 465 (SSL) |
MAIL_USE_TLS | Whether to upgrade the connection to TLS — True for most providers on port 587 |
MAIL_USERNAME | The account used to authenticate with the SMTP server |
MAIL_PASSWORD | The 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')
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.