Docs / Python

Send email in Python over SMTP

Python's standard library covers email end to end: smtplib speaks SMTP and email.message builds the message. No dependencies required. This guide connects them to Postwing and covers HTML mail, attachments, inline images, bulk sending and the asyncio equivalent.

You can get them on the token management page. For security reasons, a token is shown only once — at the moment it is created.

SMTP connection settings

SettingValue
SMTP hostsmtp.postwing.app
Port587
EncryptionSTARTTLS (the connection is upgraded to TLS before login)
UsernameThe login of an SMTP token for your domain
PasswordThe password of that token — shown once, when the token is created
Every mode is also available on a high port: 8465 (SSL/TLS), 8587 (STARTTLS) and 8025 (plain). Many hosting providers and clouds block outbound 25, 465 and 587 — if the connection times out, switch to the matching high port.

Send your first email

python
import os
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "Acme <noreply@your-domain.com>"
msg["To"] = "customer@example.com"
msg["Subject"] = "Your order #4417 is confirmed"
msg.set_content("Thanks! Your order ships tomorrow.")

with smtplib.SMTP("smtp.postwing.app", 587, timeout=10) as smtp:
    smtp.starttls()                                   # upgrade to TLS before logging in
    smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
    smtp.send_message(msg)
On port 587 the connection starts unencrypted. Authenticating first sends your token password in clear text over the network — and the server will reject it anyway.

On port 465 the socket is encrypted from the start instead:

python
import ssl

# Implicit TLS: encrypted from the first byte, no starttls() call.
context = ssl.create_default_context()

with smtplib.SMTP_SSL("smtp.postwing.app", 465, context=context, timeout=10) as smtp:
    smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
    smtp.send_message(msg)

Send an HTML email

Set the plain-text body first, then attach the HTML as an alternative. The order matters — clients render the last part they understand:

python
msg = EmailMessage()
msg["From"] = "Acme <noreply@your-domain.com>"
msg["To"] = "customer@example.com"
msg["Subject"] = "Your order #4417 is confirmed"

# The plain-text part first...
msg.set_content("Order confirmed. Your order ships tomorrow.")

# ...then the HTML alternative. This makes it multipart/alternative.
msg.add_alternative(
    """\
    <html>
      <body>
        <h1>Order confirmed</h1>
        <p>Your order ships tomorrow.</p>
      </body>
    </html>
    """,
    subtype="html",
)

Add an attachment

python
from pathlib import Path
import mimetypes

path = Path("/srv/invoices/2026-03.pdf")
mime, _ = mimetypes.guess_type(path.name)
maintype, subtype = (mime or "application/octet-stream").split("/", 1)

msg.add_attachment(
    path.read_bytes(),
    maintype=maintype,
    subtype=subtype,
    filename="invoice.pdf",
)

For an image the HTML references inline:

python
# Inline image, referenced from the HTML as <img src="cid:logo">
msg.add_alternative(
    '<p>Thanks!</p><img src="cid:logo" width="120">', subtype="html"
)

logo = Path("assets/logo.png").read_bytes()
msg.get_payload()[1].add_related(logo, "image", "png", cid="<logo>")

Send to many recipients

Reuse a single authenticated connection rather than reconnecting per message, and send one message per recipient:

python
# Open one connection and reuse it, instead of reconnecting per message.
with smtplib.SMTP("smtp.postwing.app", 587, timeout=10) as smtp:
    smtp.starttls()
    smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])

    for user in users:
        msg = EmailMessage()
        msg["From"] = "Acme <noreply@your-domain.com>"
        msg["To"] = user.email          # one recipient per message
        msg["Subject"] = "Your weekly report"
        msg.set_content(render_report(user))

        try:
            smtp.send_message(msg)
        except smtplib.SMTPRecipientsRefused:
            log.warning("Refused: %s", user.email)
Attention!
When using BCC, CC - a separate email will be created for each recipient in the system and your limits will be used.

Sending from async code

smtplib blocks, which stalls an asyncio event loop for the whole SMTP round trip. Use aiosmtplib instead — it takes the same EmailMessage objects:

python
# aiosmtplib is the asyncio equivalent — same message objects, awaitable I/O.
import aiosmtplib

await aiosmtplib.send(
    msg,
    hostname="smtp.postwing.app",
    port=587,
    start_tls=True,
    username=os.environ["SMTP_USER"],
    password=os.environ["SMTP_PASS"],
)

Debugging

python
smtp.set_debuglevel(1)   # print the full SMTP conversation to stderr

Troubleshooting

ErrorCause and fix
SMTPAuthenticationError Wrong credentials, or login() called before starttls().
SMTPNotSupportedError: STARTTLS extension not supportedstarttls() on port 465. Use SMTP_SSL there instead.
ssl.SSLError: wrong version numberSMTP_SSL on port 587. Use plain SMTP plus starttls().
Hangs forever No timeout passed and the port is blocked. Add one and switch to 8587.
SMTPSenderRefused (550) The From domain is not verified, or the token does not cover it.
Subject shows as =?utf-8?B?… Expected — that is RFC 2047 encoding. Mail clients decode it; do not "fix" it by stripping non-ASCII.

Frequently asked questions

Should I use smtplib.SMTP with starttls() or smtplib.SMTP_SSL?

SMTP with starttls() on port 587: the connection opens in plain text and is upgraded before login. SMTP_SSL on port 465: the socket is encrypted from the first byte and you must not call starttls(). Calling starttls() on an SMTP_SSL connection raises an error, and omitting it on port 587 sends your password in the clear.

Why should I use EmailMessage instead of MIMEText?

EmailMessage is the modern email.message API and has been the recommended one since Python 3.6. It handles multipart assembly, headers, encoding and attachments through simple methods, where the older MIMEMultipart and MIMEText classes require assembling the tree yourself and get Unicode headers wrong easily.

Why does Python raise SMTPAuthenticationError?

The username or password is wrong, or you called login() before starttls() — credentials sent on an unencrypted connection are rejected. Ensure starttls() comes first on port 587.

How do I stop smtplib from hanging?

Pass timeout to the constructor. Without it, smtplib inherits the global socket default, which is usually no timeout at all, so a blocked port leaves the process waiting indefinitely. If it does time out, your host is likely blocking outbound 587 — use 8587.

How do I send HTML email in Python?

Call set_content() with the plain-text version, then add_alternative(html, subtype='html'). That order matters: mail clients display the last part they can render, so the HTML must come second. Sending HTML with no text part is a spam signal.

Can I send email asynchronously in Python?

smtplib is synchronous and blocks the event loop, so in an async application use aiosmtplib instead. It accepts the same EmailMessage objects and exposes an awaitable send(). Alternatively hand the message to a background worker such as Celery.

Next steps