How to Send Emails in Flask via SMTP or Email API

Editorial Note: We may earn a commission when you visit links on our website.
How to send emails in flask

Does your Flask app need to send password resets, receipts, or contact form alerts? Every one of those starts with the same small piece of code that sends emails in Flask.

Flask doesn’t ship with email support. You pick an extension or an email API. Older tutorials disagree on which one to use.

I’ll show you how to send emails in Flask with SMTP or an API. These examples use Flask 3.1, Flask-Mail 0.10, Flask-Mailman 1.1, and the SendLayer SDK 1.1.

How to Send Emails in Flask

You send emails in Flask in one of 2 ways. You can connect to an SMTP server through an extension like Flask-Mail. Or you can call an email provider’s API over HTTPS with its Python SDK.

Both work in production. SMTP works with almost any email provider. An API gives you clearer errors and works where SMTP ports are blocked.

MethodInstallBest for
Flask-Mail (SMTP)pip install Flask-MailMost Flask apps. Simple and well documented
Flask-Mailman (SMTP)pip install flask-mailmanTeams who know Django’s mail API or want console/file backends
Email API (SendLayer SDK)pip install sendlayerHosts that block SMTP ports, or when you want typed errors and webhooks
Two ways to send emails in Flask, SMTP on port 587 or an HTTPS email API, both reaching the inbox

Prerequisites

Here’s what you need before you start:

  • Python 3.9 or newer: Flask 3.1 requires it. You can download it from python.org.
  • Basic Python knowledge: You should be comfortable with functions and pip.
  • An email service with SMTP or API access: I’m using SendLayer for this tutorial.

After you create your SendLayer account, authorize your sending domain. This adds the DNS records that help your emails reach the inbox instead of spam.

How to Install Flask

To install Flask, create a virtual environment and install Flask with pip. If you already have a Flask app, skip to sending email with SMTP.

First, create a virtual environment for the project:

python3 -m venv env

Then activate it:

source env/bin/activate  # On Windows use: env\Scripts\activate

Next, install Flask and python-dotenv. It loads your credentials from a .env file, so they stay out of your code.

pip install Flask python-dotenv

Create this project structure:

flask-email/ ├── app.py ├── .env ├── static/ └── templates/ └── email/ ├── confirm.html └── confirm.txt

Open app.py and add a minimal Flask app:

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/")
def home():
    return jsonify({"message": "Hello World"})

if __name__ == "__main__":
    app.run(debug=True)

app = Flask(__name__) creates the app. The @app.route() decorator maps a URL to the function below it.

debug=True shows detailed errors while you build. Turn it off in production, because debug pages expose server details.

How to Send Email in Flask with SMTP

For SMTP, I’ll use Flask-Mail. It wraps Python’s built-in smtplib and reads its settings from your Flask config. For raw smtplib, see our guide to sending emails in Python.

Step 1: Install Flask-Mail and Add Your SMTP Settings

Install the extension:

pip install Flask-Mail

Then add your SMTP settings to the .env file:

MAIL_SERVER=smtp.sendlayer.net MAIL_PORT=587 MAIL_USE_TLS=True MAIL_USE_SSL=False MAIL_USERNAME=your-smtp-username MAIL_PASSWORD=your-smtp-password MAIL_DEFAULT_SENDER=paulie@example.com

MAIL_DEFAULT_SENDER is the address every message comes from unless you set a different sender. Use an address on the domain you authorized in SendLayer.

Warning: Add .env to your .gitignore. Committing it to GitHub publishes your SMTP password.

To find your SendLayer credentials, log in to your SendLayer account. Then go to Settings » SMTP Credentials.

SendLayer SMTP Credentials tab showing the host, port, username, and password for a Flask app

The host and port are the same for every account. The Username and Password are unique to yours, so paste them into MAIL_USERNAME and MAIL_PASSWORD.

Step 2: Send a Plain Text Email

Now replace the contents of app.py with this:

import os

from dotenv import load_dotenv
from flask import Flask, jsonify, request
from flask_mail import Mail, Message

load_dotenv()

app = Flask(__name__)

app.config["MAIL_SERVER"] = os.getenv("MAIL_SERVER")
app.config["MAIL_PORT"] = int(os.getenv("MAIL_PORT", 587))
app.config["MAIL_USE_TLS"] = os.getenv("MAIL_USE_TLS") == "True"
app.config["MAIL_USE_SSL"] = os.getenv("MAIL_USE_SSL") == "True"
app.config["MAIL_USERNAME"] = os.getenv("MAIL_USERNAME")
app.config["MAIL_PASSWORD"] = os.getenv("MAIL_PASSWORD")
app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_DEFAULT_SENDER")

mail = Mail(app)


@app.route("/send-email", methods=["POST"])
def send_email():
    data = request.get_json()
    msg = Message(
        subject=data["subject"],
        recipients=[data["to"]],
        body=data["message"],
    )
    mail.send(msg)
    return jsonify({"message": "Email sent"})


if __name__ == "__main__":
    app.run(debug=True)  # Set debug=False in production

Code Breakdown

  • load_dotenv() reads your .env file into environment variables.
  • The app.config[...] lines pass those values to Flask-Mail. MAIL_PORT must be an integer, so the code wraps it in int().
  • mail = Mail(app) creates the mail client. It reads the config once, right here.
  • The /send-email route builds a Message from the JSON request. Then mail.send() sends it.

You don’t pass a sender, because Flask-Mail falls back to MAIL_DEFAULT_SENDER.

Step 3: Send a Test Email

Start the development server:

python app.py

Flask runs on port 5000 by default. In a second terminal, send a test request with cURL:

curl -X POST http://127.0.0.1:5000/send-email \
  -H "Content-Type: application/json" \
  -d '{"to": "you@example.com", "subject": "Hello from Flask", "message": "This is a test email."}'

Change "to" to an inbox you can check. You should get {"message": "Email sent"} back, and the email should arrive within seconds.

You can also call the endpoint from Postman or a frontend like React or Next.js.

Test email sent from a Flask app with Flask-Mail, shown in the recipient's inbox

Send HTML Emails using Flask

To send HTML, set the html argument on the Message. Keep body too, as a plain-text fallback for clients that don’t render HTML.

@app.route("/send-html-email", methods=["POST"])
def send_html_email():
    data = request.get_json()
    msg = Message(
        subject="Your order has shipped",
        recipients=[data["to"]],
        body="Your order is on its way. Track it at https://example.com/track/123",
        html="<p>Your order is on its way. "
             "<a href='https://example.com/track/123'>Track your package</a>.</p>",
    )
    mail.send(msg)
    return jsonify({"message": "HTML email sent"})

Notice the single quotes inside the HTML string. Escaped double quotes like \\\" leave literal backslashes in the email. That breaks the link.

Send Email to Multiple Recipients

Add each address to the recipients list. Flask-Mail also takes cc and bcc lists.

msg = Message(
    subject="Team update",
    recipients=["alex@example.com", "sam@example.com"],
    cc=["manager@example.com"],
    bcc=["archive@example.com"],
    body="The weekly report is ready.",
)

Everyone in recipients can see each other’s addresses. To send one email per user, loop over your list. Reuse one connection with mail.connect():

with mail.connect() as conn:
    for user in users:
        msg = Message(subject="Your weekly summary", recipients=[user.email],
                      body=f"Hi {user.name}, here's your summary.")
        conn.send(msg)

Send Email with Attachments

To attach a file, read it and call msg.attach(). Pass the filename, the MIME type, and the file’s bytes. It’s handy for invoices, tickets, and reports.

msg = Message(subject="Your invoice", recipients=["alex@example.com"],
              body="Your invoice is attached.")

with app.open_resource("static/invoice.pdf") as fp:
    msg.attach("invoice.pdf", "application/pdf", fp.read())

mail.send(msg)

app.open_resource() opens files relative to your app’s folder. For a file somewhere else, use Python’s open("path/to/file.pdf", "rb") instead.

Use the right MIME type for each file, like image/png for a PNG.

Use HTML Templates

To send templated emails, render Flask’s Jinja templates into the message. They work for emails just like they do for pages, and they beat long HTML strings. Create templates/email/confirm.html:

<!DOCTYPE html>
<html>
  <body style="font-family: Arial, sans-serif;">
    <p>Hi {{ name }},</p>
    <p>Thanks for signing up. Please confirm your email address:</p>
    <p><a href="{{ confirm_link }}">Confirm my email</a></p>
    <p>This link expires in 1 hour.</p>
  </body>
</html>

Add a plain-text twin at templates/email/confirm.txt:

Hi {{ name }},

Thanks for signing up. Please confirm your email address:
{{ confirm_link }}

This link expires in 1 hour.

Then add a helper that renders both versions. Import render_template from Flask first.

from flask import render_template

def send_template_email(subject, recipient, template, **context):
    msg = Message(subject=subject, recipients=[recipient])
    msg.body = render_template(f"email/{template}.txt", **context)
    msg.html = render_template(f"email/{template}.html", **context)
    mail.send(msg)

Send Emails in the Background

mail.send() blocks until the SMTP server answers. That can add a second or more to every request. You can hand the work to a thread and return right away.

Flask returns a response right away while a background thread sends the email

Background code runs outside the request, so it needs its own app context. Pass the real app object into the thread:

from threading import Thread
from flask import current_app

def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

def send_email_in_background(msg):
    app = current_app._get_current_object()
    Thread(target=send_async_email, args=(app, msg)).start()


@app.route("/contact", methods=["POST"])
def contact():
    data = request.get_json()
    msg = Message(
        subject=f"New message from {data['name']}",
        recipients=["support@example.com"],
        reply_to=data["email"],
        body=data["message"],
    )
    send_email_in_background(msg)
    return jsonify({"message": "Thanks! We'll be in touch."}), 202

current_app._get_current_object() gets the app behind Flask’s proxy. Without it, the thread raises RuntimeError: Working outside of application context.

reply_to makes Reply go to the person who filled in the form.

A thread is fine for low volume. If the process restarts, you lose any unsent email. For anything important, use a task queue like Celery or RQ, which retry failed jobs.

Send an Email Verification Link

To send a verification link, sign a token with itsdangerous and email a link that contains it. Flask already depends on itsdangerous, and its tokens can expire.

Email verification flow in Flask from sign up to a signed link to a verified account

Add a SECRET_KEY to your .env file, then add these routes:

from flask import url_for
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer

app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
serializer = URLSafeTimedSerializer(app.config["SECRET_KEY"])


@app.route("/register", methods=["POST"])
def register():
    data = request.get_json()
    token = serializer.dumps(data["email"], salt="email-confirm")
    link = url_for("confirm_email", token=token, _external=True)
    send_template_email(
        subject="Confirm your email address",
        recipient=data["email"],
        template="confirm",
        name=data["name"],
        confirm_link=link,
    )
    return jsonify({"message": "Check your inbox to confirm your email."}), 201


@app.route("/confirm/<token>")
def confirm_email(token):
    try:
        email = serializer.loads(token, salt="email-confirm", max_age=3600)
    except SignatureExpired:
        return jsonify({"error": "This link has expired."}), 400
    except BadSignature:
        return jsonify({"error": "This link is invalid."}), 400
    # Mark the user with this email as confirmed in your database here.
    return jsonify({"message": f"{email} is confirmed."})

max_age=3600 makes the link expire after 1 hour. The salt keeps these tokens from working anywhere else you use the same key.

If someone tampers with the token, the BadSignature branch catches it. The route returns “This link is invalid.”

Flask-Mail vs. Flask-Mailman

Both extensions send email over SMTP. The main difference is their API style.

Flask-MailFlask-Mailman
Latest release (Oct 2026)0.10.0, May 20241.1.1, July 2024
Maintained byPallets Community EcosystemIndependent maintainer
API styleMessage + mail.send()Django-style EmailMessage + msg.send()
Test helpersrecord_messages(), MAIL_SUPPRESS_SENDConsole, file, in-memory, and dummy backends
Async supportNoNo

Flask-Mailman ports Django’s mail module to Flask. Here’s the same plain-text route with it:

from flask_mailman import EmailMultiAlternatives, Mail

mail = Mail(app)  # reads the same MAIL_* config keys

@app.route("/send-email", methods=["POST"])
def send_email():
    data = request.get_json()
    msg = EmailMultiAlternatives(
        subject=data["subject"],
        body=data["message"],
        to=[data["to"]],
    )
    msg.attach_alternative(f"<p>{data['message']}</p>", "text/html")
    msg.send()
    return jsonify({"message": "Email sent"})

My pick for a new project is Flask-Mail, because it’s simpler. Choose Flask-Mailman if your team knows Django’s mail API. It’s also handy if you want MAIL_BACKEND = "console" to print emails while you build.

How to Send Email in Flask with Email API

An email API sends your message over HTTPS instead of SMTP. That helps in 3 cases:

  • Your host blocks outgoing SMTP ports. HTTPS uses port 443, which is almost never blocked.
  • You want clear errors, like “recipient is suppressed”, instead of raw SMTP codes.
  • You want delivery events through webhooks, like bounces and opens.

SendLayer has an official Python SDK, so this method takes 3 steps.

Step 1: Get Your SendLayer API Key

Log in to your SendLayer account and go to Settings » API Keys.

API Keys tab under Settings in the SendLayer dashboard

Click the copy icon next to Default API key.

Copy icon next to the default SendLayer API key for a Flask app

Paste it into your .env file:

SENDLAYER_API_KEY=your-sendlayer-api-key

Step 2: Install the SDK

Install SendLayer’s Python SDK with pip:

pip install sendlayer

Step 3: Create the API Route

Now add a route that sends an HTML email. This version also handles errors:

import os

from dotenv import load_dotenv
from flask import Flask, jsonify, request
from sendlayer import SendLayer, SendLayerError, SendLayerValidationError

load_dotenv()

app = Flask(__name__)
sendlayer = SendLayer(os.getenv("SENDLAYER_API_KEY"))


@app.route("/api/send-email", methods=["POST"])
def send_api_email():
    data = request.get_json()
    try:
        response = sendlayer.Emails.send(
            sender="paulie@example.com",
            to=data["to"],
            subject=data["subject"],
            text=data["message"],
            html=f"<p>{data['message']}</p>",
        )
    except SendLayerValidationError as e:
        return jsonify({"error": e.message}), 400
    except SendLayerError as e:
        app.logger.error("SendLayer error %s: %s", e.status_code, e.message)
        return jsonify({"error": "Email could not be sent."}), 502
    return jsonify(response)


if __name__ == "__main__":
    app.run(debug=True)

Code Breakdown

  • SendLayer(...) creates the client with your API key.
  • sendlayer.Emails.send() sends the message. Passing both text and html sends both parts.
  • SendLayerValidationError covers bad input. The SDK checks every address before it calls the API.
  • SendLayerError catches everything else, like a wrong API key or a rate limit.

For example, pass "not-an-email" as the recipient. The route returns Invalid recipient email address: not-an-email before any API call.

Send Email to Multiple Recipients via API

Pass a list to to, cc, or bcc. You can also send a name with an address by using a dict:

sendlayer.Emails.send(
    sender={"email": "paulie@example.com", "name": "Paulie Paloma"},
    to=["alex@example.com", "sam@example.com"],
    cc=["manager@example.com"],
    bcc=["archive@example.com"],
    subject="Team update",
    text="The weekly report is ready.",
)

Note: Each request takes up to 10 To addresses, 10 CC, and 5 BCC. See the SendLayer rate limits for your plan’s sending limits.

Send Email with Attachments via API

Add an attachments list. Each item needs a path and a MIME type:

sendlayer.Emails.send(
    sender="paulie@example.com",
    to="alex@example.com",
    subject="Your invoice",
    text="Your invoice is attached.",
    html="<p>Your invoice is attached.</p>",
    attachments=[{"path": "static/invoice.pdf", "type": "application/pdf"}],
)

To attach more files, add more dicts to the list. Each email can be 10 MB at most, including attachments.

  • 200 Free Emails
  • Easy Setup
  • 5 Star Support

Test Flask Emails Without Sending Them

To test emails without sending them, turn on MAIL_SUPPRESS_SEND and use Flask-Mail’s record_messages(). Your test suite then never emails real people:

app.config.update(TESTING=True, MAIL_SUPPRESS_SEND=True)
mail.init_app(app)  # re-read the config

with mail.record_messages() as outbox:
    client = app.test_client()
    client.post("/send-email", json={"to": "a@example.com",
                                     "subject": "Hi", "message": "Test"})
    assert len(outbox) == 1
    assert outbox[0].subject == "Hi"

Important: Flask-Mail reads MAIL_SUPPRESS_SEND when Mail(app) runs. Set it before that line, or call mail.init_app(app) again.

If you set it too late, Flask-Mail ignores it, and your tests connect to the real SMTP server.

Tips for Better Email Handling

A few habits keep your email code reliable as your app grows.

1. Validate Input

Check incoming JSON before you build a message. Return a clear 400 error for missing fields, instead of a crash.

If you use the SendLayer SDK, it also checks every address before calling the API.

2. Apply Rate Limits

Set your own sending limit a little below your provider’s cap. Then a bug or bot can’t burn through your plan.

Public forms need this most. Without a limit, a spam bot can send hundreds of emails through your contact form.

3. Implement Error Handling

SMTP servers time out and credentials expire. Catch errors, log them, and return a clean response:

@app.route("/send-email-safe", methods=["POST"])
def send_email_safe():
    data = request.get_json(silent=True) or {}
    missing = [f for f in ("to", "subject", "message") if not data.get(f)]
    if missing:
        return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
    try:
        msg = Message(subject=data["subject"], recipients=[data["to"]],
                      body=data["message"])
        mail.send(msg)
    except Exception:
        app.logger.exception("Email failed")
        return jsonify({"error": "Email could not be sent."}), 502
    return jsonify({"message": "Email sent"})

The real error goes to your logs. The user sees a short message that doesn’t leak server details.

Troubleshooting Flask Email Errors

These are the errors you’re most likely to hit, with the fix for each.

SMTPAuthenticationError (535)

Your username or password is wrong. Copy them again from your provider. With SendLayer, the SMTP username isn’t your login email.

For Gmail, use an app password, not your account password.

“The message does not specify a sender”

Flask-Mail raises this AssertionError when a message has no sender. Set MAIL_DEFAULT_SENDER in your config. A custom key like MAIL_SENDER won’t work, because Flask-Mail ignores it.

Connection Timed Out or Refused

Your host may block outgoing SMTP. Many cloud providers block port 25, and some block 587 too. Ask your host which ports are open, or switch to the email API.

Also check your TLS settings. Use MAIL_USE_TLS=True on port 587. Only use MAIL_USE_SSL=True if your provider asks for port 465, which SendLayer doesn’t accept. Never set both to True.

RuntimeError: Working Outside of Application Context

You called mail.send() from a thread, a script, or a scheduled job. Wrap the call in with app.app_context():, as shown in background sending.

HTML Emails Show as Plain Text

You probably put the HTML in body instead of html. Set msg.html, and keep msg.body as the plain-text version.

Emails Land in Spam

Check that you’ve authorized your sending domain, and that your sender address uses it. Missing SPF, DKIM, and DMARC records are the usual cause.

FAQs — Send Emails in Flask

Here are answers to common questions about sending emails in Flask.

Can I use Flask-Mail with Gmail?

Yes. Set MAIL_SERVER to smtp.gmail.com, MAIL_PORT to 587, and MAIL_USE_TLS to True.

For the password, use a Google app password. App passwords need 2-Step Verification, and Google revokes them when you change your account password.

Is Flask-Mail still maintained?

Yes. The Pallets Community Ecosystem maintains it now. It released version 0.10.0 in May 2024.

That ended a 10-year gap after 0.9.1. As of October 2026, 0.10.0 is still the latest release.

What’s the difference between Flask-Mail and Flask-Mailman?

Both send email over SMTP. Flask-Mail uses a simple Message class.

Flask-Mailman ports Django’s mail module and adds console, file, and in-memory backends. Neither one supports async sending.

How do I send emails asynchronously in Flask?

Send the email from a background thread with its own app context. The request then returns right away.

For important emails, use a task queue like Celery or RQ. Queues retry failed sends, and a thread doesn’t. See the background sending section for the code.

How do I send emails with attachments?

With Flask-Mail, read the file and call msg.attach() with the filename, MIME type, and data.

With the SendLayer SDK, pass an attachments list. Each file needs a path and a type. See the attachments section for both examples.

That’s It! Now You Know How to Send Emails in Flask

Next, wondering whether SMTP or an API is the better fit for your app? Check out our guide to SMTP vs API to compare them.

  • 200 Free Emails
  • Easy Setup
  • 5 Star Support

Ready to send your emails in the fastest and most reliable way? Get started today with the most user-friendly and powerful SMTP email delivery service. SendLayer Business includes 5,000 emails a month with premium support.

author avatar
David Ozokoye
David is a technical writer at SendLayer. He tests and documents new features and updates to SendLayer's email services. Away from the computer, he enjoys playing video games and roller skating.