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

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/html

3 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 messageMessage(subject, recipients=[...])
Plain bodymsg.body = "..."
HTML bodymsg.html = "<p>...</p>"
Attach a filemsg.attach(name, type, data)
Send itmail.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 keyPurpose
MAIL_SERVERSMTP host (e.g. smtp.gmail.com)
MAIL_PORT587 for TLS, 465 for SSL
MAIL_USE_TLSEncrypt the connection
MAIL_USERNAMELogin for the SMTP server

🏆 Mini Challenge: A Newsletter Builder

Build personalized newsletters for a list of subscribers.

# 🏆 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

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.

Related lessons