Flask-Mail (Sending Emails)
Many apps need email:
- password reset
- email verification
- notifications
Flask-Mail is a popular extension.
Install
Section titled “Install”pip install Flask-MailBasic configuration
Section titled “Basic configuration”app.config.update(
MAIL_SERVER="smtp.gmail.com",
MAIL_PORT=587,
MAIL_USE_TLS=True,
MAIL_USERNAME=os.environ.get("MAIL_USERNAME"),
MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD"),
)Then:
from flask_mail import Mail
mail = Mail(app)Sending an email
Section titled “Sending an email”from flask_mail import Message
msg = Message(
subject="Welcome",
sender="noreply@example.com",
recipients=["user@example.com"],
body="Hello from Flask!",
)
mail.send(msg)Production tips
Section titled “Production tips”- Don’t send emails synchronously inside web requests (slow)
- Use background jobs (Celery/RQ) for sending
- Use environment variables for credentials
- Prefer transactional email providers (SendGrid/Mailgun)
Configuration is the whole setup
Section titled “Configuration is the whole setup”app.config.update(
MAIL_SERVER="smtp.example.com",
MAIL_PORT=587,
MAIL_USE_TLS=True,
MAIL_USERNAME=os.environ["MAIL_USERNAME"],
MAIL_PASSWORD=os.environ["MAIL_PASSWORD"], # never in the source
MAIL_DEFAULT_SENDER="bot@example.com",
)
mail = Mail(app)msg = Message("Welcome", recipients=["ada@example.com"])
msg.body = "Plain text body"
msg.html = "<p>HTML body</p>"
mail.send(msg)MAIL_DEFAULT_SENDER fills in From when a message does not set one — measured, the
captured message came back with sender='bot@example.com' without it being passed.
Test without sending anything
Section titled “Test without sending anything”app.config["MAIL_SUPPRESS_SEND"] = True
with mail.record_messages() as outbox:
send_welcome_email(user)
assert len(outbox) == 1
assert outbox[0].subject == "Welcome"
assert outbox[0].recipients == ["ada@example.com"]Measured: one message captured, with subject, sender, recipients and both bodies intact — and no SMTP connection opened. This is how the rest of this page was verified, and it is how email should be tested. A suite that really sends mail is slow, flaky, and eventually mails a real person.
flowchart TD
S["mail.send(msg)"] --> Q{"MAIL_SUPPRESS_SEND?"}
Q -->|"True"| R["recorded in the outbox
nothing leaves the machine"]
Q -->|"False"| C["open an SMTP connection"]
C --> W["wait for the server"]
W --> D["delivered, or an exception"]
What is actually transmitted
Section titled “What is actually transmitted”Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Subject line
From: bot@example.com
To: a@b.co
Date: Sun, 09 Aug 2026 19:14:47 +0530
Message-ID: <178628308768.15712...@host>A message carrying both body and html becomes multipart/mixed with a generated
boundary — measured. Clients that cannot render HTML fall back to the plain part, which
is why setting both is worth the extra line.
See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
How should email be covered in an automated test suite?
Measured one message captured with subject, sender, recipients and both bodies intact, and no SMTP connection opened. A suite that really sends mail is slow, flaky, and eventually mails a real person.
pch.quizShowAnswer
B — set MAIL_SUPPRESS_SEND and use mail.record_messages() to capture messages in memory — Measured one message captured with subject, sender, recipients and both bodies intact, and no SMTP connection opened. A suite that really sends mail is slow, flaky, and eventually mails a real person.
-
Why move mail.send() out of the request that triggered it?
A background thread is the minimum, and it needs its own app context. A real queue adds retries and visibility once mail is part of a business process.
pch.quizShowAnswer
B — it opens an SMTP connection and blocks until the server replies, so a slow or dead mail server becomes a slow or failed user action — A background thread is the minimum, and it needs its own app context. A real queue adds retries and visibility once mail is part of a business process.
-
A Message sets both body and html. What Content-Type does the transmitted message use?
Measured multipart with a boundary. Clients that cannot render HTML fall back to the plain part, which is why setting both is worth the extra line.
pch.quizShowAnswer
C — multipart/mixed with a generated boundary, so clients can choose — Measured multipart with a boundary. Clients that cannot render HTML fall back to the plain part, which is why setting both is worth the extra line.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading