MailSlurp logo

guides

Python Send Email SMTP Guide (smtplib, HTML, Attachments, and Testing)

Send email with Python SMTP using smtplib, EmailMessage, TLS auth, HTML bodies, attachments, MailSlurp API checks, and deterministic inbox tests.

View MarkdownAgent setup

If you searched for "python send email smtp", "send email python", or "smtplib python", the production-safe path is:

  1. build messages with EmailMessage
  2. send with smtplib using explicit TLS and authentication
  3. keep SMTP settings in environment variables
  4. verify inbox receipt, links, HTML, and attachments before release

Python can send a message in a few lines, but production email needs more than a successful send_message() call. You need to know that the email arrived, rendered correctly, and contained the expected links, codes, headers, and attachments.

python smtp

Choose the right Python email path

Need Recommended path Why
Learn the SMTP basics smtplib with a plain text message Smallest standard-library example.
Send transactional app email EmailMessage plus authenticated SMTP Handles headers, MIME, HTML, and attachments cleanly.
Run CI checks for signup, reset, or OTP email MailSlurp inbox plus wait-for-email checks Confirms the email actually arrived and matched expectations.
Prefer API-first sending MailSlurp API or SDK Easier to log, retry, and assert in automated workflows.

If you are deciding between SMTP and an HTTP email API, compare the tradeoffs in SMTP vs HTTP email APIs.

Minimal smtplib example

This is the smallest useful SMTP example:

import smtplib

message = "Subject: Welcome\r\n\r\nYour account is ready."

with smtplib.SMTP("smtp.example.com", 587, timeout=30) as smtp:
    smtp.starttls()
    smtp.login("smtp-user", "smtp-password")
    smtp.sendmail("no-reply@example.com", ["user@example.com"], message)

That is fine for learning, but production code should use structured messages, environment-backed settings, and receive-side assertions.

Configure SMTP settings with environment variables

Use your deployment platform, shell, .env loader, or CI secret store to provide these values:

SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=smtp-user
SMTP_PASSWORD=smtp-password
SMTP_FROM_ADDRESS=no-reply@example.com
SMTP_FROM_NAME="Example App"

Use port 587 with STARTTLS for most modern SMTP submission flows unless your provider documents another mode. Review SMTP ports and STARTTLS vs SSL/TLS when validating the setup.

Send email with Python EmailMessage and smtplib

EmailMessage keeps headers and body content clearer than hand-built strings:

import os
import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["From"] = os.environ["SMTP_FROM_ADDRESS"]
message["To"] = "recipient@example.com"
message["Subject"] = "Welcome"
message.set_content("Your account is ready.")

with smtplib.SMTP(os.environ["SMTP_HOST"], int(os.environ["SMTP_PORT"]), timeout=30) as smtp:
    smtp.starttls()
    smtp.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
    smtp.send_message(message)

Keep exception handling around the send call in application code so SMTP failures are logged and surfaced to your job runner.

Send HTML email from Python

Add a plain text body first, then attach the HTML alternative:

import os
import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["From"] = os.environ["SMTP_FROM_ADDRESS"]
message["To"] = "recipient@example.com"
message["Subject"] = "Reset your password"
message.set_content("Use the secure link in this email to reset your password.")
message.add_alternative(
    """
    <html>
      <body>
        <h1>Password reset</h1>
        <p>Use the secure link in this email to reset your password.</p>
      </body>
    </html>
    """,
    subtype="html",
)

with smtplib.SMTP(os.environ["SMTP_HOST"], int(os.environ["SMTP_PORT"]), timeout=30) as smtp:
    smtp.starttls()
    smtp.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
    smtp.send_message(message)

For release-critical HTML, run email client testing and deliverability testing before sending broadly.

Send attachments from Python

Use pathlib and mimetypes so attachment handling is explicit:

import mimetypes
from pathlib import Path

attachment_path = Path("reports/invoice.pdf")

if not attachment_path.is_file():
    raise FileNotFoundError(attachment_path)

content_type, _ = mimetypes.guess_type(attachment_path)
maintype, subtype = (content_type or "application/octet-stream").split("/", 1)

message.add_attachment(
    attachment_path.read_bytes(),
    maintype=maintype,
    subtype=subtype,
    filename=attachment_path.name,
)

After changing attachment code, send to a MailSlurp inbox and assert the expected filename, content type, and attachment count.

Send email from Python with the MailSlurp API

For API-first tests or scheduled jobs, send from a MailSlurp inbox:

import json
import os
import urllib.request

api_key = os.environ["MAILSLURP_API_KEY"]
inbox_id = os.environ["MAILSLURP_INBOX_ID"]

payload = json.dumps(
    {
        "to": ["recipient@example.com"],
        "subject": "Python API smoke test",
        "body": "Sent from a Python job through MailSlurp.",
    }
).encode("utf-8")

request = urllib.request.Request(
    f"https://api.mailslurp.com/inboxes/{inbox_id}/confirm",
    data=payload,
    method="POST",
    headers={
        "x-api-key": api_key,
        "Content-Type": "application/json",
    },
)

with urllib.request.urlopen(request, timeout=30) as response:
    sent_email = json.loads(response.read().decode("utf-8"))

This path is useful when you want API response data and test assertions without depending on a local SMTP session.

Wait for the received email

For tests, wait for a MailSlurp inbox to receive the message and assert the content:

import json
import os
import urllib.parse
import urllib.request

api_key = os.environ["MAILSLURP_API_KEY"]
receive_inbox_id = os.environ["MAILSLURP_RECEIVE_INBOX_ID"]

query = urllib.parse.urlencode(
    {
        "inboxId": receive_inbox_id,
        "timeout": 120000,
        "unreadOnly": "true",
    }
)

request = urllib.request.Request(
    f"https://api.mailslurp.com/waitForLatestEmail?{query}",
    headers={"x-api-key": api_key},
)

with urllib.request.urlopen(request, timeout=130) as response:
    email = json.loads(response.read().decode("utf-8"))

assert email["subject"] == "Python API smoke test"
assert "Python job" in email.get("body", "")

That check proves more than SMTP acceptance. It proves the workflow produced a readable email in the expected inbox.

Common Python SMTP errors and fixes

535 authentication failed

Check credentials, app-password requirements, tenant policy, and sender-domain alignment. Then verify the from address is allowed by the SMTP provider. Use the SMTP authentication guide for the full checklist.

TLS or certificate failures

Confirm the port and TLS mode. Port 587 commonly expects starttls(), while port 465 uses SMTP over SSL. If you need SSL mode, use smtplib.SMTP_SSL and validate provider settings.

Connection timed out

Check firewall rules, hosting-provider outbound SMTP restrictions, DNS, and proxy configuration. Add an explicit timeout so jobs fail clearly instead of hanging.

Message accepted but never arrives

Inspect headers, SPF, DKIM, DMARC, spam score, blacklist status, and inbox placement:

Production checklist for Python email

Before shipping Python email changes:

  1. Keep SMTP and API secrets outside source code.
  2. Use EmailMessage for headers, HTML, and attachments.
  3. Validate port, TLS, authentication, and sender-domain policy.
  4. Add timeouts and useful error logging around SMTP/API calls.
  5. Send to a MailSlurp inbox and assert subject, body, links, codes, and attachments.
  6. Re-test after template, DNS, or provider changes.
  7. Use webhooks to track delivery, bounces, and downstream workflows.

Start with Email Sandbox for isolated send-and-receive tests, email integration testing for CI assertions, and email webhooks for delivery evidence.

FAQ

Is smtplib enough to send email in production?

Yes for many workflows, if you use explicit TLS/auth settings, structured messages, timeouts, and receive-side assertions. The fragile part is usually not smtplib; it is untested configuration and deliverability.

Can Python send HTML email?

Yes. Use EmailMessage.set_content() for the text body and add_alternative(..., subtype="html") for HTML.

Can Python send attachments?

Yes. Use EmailMessage.add_attachment() with bytes, MIME type, and filename. Verify received attachments in a test inbox before release.

How do I test Python email in CI?

Send to a MailSlurp inbox, call wait-for-email, and assert subject, body, links, OTP codes, headers, and attachments before the job passes.

Next steps