Automate Emails with Python: Complete Guide

Published on: July 10, 2026
Reading time: 5 minutes
Python e ícone de e-mail sobre teclado de notebook

Learning how to automate emails with Python can save time when you need to send reports, reminders, alerts, invoices, or welcome messages. Python includes tools for building an email message and sending it through an SMTP server, so a simple script can replace repetitive manual work.

This guide shows a secure, beginner-friendly workflow with EmailMessage, smtplib, environment variables, HTML content, attachments, multiple recipients, scheduling, logging, and practical error handling.

You should already understand basic Python functions, exceptions, and file handling.

How Email Automation Works

Your Python program does not deliver a message directly to every recipient. It normally connects to an SMTP server provided by an email service. The server authenticates your account, accepts the message, and handles delivery.

Python’s official smtplib documentation explains the SMTP client. The email package documentation covers message construction and MIME content.

Important Security Rules

Do not place a real password or API key directly in your source code. Credentials committed to Git or copied into a tutorial can be exposed. Use environment variables, an application-specific password, or a transactional email provider’s token.

  • Enable encryption with TLS or SSL.
  • Use the minimum account permissions required.
  • Do not log passwords or tokens.
  • Respect provider limits and anti-spam rules.
  • Send only to recipients who expect the message.

For larger applications, read the environment variables guide.

Set Environment Variables

Set the following values in your operating system or deployment platform:

SMTP_HOST=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=your-app-password
[email protected]

The exact host and port depend on your provider. Port 587 commonly uses STARTTLS. Port 465 commonly uses an SSL connection from the beginning.

Create a Plain-Text Email

import os
from email.message import EmailMessage


def build_message(recipient: str) -> EmailMessage:
    message = EmailMessage()
    message["Subject"] = "Your weekly Python report"
    message["From"] = os.environ["EMAIL_FROM"]
    message["To"] = recipient
    message.set_content(
        """Hello!

Your automated report is ready.

Regards,
Python Bot"""
    )
    return message

EmailMessage handles headers and body encoding. It is clearer and safer than assembling raw email text manually.

Send the Message with STARTTLS

import os
import smtplib


def send_message(message) -> None:
    host = os.environ["SMTP_HOST"]
    port = int(os.environ.get("SMTP_PORT", "587"))
    username = os.environ["SMTP_USERNAME"]
    password = os.environ["SMTP_PASSWORD"]

    with smtplib.SMTP(host, port, timeout=20) as server:
        server.ehlo()
        server.starttls()
        server.ehlo()
        server.login(username, password)
        server.send_message(message)

The context manager closes the connection automatically. The timeout prevents the script from waiting forever when the server is unavailable.

Complete First Example

import os
import smtplib
from email.message import EmailMessage


def create_email(recipient: str) -> EmailMessage:
    message = EmailMessage()
    message["Subject"] = "Automated Python message"
    message["From"] = os.environ["EMAIL_FROM"]
    message["To"] = recipient
    message.set_content(
        """Hello!

This message was sent automatically with Python."""
    )
    return message


def send_email(message: EmailMessage) -> None:
    with smtplib.SMTP(
        os.environ["SMTP_HOST"],
        int(os.environ.get("SMTP_PORT", "587")),
        timeout=20,
    ) as server:
        server.starttls()
        server.login(
            os.environ["SMTP_USERNAME"],
            os.environ["SMTP_PASSWORD"],
        )
        server.send_message(message)


if __name__ == "__main__":
    email = create_email("[email protected]")
    send_email(email)
    print("Email sent successfully.")

Add an HTML Version

Provide plain text first, then add HTML as an alternative. Email clients that cannot display HTML still have readable content.

message.set_content("Your report is ready. Open the HTML version for details.")
message.add_alternative(
    """
    <html>
      <body>
        <h1>Weekly Report</h1>
        <p>Your automated report is ready.</p>
        <p><strong>Completed tasks:</strong> 18</p>
      </body>
    </html>
    """,
    subtype="html",
)

Keep HTML simple because email clients support a smaller set of web features than modern browsers.

Attach a File

Read the file as bytes, detect or define its MIME type, and attach it:

from pathlib import Path

report_path = Path("weekly-report.csv")

with report_path.open("rb") as file:
    message.add_attachment(
        file.read(),
        maintype="text",
        subtype="csv",
        filename=report_path.name,
    )

Use the pathlib guide for portable paths and the CSV guide to generate report files.

Send to Multiple Recipients

For a message where recipients may see each other, use a list in the To header:

recipients = [
    "[email protected]",
    "[email protected]",
]

message["To"] = ", ".join(recipients)

For private bulk communication, send separate messages or use Bcc carefully. A transactional email service is usually a better choice for large recipient lists because it provides delivery monitoring, bounce handling, and unsubscribe controls.

Personalize Messages from Data

customers = [
    {"name": "Alex", "email": "[email protected]"},
    {"name": "Sam", "email": "[email protected]"},
]

for customer in customers:
    message = EmailMessage()
    message["Subject"] = "Your account update"
    message["From"] = os.environ["EMAIL_FROM"]
    message["To"] = customer["email"]
    message.set_content(
        f"""Hello {customer['name']},

Your account update is ready."""
    )
    send_email(message)

When processing many records, validate every address and decide what should happen after an individual failure.

Handle SMTP Errors

import smtplib

try:
    send_email(message)
except smtplib.SMTPAuthenticationError:
    print("Authentication failed. Check the account and app password.")
except smtplib.SMTPRecipientsRefused:
    print("The server refused the recipient address.")
except smtplib.SMTPException as error:
    print(f"SMTP error: {error}")
except OSError as error:
    print(f"Network or system error: {error}")
else:
    print("Message accepted by the SMTP server.")

Being accepted by an SMTP server does not guarantee that the message reached the inbox. It may still bounce or be classified as spam.

Add Logging

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

try:
    send_email(message)
except Exception:
    logging.exception("Email delivery failed")
else:
    logging.info("Email submitted to the SMTP server")

Do not include message secrets or credentials in logs. The Python logging guide explains handlers, levels, and file rotation.

Schedule the Script

Keep scheduling outside the email code. On Linux and macOS, cron can run a script at a chosen time. On Windows, use Task Scheduler. Cloud platforms also provide scheduled jobs.

A good design has one function that creates a report, another that creates the email, and another that sends it. This separation makes the program easier to test and reuse.

Practical Project: Send a Daily Summary

import os
import smtplib
from datetime import date
from email.message import EmailMessage


def build_summary(completed: int, pending: int) -> str:
    return f"""Daily summary for {date.today():%Y-%m-%d}

Completed tasks: {completed}
Pending tasks: {pending}
"""


def create_summary_email(recipient: str) -> EmailMessage:
    message = EmailMessage()
    message["Subject"] = f"Daily summary - {date.today():%Y-%m-%d}"
    message["From"] = os.environ["EMAIL_FROM"]
    message["To"] = recipient
    message.set_content(build_summary(completed=12, pending=4))
    return message


def send_email(message: EmailMessage) -> None:
    with smtplib.SMTP(
        os.environ["SMTP_HOST"],
        int(os.environ.get("SMTP_PORT", "587")),
        timeout=20,
    ) as server:
        server.starttls()
        server.login(
            os.environ["SMTP_USERNAME"],
            os.environ["SMTP_PASSWORD"],
        )
        server.send_message(message)


if __name__ == "__main__":
    message = create_summary_email("[email protected]")
    send_email(message)

The date formatting is covered in the Python datetime guide.

Testing Email Automation

Do not send a real message in every automated test. Test message construction separately and mock the function that communicates with SMTP.

  • Verify the subject, sender, and recipient.
  • Check that the body contains expected values.
  • Confirm that attachments have the correct filename.
  • Mock authentication and sending failures.
  • Use a test inbox before enabling production delivery.

The Pytest beginner guide introduces assertions and mocks.

Common Mistakes

  • Hardcoding passwords in the script.
  • Using the wrong SMTP port or encryption mode.
  • Sending bulk messages from a personal mailbox.
  • Exposing all recipient addresses in the To field.
  • Not using a timeout.
  • Ignoring provider rate limits.
  • Creating HTML-only messages without a text alternative.
  • Retrying immediately and endlessly after a failure.

Frequently Asked Questions

Can Python send email without a Gmail account?

Yes. You can use any provider that offers SMTP access or a supported email API.

Should I use SMTP or an email API?

SMTP is convenient for small scripts. An API is often better for production systems, large volumes, analytics, templates, and bounce handling.

Can the script run every day?

Yes. Use cron, Task Scheduler, a cloud scheduler, or a workflow system.

Can I attach PDF and Excel files?

Yes. Read the file as bytes and use the correct MIME type. Avoid sending sensitive files without appropriate protection.

Conclusion

Python email automation combines message construction, secure credential handling, SMTP communication, scheduling, and error reporting. Begin with a single test recipient, use environment variables, enable encryption, and add a timeout.

As the workflow grows, separate report generation from delivery, log meaningful events, test without contacting the real server, and move high-volume communication to a dedicated email service.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Logos do Python e Excel lado a lado representando a importação de dados do Excel para Python.
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Import Excel Data into Python with Pandas

    Learn how to import Excel data into Python with Pandas and openpyxl, select sheets and columns, clean values, handle dates

    Ler mais

    Tempo de leitura: 4 minutos
    10/07/2026
    Logo do Python com o texto 'Python argparse CLI' abaixo
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Build a Python CLI with argparse

    Build a Python CLI with argparse. Learn positional and optional arguments, flags, choices, subcommands, validation, testing, and packaging.

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026
    Sistema de backup automático de arquivos usando Python
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Automate File Backups with Python

    Learn how to automate file backups with Python using shutil, os, pathlib, and datetime: define source/destination, copy folders, and schedule

    Ler mais

    Tempo de leitura: 3 minutos
    03/06/2026
    Compactação de arquivos ZIP usando Python
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Unzip .zip Files in Python Without Errors

    Learn how to unzip .zip files in Python without errors using the zipfile module: extractall, testzip integrity check, extract specific

    Ler mais

    Tempo de leitura: 3 minutos
    03/06/2026
    Execução de comandos do terminal usando Python
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    Run Terminal Commands with Python in 2 Minutes

    Run terminal commands from Python using subprocess.run: capture output, handle errors with check=True, use shell=True for pipes, and build a

    Ler mais

    Tempo de leitura: 3 minutos
    01/06/2026
    Script Python configurado para iniciar junto com o Windows
    Automation and Scripts
    Foto de perfil de Leandro Hirt da Academify

    How to Run a Python Script Automatically on Windows Startup

    Learn how to run Python scripts on Windows startup with the Startup folder, Task Scheduler, .bat files, pythonw.exe, and complete

    Ler mais

    Tempo de leitura: 8 minutos
    19/05/2026