Atomic Mail
Products
↓
Atomic VPN
Private VPN service
Atomic Mail
Secure encrypted email
Atomic Bot
One-click AI agent runner
Atomic Chat
Local & open-source AI super chat
Atomic Wallet
Secure, anonymous crypto wallet
Sigma Browser
Private AI browser
Atomic Agent
Local-first AI agent
How it worksUse casesCompareFAQBlog
Docs
Login
Blog
/
How to Send Email in Python: smtplib, Gmail, Attachments and APIs (2026)

How to Send Email in Python: smtplib, Gmail, Attachments and APIs (2026)

Guides
11 min read
October 5, 2026
How to Send Email in Python: smtplib, Gmail, Attachments and APIs (2026)
Share this post
Copied!

Python can send email with nothing but the standard library: smtplib talks to the mail server, email.message.EmailMessage builds the message. Ten lines of code are enough for a plain-text message. The part that takes the time is everything around it: TLS, Gmail passwords that suddenly stop working, attachments, and cloud hosts that block your port.

This guide covers the full path, from a first message sent to a local test server to HTML, attachments, Gmail, error handling, and the point where an email API becomes the better tool.

TL;DR
  • smtplib plus EmailMessage from the standard library is all you need to send email from Python. No pip install.
  • Use port 465 with SMTP_SSL, or port 587 with starttls(). Never send credentials over a plain connection.
  • Gmail rejects your normal password over SMTP. Turn on 2-Step Verification and create an app password.
  • SMTP only sends. To receive or read replies you need IMAP, or an API with an inbox.
  • Once you send at volume, from your own domain, or from a cloud host, an HTTP email API is usually less work than SMTP.

How sending email from Python works

SMTP (Simple Mail Transfer Protocol) is the protocol that moves a message from a sender to a mail server. When you call smtplib, your script behaves like a mail client: it opens a connection to an SMTP server, authenticates, hands over the message and disconnects. The server then delivers it onward

That has two consequences worth knowing before you write any code.

First, you need an SMTP server that accepts you. That can be Gmail, your company's mail server, a hosting provider's relay, or a transactional email service. Python does not ship with one that delivers to the internet.

Second, building the message and sending it are separate steps. EmailMessage creates headers and body. smtplib transports it. Mixing the two up is the source of most old tutorials' ugly code (manual MIMEText assembly, hand-written header strings). Since Python 3.6 the EmailMessage API is the one to use.

Send your first email without a real server

Test locally before you touch real credentials. The aiosmtpd package runs a throwaway SMTP server that prints every message it receives. (The old python -m smtpd debug server was removed in Python 3.12, so aiosmtpd is the replacement.)

bash
pip install aiosmtpd
python -m aiosmtpd -n -l localhost:1025
python
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "sender@example.com"
msg["To"] = "recipient@example.com"
msg["Subject"] = "Hello from Python"
msg.set_content("This is the plain-text body.")

with smtplib.SMTP("localhost", 1025) as server:
    server.send_message(msg)

The first terminal prints the full message with its headers. Nothing leaves your machine, so you can iterate on subject lines, recipients and formatting without sending real mail.

Send email through a real SMTP server

A real server adds two requirements: an encrypted connection and a login. SMTP has two common ways to encrypt.

Port Mode How you connect
465 Implicit TLS smtplib.SMTP_SSL encrypts from the first byte
587 STARTTLS smtplib.SMTP, then starttls() upgrades the connection
25 Server-to-server relay Not for client submission, and often blocked

Port 465 version:

python
import os
import smtplib
import ssl

context = ssl.create_default_context()

with smtplib.SMTP_SSL("smtp.example.com", 465, context=context) as server:
    server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
    server.send_message(msg)

Port 587 version:

python
with smtplib.SMTP("smtp.example.com", 587) as server:
    server.starttls(context=ssl.create_default_context())
    server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
    server.send_message(msg)

Two details matter here. Pass an explicit ssl.create_default_context() so the server certificate is actually verified. And read credentials from environment variables, not from the source file. A password committed to a repository is the most common way these scripts leak.

Send email from Gmail with Python

Gmail uses smtp.gmail.com, port 465 for SSL or 587 for STARTTLS. The code is the same as above. What trips people up is the password.

Google removed "less secure app" access, so logging in with your regular account password fails. The working setup:

  1. Turn on 2-Step Verification for the Google account.
  2. Create an app password in the account's security settings. It is a 16-character code made for one app.
  3. Use that code as SMTP_PASSWORD, with your full Gmail address as SMTP_USER.
python
import os
import smtplib
import ssl
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = os.environ["SMTP_USER"]
msg["To"] = "recipient@example.com"
msg["Subject"] = "Sent from Python through Gmail"
msg.set_content("Hello from a script.")

with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=ssl.create_default_context()) as server:
    server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
    server.send_message(msg)

Gmail is fine for personal scripts and alerts to yourself. It enforces daily sending limits and rewrites the sender to the logged-in account, so it is a poor base for anything customer-facing.

Send HTML email and multiple recipients

For an HTML message, set a plain-text body first and add the HTML as an alternative. Clients that cannot render HTML, and spam filters that expect a text part, both use the plain version.

python
msg = EmailMessage()
msg["From"] = "sender@example.com"
msg["To"] = "a@example.com, b@example.com"
msg["Cc"] = "c@example.com"
msg["Bcc"] = "d@example.com"
msg["Subject"] = "Weekly report"

msg.set_content("Your report is ready. Open this email in an HTML-capable client.")
msg.add_alternative(
    """
    <html>
      <body>
        <h1>Weekly report</h1>
        <p>Your report is ready.</p>
      </body>
    </html>
    """,
    subtype="html",
)

send_message() reads To, Cc and Bcc and delivers to all of them. It also strips the Bcc header from the copy that goes out, so the blind recipients stay hidden. To build the header from a list, use ", ".join(addresses).

Send an email with an attachment

EmailMessage.add_attachment() takes the file as bytes, plus a MIME type and a filename. Use mimetypes to pick the type instead of hard-coding it:

python
import mimetypes
from pathlib import Path

path = Path("report.pdf")
ctype, _ = mimetypes.guess_type(path.name)
maintype, subtype = (ctype or "application/octet-stream").split("/", 1)

msg.add_attachment(
    path.read_bytes(),
    maintype=maintype,
    subtype=subtype,
    filename=path.name,
)

The same code attaches a script. A file named job.py is guessed as text/x-python, and the recipient gets it as a normal attachment. Some corporate gateways block .py and other executable-looking extensions, so for those recipients zip the file first.

Keep an eye on size. Most servers refuse messages above roughly 20 to 25 MB once the attachment is base64-encoded, which inflates it by about a third. For big files, send a link instead.

Fix the errors you will actually hit

Error Usual cause
SMTPAuthenticationError: (535, ...) Wrong password. On Gmail, you used the account password instead of an app password.
SMTPServerDisconnected Wrong port or mode: SMTP on 465, or SMTP_SSL on 587.
ConnectionRefusedError / timeout Outbound port blocked by a firewall or cloud host. Many cloud providers block port 25. Try 587 or 465.
SMTPRecipientsRefused The server rejected the address, or refuses to relay for your account.
SMTPSenderRefused The From address is not one your account may send as.
ssl.SSLCertVerificationError Missing system certificates or a self-signed server. Fix the certificates, do not disable verification.

When a connection misbehaves, server.set_debuglevel(1) prints the whole SMTP conversation. It shows which step fails: connect, TLS, login, or recipient.

SMTP or an email API: which one to use

SMTP is the right choice when you already have a server to relay through, when the volume is small, or when you want zero dependencies. It starts to hurt in a few predictable cases:

  • You send from your own domain and need SPF, DKIM and DMARC set up so mail does not land in spam.
  • Your host blocks SMTP ports.
  • You need delivery events, bounces or open tracking.
  • You need to send in volume, where connection handling and retries become your problem.

Email APIs (SendGrid, Mailgun, Resend, Amazon SES and others) take the same message over HTTPS. The shape is nearly identical across providers, here with requests:

python
import os
import requests

response = requests.post(
    "https://api.example-provider.com/v1/send",   # endpoint and field names differ per provider
    headers={"Authorization": f"Bearer {os.environ['EMAIL_API_KEY']}"},
    json={
        "from": "sender@example.com",
        "to": ["recipient@example.com"],
        "subject": "Hello from an API",
        "text": "Sent over HTTPS, no SMTP port needed.",
    },
    timeout=10,
)
response.raise_for_status()

Port 443 is rarely blocked, there is no TLS negotiation to get wrong, and errors come back as JSON with a status code. The trade-off is a new dependency, an account, and usually a verified domain. Check each provider's docs for the exact endpoint and field names. They are not interchangeable.

What SMTP cannot do: receive and read email

Everything above is one-way. smtplib sends. It cannot list an inbox, read a reply or search a thread. In Python, the built-in route for reading is imaplib, which speaks IMAP. It works, but it is a verbose, stateful protocol, and Gmail and most providers require OAuth or app passwords for it as well.

For scripts that only notify, one-way is fine. For scripts that wait for an answer (confirm a code, handle a reply, continue a conversation), you need a second channel or a service that gives you both directions.

Sending and receiving from a Python AI agent

This is the case where the SMTP-plus-IMAP approach gets awkward. An autonomous agent that must sign up for a service, receive a verification email and answer a reply should not use your personal Gmail login. It needs an inbox of its own, and ideally one it can create without a human filling in a form.

Atomic Mail Agentic is built for that. An agent registers its own @atomicmail.ai inbox through a proof-of-work challenge, with no domain, card or phone verification. The inbox API is JMAP, the JSON email protocol standardized in RFC 8620 and RFC 8621, which covers both sending and reading in one request format. The service is in open alpha and free. Errors come back with a plain-language hint, which suits agents that debug their own requests.

From Python, the shortest path is the AgentSkill CLI. Register once:

bash
npx --package=@atomicmail/agent-skill atomicmail register \
  --username "myagent" \
  --watch on-demand
python
import json
import subprocess

result = subprocess.run(
    [
        "npx", "--package=@atomicmail/agent-skill", "atomicmail", "jmap_request",
        "--ops-file", "send_mail.json",
        "--vars", json.dumps({
            "TO": "alice@example.com",
            "SUBJECT": "Hello",
            "BODY": "Hi there",
        }),
    ],
    capture_output=True,
    text=True,
    check=True,
)
print(result.stdout)

The credentials live in ~/.atomicmail, so the script never handles a password. The same CLI reads the inbox, so the agent can pick up the reply to the message it just sent. If you would rather skip the CLI and call JMAP over HTTPS directly from requests, the Atomic Mail docs walk through the session flow, and the guide to building an AI email agent with its own inbox shows a full send, receive and reply loop.

Building an agent that needs its own inbox?
Your agent registers its own @atomicmail.ai address with proof-of-work: no domain, no card, no personal Gmail login. Free while in open alpha.
Get started →

FAQ

Is there an SMTP library in Python?

Yes. smtplib is in the standard library, so there is nothing to install. It pairs with email.message.EmailMessage for building the message.

How to send email in Python without SMTP?

Call an email API over HTTPS with requests or the provider's SDK. SendGrid, Mailgun, Resend and Amazon SES all offer one. For an agent that must also receive mail, use an API with an inbox, such as JMAP.

How to send email using SMTP?

Connect to the server, encrypt the connection, log in, then send. In Python: SMTP_SSL(host, 465) or SMTP(host, 587) followed by starttls(), then login() and send_message().

How do I email a .py file?

Attach it with add_attachment() using maintype="text" and subtype="x-python", or let mimetypes.guess_type() choose. Zip it if the recipient's mail system blocks script files.

Why does Gmail return SMTPAuthenticationError 535?

You are using your account password. Gmail only accepts an app password over SMTP, and creating one requires 2-Step Verification.

Which SMTP port should I use: 25, 465 or 587?

Use 587 with STARTTLS or 465 with implicit TLS. Port 25 is for server-to-server relay, and many hosts block it for outbound traffic.

Can Python receive and read email too?

Yes, with imaplib for IMAP, or through an API that exposes an inbox. smtplib alone only sends.

Should I hard-code my SMTP password?

No. Read it from an environment variable or a secrets manager. A password in source ends up in version control sooner or later.

What is the difference between send_message() and sendmail()?

send_message() takes an EmailMessage and reads the sender and recipients from its headers. sendmail() takes raw strings and an explicit envelope, which you only need for unusual routing. Prefer send_message().

Which route to pick

If you send occasional mail from a script you own, smtplib with an app password or your provider's SMTP credentials is enough. If you send from your own domain or at volume, use an email API. If the script is an agent that has to receive and answer mail on its own, it needs an inbox it owns, not a borrowed Gmail login.

‍

Posts you might have missed

No items found.
Go through all posts

Product

How it worksUse casesCompareFAQDocumentationBlogEmail for humans

Compare to

AgentMailResendSendGridOpenMailHostingerNylas Mailtrap

Policies

Terms of UsePrivacy Policy
support@atomicmail.ai
Atomic Mail Agentic - Let your agents read, send, and react to email autonomously | Product Hunt

ATOMICMAIL SYSTEMS OÜ.
HARJU MAAKOND, TALLINN, KESKLINNA LINNAOSA, HARJU TN 3 // VANA-POSTI TN 2, 10146

© 2026 ATOMIC MAIL