Sending Email (Flask-Mail)
Reviewed & published by Brayan K
Sending email means building a message with a subject, recipients, and a body, then handing it to an SMTP server that delivers it to the inbox.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll build real email payloads with Python's standard library, add HTML bodies and attachments, send one from a route, and see the Flask-Mail equivalent.
What You'll Learn in This Lesson
- Building a message: subject, from, recipients, body
- Adding an HTML body with a plain-text fallback
- Attaching a file to a message
- Configuring an SMTP mail server
- Sending email inside a Flask route
- The Flask-Mail Message and mail.send() API
1 Building a Message
Every email is just structured data: a subject, a from address, one or more recipients, and a body. Flask-Mail's Message object holds exactly these fields. Under the hood it's the same structure as Python's standard-library EmailMessage, which we can build and inspect runnably.
The example below assembles a welcome email and prints its headers and body — this is precisely the payload that would be handed to an SMTP server.
# Building an email message with the standard library
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Welcome to LearnCodingFast!"
msg["From"] = "noreply@example.com"
msg["To"] = "ada@example.com"
msg.set_content("Thanks for signing up. Happy coding!") # plain-text body
# Inspect the built message (this is exactly what SMTP would send):
print(msg["Subject"]) # Welcome to LearnCodingFast!
print(msg["From"], "->", msg["To"]) # noreply@example.com -> ada@example.com
print(msg.get_content().strip()) # Thanks for signing up. Happy coding!
# ✅ Expected output:
# Welcome to LearnCodingFast!
# noreply@example.com -> ada@example.com
# Thanks for signing up. Happy coding!The printout shows the subject, the from/to pair, and the body — the four pieces every message needs before it can be sent.
2 HTML Bodies & Attachments
Real product emails are formatted. You send an HTML version for clients that render it and keep a plain-text version as a fallback. Technically this is a multipart/alternative message holding a text/plain and a text/html part.
The runnable example sets the plain body with set_content and adds the HTML with add_alternative, then prints the resulting multipart structure to prove both parts are present.
# HTML body with a plain-text fallback (multipart/alternative)
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Your receipt"
msg["From"] = "billing@example.com"
msg["To"] = "ada@example.com"
# set_content = plain text; add_alternative(subtype="html") = HTML version
msg.set_content("Your order #42 totals $9.99.")
msg.add_alternative(
"<h1>Thanks!</h1><p>Order <b>#42</b> totals $9.99.</p>",
subtype="html",
)
print("content type:", msg.get_content_type()) # content type: multipart/alternative
for part in msg.iter_parts():
print("-", part.get_content_type()) # - text/plain then - text/html
# ✅ Expected output:
# content type: multipart/alternative
# - text/plain
# - text/html3 Sending Email From a Route
The common case is sending mail in response to a user action — a contact form, a password reset, an order confirmation. You read the form data, build the message, and send it. The runnable example does all of that against an in-memory "outbox" so you can see it work end to end, and even shows an attachment.
# Sending email inside a Flask request (stdlib build, no real SMTP)
from flask import Flask, request, jsonify
from email.message import EmailMessage
app = Flask(__name__)
sent_box = [] # stands in for the mail server's outbox
def build_and_send(to, subject, body, attachment=None):
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = "noreply@example.com"
msg["To"] = to
msg.set_content(body)
if attachment:
name, data = attachment
# attach raw bytes with an explicit type and filename
msg.add_attachment(data, maintype="application", subtype="octet-stream", filename=name)
sent_box.append(msg) # in production: smtp.send_message(msg)
return msg
@app.route("/contact", methods=["POST"])
def contact():
email = request.form.get("email", "")
build_and_send(email, "We got your message", "Thanks for reaching out!")
return jsonify(status="queued", to=email), 202
# Post the form and confirm the message landed in the outbox:
with app.test_client() as c:
r = c.post("/contact", data={"email": "ada@example.com"})
print(r.status_code, r.get_json()) # 202 {'status': 'queued', 'to': 'ada@example.com'}
print("outbox size:", len(sent_box)) # outbox size: 1
print("subject:", sent_box[0]["Subject"]) # subject: We got your message
# Attachment demo (bytes payload):
msg = build_and_send("ada@example.com", "Report", "See attached.", attachment=("report.csv", b"row1\nrow2"))
print("attachments:", [p.get_filename() for p in msg.iter_attachments()]) # ['report.csv']
# app.run() would serve this forever, so it goes last and stays commented
# out — the test client above is how Flask itself exercises routes:
# if __name__ == "__main__":
# app.run(debug=True)
# ✅ Expected output:
# 202 {'status': 'queued', 'to': 'ada@example.com'}
# outbox size: 1
# subject: We got your message
# attachments: ['report.csv']🔬 Flask-Mail Cheat Sheet
| You want to... | Use |
|---|---|
| Create a message | Message(subject, recipients=[...]) |
| Plain body | msg.body = "..." |
| HTML body | msg.html = "<p>...</p>" |
| Attach a file | msg.attach(name, type, data) |
| Send it | mail.send(msg) |
🎯 Your Turn: Fill in the Blanks
Complete the message below. Replace each ___ to set the recipient and the body.
# 🎯 YOUR TURN: replace each ___ to finish the message
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Password reset"
msg["From"] = "noreply@example.com"
msg["___"] = "bob@example.com" # which header is the recipient?
msg.___("Click the link to reset your password.") # which method sets the body?
print(msg["To"]) # bob@example.com
print(msg.get_content().strip()) # Click the link to reset your password.Common Errors & Quick Reference
Your MAIL_USERNAME/MAIL_PASSWORD are wrong, or the provider needs an app-specific password. For Gmail you must create an app password — your normal login won't work over SMTP.
❌ "Working outside of application context"
You called mail.send() outside a request or app context (for example in a background thread). Wrap it in with app.app_context(): so Flask-Mail can read your config.
| Config key | Purpose |
|---|---|
| MAIL_SERVER | SMTP host (e.g. smtp.gmail.com) |
| MAIL_PORT | 587 for TLS, 465 for SSL |
| MAIL_USE_TLS | Encrypt the connection |
| MAIL_USERNAME | Login for the SMTP server |
🏆 Mini Challenge: A Newsletter Builder
Build personalized newsletters for a list of subscribers.
- Write a function that builds one message for a recipient and name
- Personalize the body with the subscriber's name
- Build the whole batch into an outbox list
# 🏆 Mini Challenge: a bulk newsletter builder
from email.message import EmailMessage
def make_newsletter(recipient, name):
msg = EmailMessage()
msg["Subject"] = "This week in Python"
msg["From"] = "news@example.com"
msg["To"] = recipient
msg.set_content(f"Hi {name}, here is your weekly digest!")
return msg
recipients = [("ada@example.com", "Ada"), ("bob@example.com", "Bob")]
outbox = [make_newsletter(addr, name) for addr, name in recipients]
print("built:", len(outbox)) # built: 2
for m in outbox:
print(m["To"], "|", m.get_content().strip())
# ada@example.com | Hi Ada, here is your weekly digest!
# bob@example.com | Hi Bob, here is your weekly digest!❓ Frequently Asked Questions
Lesson complete — your app can reach the inbox!
You built real email payloads with subject, from, recipients, and body, added HTML with a text fallback and an attachment, sent one from a route, and saw the Flask-Mail API.
Practice quiz
Which extension is commonly used to send email from Flask?
- Flask-Mailer
- Flask-Mail
- Flask-SMTP
- Flask-Inbox
Answer: Flask-Mail. Flask-Mail provides the Mail object and Message class for sending email.
What protocol delivers email between mail servers?
- SMTP
- HTTP
- FTP
- IMAP-send
Answer: SMTP. SMTP (Simple Mail Transfer Protocol) accepts and relays outgoing mail.
How do you create a message in Flask-Mail?
- Mail(subject, ...)
- Email(subject, ...)
- Message(subject, recipients=[...])
- send(subject, ...)
Answer: Message(subject, recipients=[...]). Message(subject, recipients=[...]) builds the email; mail.send delivers it.
Which method actually delivers a Flask-Mail message?
- msg.deliver()
- msg.post()
- Mail.dispatch(msg)
- mail.send(msg)
Answer: mail.send(msg). mail.send(msg) hands the message to the configured SMTP server.
Which config key sets the SMTP host for Flask-Mail?
- MAIL_SERVER
- MAIL_HOST
- SMTP_SERVER
- MAIL_URL
Answer: MAIL_SERVER. MAIL_SERVER names the SMTP host, e.g. smtp.gmail.com.
How do you set the rich HTML body on a Flask-Mail Message?
- msg.render = ...
- msg.html = '<p>...</p>'
- msg.content_html(...)
- msg.set_html(...)
Answer: msg.html = '<p>...</p>'. msg.html holds the HTML version; msg.body holds the plain-text fallback.
Which Message attribute holds the plain-text fallback?
- msg.text
- msg.plain
- msg.body
- msg.content
Answer: msg.body. msg.body is the plain-text version shown by clients that do not render HTML.
How do you attach a file to a Flask-Mail message?
- msg.file(name, data)
- msg.add_file(data)
- msg.append(name, data)
- msg.attach(name, content_type, data)
Answer: msg.attach(name, content_type, data). msg.attach(filename, content_type, data) adds the file's bytes to the message.
Which port is typically used for SMTP over TLS?
- 587
- 21
- 8080
- 3306
Answer: 587. Port 587 is standard for SMTP with TLS (465 for SSL).
Why enqueue email as a background task for bulk sending?
- SMTP is free only in background
- email cannot run in a route
- an SMTP send can take seconds and block the request
- Flask-Mail requires Celery
Answer: an SMTP send can take seconds and block the request. Slow SMTP round-trips block the request; a worker lets the response return immediately.
Continue this course
- Previous: Real-Time with WebSockets (Flask-SocketIO)
- Next: Logging & Monitoring — Configure app.logger, structured logs, and error reporting
- Quick reference: Flask cheat sheet
Frequently asked questions
How does a Flask app send email?
It builds a message — subject, from, recipients, and a body — and hands it to an SMTP server that delivers it. Flask-Mail wraps this in a Mail object and a Message class so you configure your mail server once and call mail.send(message) wherever you need to notify a user.
What is SMTP and what config does Flask-Mail need?
SMTP (Simple Mail Transfer Protocol) is how mail servers accept and relay messages. Flask-Mail reads MAIL_SERVER, MAIL_PORT, MAIL_USE_TLS, MAIL_USERNAME, and MAIL_PASSWORD from your config. For Gmail or a provider like SendGrid/Mailgun you point those at the provider's SMTP host and credentials.
How do I send both HTML and plain text?
Set the plain-text fallback in Message.body and the rich version in Message.html. Mail clients that render HTML show the HTML; those that don't fall back to the text. In the standard library this is a multipart/alternative message with a text/plain and a text/html part.
How do I add an attachment?
Call message.attach(filename, content_type, data) in Flask-Mail, passing the file's bytes and its MIME type. The standard library equivalent is msg.add_attachment(data, maintype=..., subtype=..., filename=...). Either way the attachment rides along with the message body.
Should I send email directly in the request?
Sending email blocks on an SMTP round-trip, which can take seconds. For one-off transactional mail it's acceptable, but for anything slow or bulk, enqueue it as a background task (Celery/RQ) so the request returns immediately and a worker does the sending.