Courses/Streamlit: 20 Real Apps/Module 5: APIs & Automation
Module 5 · Lesson 530 minIntermediate

Project 18: Email Automation

What you'll build
Send reports and alerts by email automatically — text, HTML, and attachments.

Introduction

Email is the API everything already has. Reports, alerts, reminders, weekly digests — if it can be emailed, it can be automated, and Python's built-in smtplib does it in a few dozen lines. This tutorial builds a reusable mailer that sends plain text, styled HTML, and file attachments through Gmail (or any SMTP provider), with credentials handled the secure way.

This is the delivery half of your automation stack: pair it with the web scraper to mail yourself scraped data, or the Excel report generator to ship formatted reports on a schedule.

Features

  • Plain and HTML emails — styled bodies with fallback text.
  • Attachments — any file type, MIME-encoded correctly.
  • Multiple recipients — To, CC, and BCC support.
  • Secure auth — app passwords, never your real password.
  • Reusable function — one send_email() call from any script.
  • Prerequisites

  • Python 3.8+smtplib and email are built in.
  • A Gmail App Password — Google Account → Security → 2-Step Verification → App passwords. (Regular passwords no longer work with Gmail SMTP.)
  • Step 1: Create the Script

    Save as mailer.py:

    code
    import smtplib
    import os
    from email.message import EmailMessage
    from email.utils import formataddr
    
    SMTP_HOST = "smtp.gmail.com"
    SMTP_PORT = 587
    
    
    def send_email(to, subject, body, html=None, attachments=None,
                   cc=None, bcc=None, sender_name="Python Automation"):
        user = os.environ["MAIL_USER"]
        password = os.environ["MAIL_PASSWORD"]
    
        msg = EmailMessage()
        msg["From"] = formataddr((sender_name, user))
        msg["To"] = ", ".join(to)
        msg["Subject"] = subject
        if cc:
            msg["Cc"] = ", ".join(cc)
    
        msg.set_content(body)
        if html:
            msg.add_alternative(html, subtype="html")
    
        for path in attachments or []:
            with open(path, "rb") as f:
                data = f.read()
            msg.add_attachment(
                data, maintype="application", subtype="octet-stream",
                filename=os.path.basename(path),
            )
    
        recipients = to + (cc or []) + (bcc or [])
        with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
            server.starttls()
            server.login(user, password)
            server.send_message(msg, to_addrs=recipients)
    
        print(f"Sent '{subject}' to {len(recipients)} recipient(s)")
    
    
    if __name__ == "__main__":
        send_email(
            to=["friend@example.com"],
            subject="Automated report",
            body="Hi!\n\nToday's report is attached.\n\n- The robot",
            html="""<h2 style='color:#0F766E'>Daily Report</h2>
                   <p>Everything ran <b>successfully</b>.</p>""",
            attachments=["report.xlsx"],
        )

    Step 2: Configure and Run

    code
    export MAIL_USER="you@gmail.com"
    export MAIL_PASSWORD="your-16-char-app-password"
    python mailer.py

    The email arrives with styled HTML, a plain-text fallback, and the attachment.

    How It Works

    SMTP is a conversation: connect to the server on port 587, upgrade to TLS with starttls(), log in, hand over the message. smtplib handles the protocol; your job is building the message correctly — which is what the email.message.EmailMessage class is for.

    EmailMessage manages MIME so you don't have to. set_content sets the plain text; add_alternative(html) attaches the styled version — mail clients that render HTML show it, everything else falls back to plain text. Attachments are base64-encoded automatically with correct headers; before EmailMessage existed, this was thirty lines of manual MIME.

    The credentials discipline is the security lesson: the app password comes from environment variables, never from code. App passwords (not your real password) are revocable per-app, so a leaked script doesn't compromise your account — the same principle as the file encryption tool's key handling: secrets never live in source.

    BCC works through the to_addrs argument, not the headers: BCC'd recipients receive the mail but don't appear in any header — that separation is exactly what the parameter provides.

    Common Errors & Fixes

  • `SMTPAuthenticationError` with a correct password — Gmail requires an *app password* with 2FA enabled; regular passwords are rejected outright.
  • Emails land in spam — automated mail from personal accounts often does; keep volume low, avoid ALL-CAPS subjects, and for real volume use a transactional service (Resend, SendGrid) with proper SPF/DKIM.
  • `SMTPServerDisconnected` — the connection dropped mid-send; port 587 + starttls() is the correct combo, port 465 needs SMTP_SSL instead.
  • Attachment arrives corrupt — you opened the file in text mode; always 'rb' for binary data.
  • Key Concepts

  • SMTP flow — connect, TLS, login, send.
  • MIME alternatives — plain text plus HTML in one message.
  • Environment secrets — credentials via env vars, revocable app passwords.
  • `to_addrs` vs headers — envelope recipients control BCC invisibility.
  • What to Try Next

  • Schedule it with cron on your home server for a weekly digest.
  • Mail yourself scraped price drops from the web scraper.
  • Add inline images with add_alternative + CID attachments for branded newsletters.
  • Build a mail-merge script — one template, a CSV of recipients, personalized sends with a delay.
  • FAQ

    Can I send from a non-Gmail address?

    Yes — change SMTP_HOST/SMTP_PORT to your provider's (Outlook: smtp.office365.com:587; Yahoo: smtp.mail.yahoo.com:587). The rest is identical.

    How many emails can I send per day?

    Gmail allows ~500/day for personal accounts. For anything beyond notifications and digests, use a transactional email service.

    Is starttls() secure enough?

    Yes — it upgrades the connection to TLS before credentials are sent. The riskier pattern is port 25 without TLS, which you should never use with authentication.

    Adapted from: Email Automation using Python and smtplib

    Checkpoint
    A styled email with an attachment arrives in your inbox from your script.
    What you learned
    • SMTP flow with TLS
    • MIME: plain + HTML alternatives
    • App passwords and env secrets