Email Automation with Python

· 6 min read · Updated March 20, 2026 · beginner
python email smtp imap

Python ships with everything you need to work with email. The smtplib and imaplib modules in the standard library handle sending and receiving messages, and third-party packages like yagmail and the SendGrid SDK make common tasks even simpler. This tutorial covers both paths — the stdlib approach for full control, and the library approach for convenience.

Prerequisites

You need Python 3.6 or later. No external packages are required for the standard library examples. For the third-party sections, install with pip:

pip install yagmail sendgrid

You also need an email account that allows SMTP/IMAP access. Gmail requires an App Password if you have 2FA enabled — generate one at your Google Account’s Security settings. Yahoo and Outlook have similar app-password flows. Do not use your actual account password with SMTP; app passwords are specifically designed for this use.

Sending Email with smtplib

The smtplib module is part of Python’s standard library. It gives you direct control over the SMTP protocol, which is what email servers use to relay messages.

A Minimal Example

import smtplib
from email.mime.text import MIMEText

msg = MIMEText("This is the email body.")
msg["From"] = "sender@gmail.com"
msg["To"] = "recipient@example.com"
msg["Subject"] = "Test Subject"

with smtplib.SMTP("smtp.gmail.com", 587) as server:
    server.starttls()
    server.login("sender@gmail.com", "your_app_password")
    server.sendmail("sender@gmail.com", ["recipient@example.com"], msg.as_string())

# Output: no output on success; raises SMTPAuthenticationError on bad credentials

The flow is straightforward: connect on port 587, upgrade the connection to TLS with starttls(), authenticate, then send. Always use a context manager — it ensures the connection closes even if an error occurs.

Sending HTML Email with Attachments

Most real emails include HTML content and file attachments. You need MIMEMultipart to build a message with multiple parts:

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders

def send_html_email(sender, password, recipient, subject, html_body, attachment_path=None):
    msg = MIMEMultipart("alternative")
    msg["From"] = sender
    msg["To"] = recipient
    msg["Subject"] = subject

    plain_part = MIMEText("View this email in an HTML-capable client.", "plain")
    html_part = MIMEText(html_body, "html")
    msg.attach(plain_part)
    msg.attach(html_part)

    if attachment_path:
        with open(attachment_path, "rb") as f:
            part = MIMEBase("application", "octet-stream")
            part.set_payload(f.read())
        encoders.encode_base64(part)
        filename = attachment_path.split("/")[-1]
        part.add_header("Content-Disposition", f"attachment; filename={filename}")
        msg.attach(part)

    with smtplib.SMTP("smtp.gmail.com", 587) as server:
        server.starttls()
        server.login(sender, password)
        server.sendmail(sender, [recipient], msg.as_string())
    print(f"Email sent to {recipient}")

# Usage
send_html_email(
    sender="you@gmail.com",
    password="xxxx xxxx xxxx xxxx",  # App password
    recipient="friend@example.com",
    subject="Your Monthly Report",
    html_body="<h1>Report</h1><p>Here is your attached report.</p>",
    attachment_path="/path/to/report.pdf"
)
# Expected output: Email sent to friend@example.com

MIMEMultipart("alternative") tells the email client that the parts are alternatives — it should prefer the HTML version if it can render it. Including a plain-text fallback is good practice for recipients whose clients block HTML.

Port 465 and SSL

Some providers still support port 465 with SSL from the start. In that case, use SMTP_SSL instead:

import smtplib

with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
    server.login("sender@gmail.com", "app_password")
    server.sendmail("sender@gmail.com", ["recipient@example.com"], msg.as_string())
# No starttls() call needed — SSL is active from the beginning

Do not call starttls() when using SMTP_SSL. Also avoid port 25 — it is commonly blocked by ISPs and cloud providers.

Reading Email with imaplib

The imaplib module handles IMAP, the protocol for fetching and managing emails on a server. Gmail uses port 993 with SSL.

Fetching Recent Emails

import imaplib
import email
from email.header import decode_header

def read_last_n_emails(account, password, n=5):
    with imaplib.IMAP4_SSL("imap.gmail.com", 993) as client:
        client.login(account, password)
        client.select("INBOX")
        _, msgs = client.search(None, "ALL")
        ids = msgs[0].split()
        last_ids = ids[-n:] if len(ids) >= n else ids

        for mid in reversed(last_ids):
            _, data = client.fetch(mid, "(RFC822)")
            msg = email.message_from_bytes(data[0][1])
            subject, enc = decode_header(msg["Subject"])[0]
            if isinstance(subject, bytes):
                subject = subject.decode(enc or "utf-8", errors="replace")
            print(f"ID {mid.decode()}: {subject} | From: {msg['From']}")

# Usage
read_last_n_emails("you@gmail.com", "app_password", n=5)
# Expected output:
# ID 123: Meeting Tomorrow | From: boss@company.com
# ID 124: Invoice #9921 | From: billing@vendor.com

search(None, "ALL") returns all message IDs as bytes. The IDs are space-separated in a single byte string, so split() turns them into a list. Use fetch() with "(RFC822)" to get the raw email bytes, then parse it with the email module.

Filtering by Sender or Subject

You can filter which messages to fetch by passing criteria to search():

import imaplib

with imaplib.IMAP4_SSL("imap.gmail.com", 993) as client:
    client.login("user@gmail.com", "app_password")
    client.select("INBOX")
    # Search for messages from a specific sender
    _, msgs = client.search(None, 'FROM "newsletter@company.com"')
    print(f"Found {len(msgs[0].split())} messages from newsletter")

Criteria must be bytes. Common filters include FROM, SUBJECT, SINCE, and BEFORE. Gmail’s IMAP search syntax supports compound queries like FROM "boss" SINCE "01-Jan-2025".

To mark messages as read or delete them, use store():

# Mark message 123 as seen
client.store(b"123", "+FLAGS", "\\Seen")

# Mark for deletion (actually deleted on EXPUNGE)
client.store(b"123", "+FLAGS", "\\Deleted")
client.expunge()

Using yagmail for Simpler Sending

yagmail wraps smtplib with a friendlier interface. It handles MIME construction automatically and supports attachments with a single parameter:

import yagmail

def send_bulk_newsletter(sender, password, recipients, subject, html_content):
    yag = yagmail.SMTP(sender, password, "smtp.gmail.com")
    yag.send(
        to=recipients,
        subject=subject,
        contents=[html_content],
    )
    yag.close()
    print(f"Newsletter sent to {len(recipients)} recipients")

# Usage
send_bulk_newsletter(
    sender="newsletter@gmail.com",
    password="app_password",
    recipients=["alice@example.com", "bob@example.com", "carol@example.com"],
    subject="March Newsletter",
    html_content="<h2>March Update</h2><p>Highlights from this month.</p>"
)
# Expected output: Newsletter sent to 3 recipients

The contents parameter accepts a list to send both plain-text and HTML versions. Use attachments="/path/to/file.pdf" or a list of paths to attach files.

Using SendGrid for Scalable Delivery

SendGrid is a cloud email service that handles delivery, reputation, and analytics. The sendgrid-python SDK is the official client:

from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

sg = SendGridAPIClient(api_key="SG.your_api_key_here")

message = Mail(
    from_email="sender@example.com",
    to_emails="recipient@example.com",
    subject="Hello from SendGrid",
    plain_text_content="Plain text body",
    html_content="<p>HTML body</p>"
)

response = sg.send(message)
print(response.status_code)  # 202 means the message was accepted
print(response.body)         # b'' on success
# Expected output: 202
# Expected output: b''

A status code of 202 Accepted means SendGrid accepted the message — it does not guarantee delivery to the inbox. SendGrid’s free tier allows 100 emails per day.

Storing Credentials Safely

Never hardcode passwords or API keys in your source files. Use environment variables:

import os
import smtplib
from email.mime.text import MIMEText

app_password = os.environ.get("GMAIL_APP_PASSWORD")
if not app_password:
    raise ValueError("GMAIL_APP_PASSWORD environment variable is not set")

msg = MIMEText("Body text")
msg["From"] = "you@gmail.com"
msg["To"] = "them@example.com"
msg["Subject"] = "Subject"

with smtplib.SMTP("smtp.gmail.com", 587) as server:
    server.starttls()
    server.login("you@gmail.com", app_password)
    server.send_message(msg)

Set the variable in your shell before running the script:

export GMAIL_APP_PASSWORD="xxxx xxxx xxxx xxxx"
python send_email.py

For production deployments, consider a secrets manager like AWS Secrets Manager or HashiCorp Vault instead of environment variables.

Error Handling

Network errors, authentication failures, and invalid recipients can all cause exceptions. Wrap your email operations in try/except blocks:

import smtplib

try:
    with smtplib.SMTP("smtp.gmail.com", 587) as server:
        server.starttls()
        server.login("user@gmail.com", "app_password")
        server.sendmail("user@gmail.com", ["recipient@example.com"], msg.as_string())
except smtplib.SMTPAuthenticationError:
    print("Authentication failed — check your app password")
except smtplib.SMTPException as e:
    print(f"SMTP error occurred: {e}")
except TimeoutError:
    print("Connection timed out — check your network or SMTP host")

For imaplib, catch imaplib.IMAP4.error for general IMAP errors and imaplib.IMAP4.abort for connection-level failures.

Common SMTP Provider Settings

ProviderSMTP HostTLS Port (587)SSL Port (465)
Gmailsmtp.gmail.comYesYes
Outlooksmtp-mail.outlook.comYesYes
Yahoosmtp.mail.yahoo.comYesYes
Office 365smtp.office365.comYesNo

These settings assume you are using an app password, not your actual account password.

See Also

  • Working with APIs — REST API calls are how services like SendGrid receive email instructions
  • Python Webhooks — Webhooks and email notifications often work together in automated workflows
  • Environment Variables — Safely storing SMTP credentials and API keys without hardcoding