|

How to Send Emails Automatically Using Python — Complete Step-by-Step Guide (2026)

Imagine your Python script finishing a task and instantly emailing you the results — no manual work, no copy-pasting, no forgetting. That is the power of email automation, and it is one of the most practical skills a Python developer can learn.

If you’re new to Python, you can start with the basics on the PyCodeRoom homepage, where you’ll find beginner-friendly guides to build a strong foundation before diving into automation.

Whether you want to receive daily reports, send password reset links, fire off server alerts, or notify customers automatically, you can send emails automatically using Python with just a few lines of code and the libraries that come built right into the language. For example, you might collect dynamic data using user input in Python or read content directly from files using this guide on reading and writing text files in Python, and then include that data in your automated emails.

In this guide, you will go from zero to a fully working email automation script. Every concept is explained clearly, every code block is practical, and every step is designed with beginners in mind.

What You Will Learn in This Guide

  • How SMTP (email sending protocol) works
  • Which Python libraries to use and why
  • How to set up Gmail securely with App Passwords
  • Sending plain text emails step by step
  • Sending beautiful HTML emails
  • Attaching files (PDFs, images, CSVs)
  • Emailing multiple recipients with personalization
  • Secure connections using TLS and SSL
  • Email APIs vs raw SMTP — which to choose
  • Robust error handling and debugging
  • The most common beginner mistakes and how to avoid them
  • Best practices for security and reliability
  • Real-world mini projects you can build today

Prerequisites: Python 3.7+ installed, a Gmail account, and basic knowledge of Python variables and functions. New to Python functions? Check out our Python Functions Guide before diving in.


Why Email Automation Matters

Sending emails manually is slow, repetitive, and error-prone. A single Python script can replace hours of copy-pasting and clicking. Here are just a few situations where email automation pays off immediately:

  • System monitoring — Get alerted the moment your server goes down or a script fails
  • Scheduled reporting — Receive a formatted sales or attendance report every Monday morning
  • User notifications — Automatically email customers after a purchase or form submission
  • Data pipelines — Send a summary email when a data processing job completes
  • Personal productivity — Get a daily weather briefing or task reminder in your inbox

The good news? Python makes all of this surprisingly straightforward.


Understanding SMTP — The Engine Behind Email Sending

Before writing a single line of code, it helps to understand what is actually happening when an email is sent.

What is SMTP?

SMTP stands for Simple Mail Transfer Protocol. It is the communication standard that governs how email is transmitted between computers and mail servers across the internet. Introduced in 1982 and updated in RFC 5321, SMTP is the universal language of email delivery.

Think of it like a postal system. When you send an email, your email client (or Python script) acts as the sender handing a letter to a post office (the SMTP server). That post office routes the letter to the recipient’s mail server, which delivers it to their inbox.

The Python Email Sending Flow

Your Python Script
       ↓
SMTP Server (e.g., smtp.gmail.com)
       ↓
Recipient's Mail Server
       ↓
Recipient's Inbox

Key SMTP Terms You Need to Know

TermWhat It Means
SMTP ServerThe mail server that processes and routes outgoing emails
Port 587Used for TLS/STARTTLS connections (modern recommended standard)
Port 465Used for direct SSL connections (encrypted from the first byte)
EHLOExtended Hello — how Python introduces itself to the SMTP server
STARTTLSA command that upgrades a plain connection to an encrypted one
AuthenticationVerifying your identity with a username and password (or App Password)

Common SMTP Server Addresses

Email ProviderSMTP Server Address
Gmailsmtp.gmail.com
Outlook / Hotmailsmtp.office365.com
Yahoo Mailsmtp.mail.yahoo.com
Custom domainProvided by your web host or email provider

Required Python Libraries — smtplib and email

One of the best things about Python email automation is that you do not need to install anything for basic sending. All the tools you need are already included in Python’s standard library.

smtplib — The Connection Manager

smtplib is Python’s built-in module for establishing and managing SMTP connections. It handles the low-level communication between your script and the mail server.

import smtplib  # Already installed — no pip needed

Key methods you will use:

MethodWhat It Does
smtplib.SMTP(host, port)Creates an SMTP connection object
smtplib.SMTP_SSL(host, port)Creates a direct SSL-encrypted connection
.ehlo()Identifies your script to the server
.starttls(context)Upgrades the connection to TLS encryption
.login(user, password)Authenticates with your credentials
.send_message(msg)Sends a pre-built email message object
.sendmail(from, to, msg)Sends a raw email string
.quit()Closes the connection gracefully

The email Module Family — For Formatting Messages

While smtplib handles sending, the email module handles building the email content. It gives you the building blocks to construct everything from simple text emails to complex multi-part messages with attachments.

SubmodulePurpose
email.mime.text.MIMETextCreates plain text or HTML email bodies
email.mime.multipart.MIMEMultipartCombines multiple parts (body + attachments)
email.mime.base.MIMEBaseBase class for encoding file attachments
email.mime.applicationFor PDFs and application files
email.encodersEncodes binary attachment data (Base64)
email.message.EmailMessageModern, simpler API available in Python 3.6+

The ssl Module — For Encrypted Connections

The built-in ssl module lets you create secure SSL/TLS contexts for encrypted SMTP connections. Always use it — never send credentials over an unencrypted connection.

import ssl
context = ssl.create_default_context()
# Automatically validates server certificate, checks hostname,
# and uses the strongest available cipher suite

For an overview of Python’s powerful built-in tools, explore our Python Standard Library Guide.


Setting Up Gmail for Python Email Automation

Why Your Regular Gmail Password Will Not Work

Since May 2022, Google has blocked direct username and password access via SMTP for third-party applications. If you try using your normal Gmail password in a Python script, you will get an authentication error:

smtplib.SMTPAuthenticationError: (534) Application-specific password required

The solution is a Gmail App Password — a special 16-character password generated specifically for your Python script, completely separate from your main account password.

Step-by-Step: Generate a Gmail App Password

  1. Go to your Google Account at myaccount.google.com
  2. Click on Security in the left sidebar
  3. Enable 2-Step Verification if you have not already (this is required)
  4. In the search bar at the top of the Security page, type “App Passwords”
  5. Click the result and sign in again if prompted
  6. Under “App name”, type something descriptive like Python Email Script
  7. Click Create
  8. A 16-character password will appear — copy it immediately (you will not see it again)
  9. Store it safely — you will use this instead of your Gmail password

⚠️ Security Note: Treat your App Password like your bank PIN. Anyone with it can send email from your Gmail account.

Storing Credentials Safely with Environment Variables

Never paste your email or password directly into your Python code. If you ever share the file, push it to GitHub, or let someone look over your shoulder, your credentials are exposed.

The correct approach is to use environment variables — values stored in your operating system that your script reads at runtime.

Setting environment variables:

On Mac/Linux (Terminal):

export EMAIL_SENDER="you@gmail.com"
export EMAIL_PASSWORD="your-16-char-app-password"

On Windows (Command Prompt):

set EMAIL_SENDER=you@gmail.com
set EMAIL_PASSWORD=your-16-char-app-password

Reading them in Python:

import os

sender_email = os.environ.get("EMAIL_SENDER")
password = os.environ.get("EMAIL_PASSWORD")

Better approach for projects — use a .env file:

pip install python-dotenv

Create a file named .env in your project folder:

EMAIL_SENDER=you@gmail.com
EMAIL_PASSWORD=your-16-char-app-password

Load it in Python:

from dotenv import load_dotenv
import os

load_dotenv()  # Reads .env file into environment
sender_email = os.getenv("EMAIL_SENDER")
password = os.getenv("EMAIL_PASSWORD")

Add .env to your .gitignore file so it is never committed to version control:

# .gitignore
.env
*.env

Sending Your First Plain Text Email — Step by Step

Now for the moment you have been waiting for. Let us send an actual email.

There are two secure methods available. Both produce the same result — a fully encrypted, authenticated email sent via Gmail. The difference is when encryption kicks in.

Method 1: SMTP_SSL (Port 465 — Encrypted from the Start)

This method creates an SSL-encrypted connection immediately when you connect. It is slightly more concise.

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

# --- Configuration ---
sender_email = os.environ.get("EMAIL_SENDER")
receiver_email = "recipient@example.com"
password = os.environ.get("EMAIL_PASSWORD")

# --- Build the Email ---
subject = "Hello from Python!"
body = "This email was sent automatically using Python. Great job setting this up!"

message = MIMEText(body, "plain")
message["Subject"] = subject
message["From"] = sender_email
message["To"] = receiver_email

# --- Send the Email ---
context = ssl.create_default_context()

with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
    server.login(sender_email, password)
    server.send_message(message)
    print("Email sent successfully!")

Method 2: SMTP with STARTTLS (Port 587 — Upgraded to Encrypted)

This method starts with a plain connection and upgrades it to encrypted using the STARTTLS command. This is the modern recommended approach and the one you will see most often in production systems.

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

# --- Configuration ---
sender_email = os.environ.get("EMAIL_SENDER")
receiver_email = "recipient@example.com"
password = os.environ.get("EMAIL_PASSWORD")

# --- Build the Email ---
subject = "Hello via STARTTLS!"
body = "This email was sent using Python with TLS encryption on port 587."

message = MIMEText(body, "plain")
message["Subject"] = subject
message["From"] = sender_email
message["To"] = receiver_email

# --- Send the Email ---
context = ssl.create_default_context()

with smtplib.SMTP("smtp.gmail.com", 587) as server:
    server.ehlo()                        # Step 1: Identify to the server
    server.starttls(context=context)     # Step 2: Upgrade to TLS encryption
    server.ehlo()                        # Step 3: Re-identify after TLS upgrade
    server.login(sender_email, password) # Step 4: Authenticate
    server.send_message(message)         # Step 5: Send
    print("Email sent successfully!")

What Each Line Does

CodePurpose
MIMEText(body, "plain")Creates a plain text email body
message["Subject"]Sets the email subject line
message["From"] / ["To"]Sets header info (for display in email client)
with smtplib.SMTP(...) as serverOpens connection, auto-closes when done
server.ehlo()Sends EHLO greeting — tells server who you are
server.starttls()Encrypts the connection using TLS
server.login()Authenticates with your credentials
server.send_message()Transmits the email

Tip: Test Without Sending — Use Python’s Built-In Debugging Server

During development, you can capture emails locally without sending them to anyone:

# Run this in a terminal — it prints all emails instead of sending them
python -m smtpd -c DebuggingServer -n localhost:1025

Then in your script, connect to localhost:1025 instead of Gmail. Perfect for testing logic without using your real credentials or email quota.


Sending HTML Emails with Python

Plain text gets the job done, but HTML emails look professional — with formatting, colors, buttons, and branding. Almost every commercial email you receive is HTML under the hood.

Why Use HTML Emails?

  • Visual impact — Bold text, colors, headings, and images grab attention
  • Clickable buttons — Add styled call-to-action links
  • Professional appearance — Looks like it came from a real product or company
  • Compatibility — Supported by virtually every modern email client

Sending an HTML Email with a Plain Text Fallback

The professional approach is to send both an HTML version and a plain text version in the same email. The recipient’s email client automatically picks the best one it can display. This also helps with spam filters.

import smtplib
import ssl
import os
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# --- Configuration ---
sender_email = os.environ.get("EMAIL_SENDER")
receiver_email = "recipient@example.com"
password = os.environ.get("EMAIL_PASSWORD")

# --- Build the Email Container ---
# "alternative" means: show HTML if supported, fall back to plain text
message = MIMEMultipart("alternative")
message["Subject"] = "Your Python HTML Email Is Here"
message["From"] = sender_email
message["To"] = receiver_email

# --- Plain Text Version (fallback) ---
plain_text = """\
Hello!

This email was sent using Python.
If you cannot see the HTML version, this plain text is your fallback.

Visit: https://python.org
"""

# --- HTML Version ---
html_content = """\
<html>
  <body style="font-family: Arial, sans-serif; color: #333; padding: 20px;">

    <h2 style="color: #2c3e50;">Hello from Python! 🐍</h2>

    <p>
      This is a <strong>professionally formatted HTML email</strong>
      sent automatically using Python and smtplib.
    </p>

    <p>Here is what you can do with Python email automation:</p>
    <ul>
      <li>✅ Send daily reports</li>
      <li>✅ Fire server alerts</li>
      <li>✅ Notify users automatically</li>
    </ul>

    <a href="https://python.org"
       style="display:inline-block; background:#3498db; color:white;
              padding:12px 24px; text-decoration:none; border-radius:5px;
              font-weight:bold;">
      Learn More at Python.org
    </a>

    <hr style="margin-top:30px; border:none; border-top:1px solid #eee;">
    <small style="color:#999;">
      Sent automatically using Python smtplib &mdash; not spam, just automation!
    </small>

  </body>
</html>
"""

# --- Attach Both Parts (order matters: plain first, HTML last) ---
message.attach(MIMEText(plain_text, "plain"))
message.attach(MIMEText(html_content, "html"))

# --- Send ---
context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
    server.login(sender_email, password)
    server.send_message(message)
    print("HTML email sent!")

HTML Email Best Practices

  • Always include plain text — many spam filters and corporate email systems expect it
  • Use inline CSS only — most email clients (especially Outlook) strip <style> tags from the <head>
  • Avoid JavaScript — it is blocked by every email client without exception
  • Keep it simple — complex layouts can break in Outlook; test in multiple clients
  • Use absolute URLs — relative paths do not work in emails

Sending Emails with File Attachments

Attaching files to automated emails is one of the most commonly needed features — think monthly PDF reports, exported CSVs, or image notifications.

How Attachments Work in Python

Attachments use three key components:

  1. MIMEMultipart("mixed") — a container that holds both the email body and the attached file
  2. MIMEBase — a generic wrapper for the binary file data
  3. encoders.encode_base64() — converts binary data into a text-safe Base64 format that email can carry

Complete Example: Sending a PDF Attachment

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

# --- Configuration ---
sender_email = os.environ.get("EMAIL_SENDER")
receiver_email = "manager@example.com"
password = os.environ.get("EMAIL_PASSWORD")
file_path = "monthly_report.pdf"   # Path to the file you want to attach

# --- Build Email Container ---
message = MIMEMultipart()
message["Subject"] = "Monthly Sales Report — July 2025"
message["From"] = sender_email
message["To"] = receiver_email

# --- Email Body ---
body = (
    "Hi,\n\n"
    "Please find the monthly sales report attached to this email.\n\n"
    "This report was generated and sent automatically.\n\n"
    "Best regards,\n"
    "Automated Reporting System"
)
message.attach(MIMEText(body, "plain"))

# --- Attach the File ---
with open(file_path, "rb") as attachment_file:
    part = MIMEBase("application", "octet-stream")
    part.set_payload(attachment_file.read())

# Encode to Base64 so it travels safely through email
encoders.encode_base64(part)

# Add header telling the email client to treat this as a downloadable file
part.add_header(
    "Content-Disposition",
    f"attachment; filename={os.path.basename(file_path)}"
)
message.attach(part)

# --- Send ---
context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
    server.login(sender_email, password)
    server.send_message(message)
    print(f"Email with attachment '{file_path}' sent successfully!")

Auto-Detect File Type with mimetypes

For a cleaner approach that automatically handles any file type:

import mimetypes

def attach_file(message, file_path):
    """Attach any file to an email message, auto-detecting its type."""
    mime_type, _ = mimetypes.guess_type(file_path)
    
    if mime_type is None:
        mime_type = "application/octet-stream"  # Default for unknown types
    
    main_type, sub_type = mime_type.split("/", 1)
    
    with open(file_path, "rb") as f:
        part = MIMEBase(main_type, sub_type)
        part.set_payload(f.read())
    
    encoders.encode_base64(part)
    part.add_header(
        "Content-Disposition",
        f"attachment; filename={os.path.basename(file_path)}"
    )
    message.attach(part)
    return message

This function works with PDFs, images, ZIP files, CSVs, Excel files — anything. For deeper file reading and writing skills, visit our Python File Handling Guide.


Sending Emails to Multiple Recipients

Method 1: Sending to a Simple List

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

sender_email = os.environ.get("EMAIL_SENDER")
password = os.environ.get("EMAIL_PASSWORD")

# Define your recipient list
recipients = [
    "alice@example.com",
    "bob@example.com",
    "carol@example.com"
]

subject = "Team Update — Q3 Results"
body = "Hi Team,\n\nPlease review the Q3 results attached to the dashboard.\n\nThanks!"

message = MIMEText(body, "plain")
message["Subject"] = subject
message["From"] = sender_email
message["To"] = ", ".join(recipients)  # Display all recipients in the To header

context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
    server.login(sender_email, password)
    # Pass the list directly to sendmail — this is what controls actual delivery
    server.sendmail(sender_email, recipients, message.as_string())
    print(f"Email sent to {len(recipients)} recipients!")

Method 2: Personalized Emails from a CSV File

This is the most powerful bulk email pattern — each recipient gets a message addressed specifically to them.

First, create a contacts.csv file:

name,email
Alice Johnson,alice@example.com
Bob Smith,bob@example.com
Carol White,carol@example.com

Then run this script:

import csv
import smtplib
import ssl
import os
import time
from email.mime.text import MIMEText

sender_email = os.environ.get("EMAIL_SENDER")
password = os.environ.get("EMAIL_PASSWORD")
context = ssl.create_default_context()

with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
    server.login(sender_email, password)

    with open("contacts.csv", newline="", encoding="utf-8") as csvfile:
        reader = csv.DictReader(csvfile)

        for row in reader:
            name = row["name"]
            recipient = row["email"]

            # Personalize the body for each individual
            body = (
                f"Hi {name},\n\n"
                f"Thank you for being part of our community!\n"
                f"We have exciting updates to share with you this week.\n\n"
                f"Best regards,\n"
                f"The Team"
            )

            message = MIMEText(body, "plain")
            message["Subject"] = f"A Personal Update for You, {name.split()[0]}!"
            message["From"] = sender_email
            message["To"] = recipient

            server.send_message(message)
            print(f"Sent to {name} ({recipient})")

            time.sleep(1)  # Wait 1 second between sends to avoid rate limits

print("All emails sent!")

Adding CC and BCC

# CC — visible to all recipients
message["Cc"] = "team_lead@example.com"

# BCC — hidden from other recipients
# Add to sendmail recipient list but NOT to the message headers
cc_recipient = "team_lead@example.com"
bcc_recipient = "archive@example.com"

all_recipients = [receiver_email, cc_recipient, bcc_recipient]
server.sendmail(sender_email, all_recipients, message.as_string())
# Note: bcc_recipient receives the email but is not visible in the headers

Using Secure Connections — TLS and SSL Explained

Security is not optional when sending automated emails. Your credentials and email content travel over the internet — without encryption, they can be intercepted.

SSL vs TLS — What Is the Difference?

Both SSL and TLS provide encryption. The difference is the handshake timing:

FeatureSSL (Port 465)TLS / STARTTLS (Port 587)
Connection startEncrypted from the very first byteStarts plain, upgrades to encrypted
Python methodsmtplib.SMTP_SSL()smtplib.SMTP() + .starttls()
Security levelBoth are secure for modern useConsidered the modern standard
Use caseLegacy systems, simpler codeModern apps, recommended default

Neither is significantly more secure than the other today. Use port 587 + STARTTLS if you are unsure — it is what most modern email servers expect.

Always Use ssl.create_default_context()

import ssl
context = ssl.create_default_context()

This single line automatically:

  • ✅ Loads your system’s trusted CA certificates
  • ✅ Validates the SMTP server’s SSL certificate
  • ✅ Checks that the hostname matches the certificate
  • ✅ Selects the strongest available cipher suite

What You Must Never Do

# ❌ DANGER — Disabling certificate verification removes all security
context = ssl.create_default_context()
context.check_hostname = False           # Never do this
context.verify_mode = ssl.CERT_NONE      # Never do this

# This makes you vulnerable to man-in-the-middle attacks
# Someone could intercept your email and credentials silently

There is no legitimate reason to disable certificate verification in a properly configured environment. If you are getting SSL errors, fix the root cause instead of bypassing security.


Email APIs vs SMTP — Which Approach Should You Use?

As you grow beyond personal scripts, you will encounter Email APIs — cloud services that handle email sending on your behalf. Understanding when to use each option is an important step in your development journey.

What is an Email API?

Instead of connecting directly to an SMTP server, you make an HTTP request to a third-party service’s API. They handle the infrastructure, deliverability, bounce management, and compliance. Popular options include SendGrid, Mailgun, Mailtrap, Amazon SES, and Postmark.

SMTP vs Email API — Direct Comparison

FactorRaw SMTP (smtplib)Email API (SendGrid, Mailgun, etc.)
SetupMinimal — built into PythonRequires account signup and API key
Best forLearning, scripts, personal useProduction apps, business systems
DeliverabilityDepends on your IP/domainHigh — professionally managed
AnalyticsNoneOpens, clicks, bounces, unsubscribes
Daily limit (free)~500 (Gmail)Varies — typically 100–300/day free
ScalabilityLimitedHandles millions of emails
Bounce handlingManualAutomatic
CostFreeFree tier + paid plans

When to Use Raw SMTP (smtplib)

  • You are learning Python email automation (this guide!)
  • Sending personal notifications or alerts from scripts
  • Internal tools with low email volume (under 100/day)
  • Situations where installing external libraries is not possible

When to Use an Email API

  • Building a production web application (Flask, Django, FastAPI)
  • Sending hundreds or thousands of emails
  • Needing delivery analytics and bounce tracking
  • Requiring guaranteed high deliverability and sender reputation management
  • Running marketing campaigns or transactional email at scale

Quick Example: Sending with the SendGrid API

pip install sendgrid
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

message = Mail(
    from_email="you@yourdomain.com",
    to_emails="recipient@example.com",
    subject="Automated Email via SendGrid API",
    html_content="<strong>This was sent using the SendGrid API and Python!</strong>"
)

try:
    sg = SendGridAPIClient(os.environ.get("SENDGRID_API_KEY"))
    response = sg.send(message)
    print(f"Status Code: {response.status_code}")
    # 202 = Accepted for delivery
except Exception as e:
    print(f"Error: {e}")

For the full SendGrid Python setup, see the official SendGrid Python documentation.


Error Handling and Debugging Email Scripts

Email scripts frequently run unattended — as scheduled jobs, background processes, or server daemons. When something goes wrong, you need your script to tell you clearly what failed and why, not just crash silently.

Proper error handling is not optional for production scripts. To strengthen your general Python error handling skills, visit our Python Exception Handling Guide.

Common smtplib Exceptions and Their Fixes

ExceptionWhat It MeansHow to Fix It
SMTPAuthenticationErrorCredentials rejectedUse Gmail App Password; check 2FA is enabled
SMTPConnectErrorCannot reach the serverCheck internet connection, port, firewall
SMTPRecipientsRefusedRecipient address invalidValidate email format before sending
SMTPSenderRefusedSender not authorizedEnsure “From” matches your login email
ConnectionRefusedErrorPort blocked or wrongTry port 465 if 587 fails (or vice versa)
ssl.SSLErrorCertificate or TLS issueUpdate Python; check ssl.create_default_context()
TimeoutErrorConnection timed outCheck network; increase timeout with smtplib.SMTP(timeout=10)

A Professional Email Sending Function with Full Error Handling

import smtplib
import ssl
import logging
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

# Configure logging so errors are recorded with timestamps
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("email_log.txt"),  # Save to file
        logging.StreamHandler()                 # Also print to console
    ]
)

def send_email(
    receiver_email: str,
    subject: str,
    body_plain: str,
    body_html: str = None
) -> bool:
    """
    Send an email with full error handling and logging.

    Args:
        receiver_email: Recipient's email address
        subject: Email subject line
        body_plain: Plain text email body (always required)
        body_html: Optional HTML body (if provided, sends as multipart)

    Returns:
        True if sent successfully, False otherwise
    """
    sender_email = os.environ.get("EMAIL_SENDER")
    password = os.environ.get("EMAIL_PASSWORD")

    # Validate that credentials are available
    if not sender_email or not password:
        logging.error("Missing EMAIL_SENDER or EMAIL_PASSWORD environment variables.")
        return False

    # Build the message
    if body_html:
        message = MIMEMultipart("alternative")
        message.attach(MIMEText(body_plain, "plain"))
        message.attach(MIMEText(body_html, "html"))
    else:
        message = MIMEText(body_plain, "plain")

    message["Subject"] = subject
    message["From"] = sender_email
    message["To"] = receiver_email

    context = ssl.create_default_context()

    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
            server.login(sender_email, password)
            server.send_message(message)
            logging.info(f"Email sent successfully to {receiver_email} | Subject: {subject}")
            return True

    except smtplib.SMTPAuthenticationError:
        logging.error("Authentication failed. Verify your Gmail App Password and 2FA settings.")
    except smtplib.SMTPConnectError:
        logging.error("Could not connect to SMTP server. Check internet and firewall settings.")
    except smtplib.SMTPRecipientsRefused as e:
        logging.error(f"Recipient address refused: {e}")
    except smtplib.SMTPSenderRefused as e:
        logging.error(f"Sender address refused: {e}")
    except smtplib.SMTPException as e:
        logging.error(f"An SMTP error occurred: {e}")
    except Exception as e:
        logging.error(f"Unexpected error while sending email: {e}")

    return False


# --- Usage ---
if __name__ == "__main__":
    success = send_email(
        receiver_email="friend@example.com",
        subject="Automated Report",
        body_plain="Hello! Your weekly report is ready.",
        body_html="<h2>Hello!</h2><p>Your weekly report is ready.</p>"
    )

    if not success:
        print("Email failed to send. Check email_log.txt for details.")

Enable SMTP Debug Mode During Development

with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
    server.set_debuglevel(2)  # Print all SMTP conversation to console
    server.login(sender_email, password)
    server.send_message(message)

Setting debuglevel=2 prints every command and response exchanged between your script and the SMTP server — invaluable for diagnosing authentication or connection issues.


Common Mistakes Beginners Make

Knowing what not to do can save you hours of head-scratching. Here are the most frequent mistakes beginners encounter when automating emails with Python.

Mistake 1: Hardcoding Credentials in the Script

# ❌ WRONG — Exposed if you share the file or push to GitHub
password = "myGmailPassword123"
sender = "myemail@gmail.com"

# ✅ CORRECT — Use environment variables
password = os.environ.get("EMAIL_PASSWORD")
sender = os.environ.get("EMAIL_SENDER")

This is the most critical security mistake. Even a private GitHub repository can be accidentally made public. Even a shared code file in a Slack message exposes your credentials to everyone who sees it.

Mistake 2: Using Your Regular Gmail Password

Since May 2022, Google blocks normal password authentication via SMTP. Always use a Gmail App Password — a separate 16-character password generated specifically for your script.

Symptom you will see:

smtplib.SMTPAuthenticationError: (534) Please log in via your web browser

Fix: Generate an App Password in Google Account → Security → App Passwords.

Mistake 3: Forgetting the Plain Text Fallback in HTML Emails

# ❌ WRONG — HTML-only email may be flagged as spam
message = MIMEMultipart("alternative")
message.attach(MIMEText(html_content, "html"))  # Only HTML — missing plain text!

# ✅ CORRECT — Always attach both
message.attach(MIMEText(plain_text, "plain"))   # Plain text first
message.attach(MIMEText(html_content, "html"))  # HTML second (takes priority)

Many spam filters and corporate email gateways treat HTML-only emails with suspicion. Always include a plain text version.

Mistake 4: Not Using the with Statement for SMTP Connections

# ❌ WRONG — Connection may stay open if an error occurs
server = smtplib.SMTP_SSL("smtp.gmail.com", 465)
server.login(email, password)
server.send_message(msg)
# If anything above raises an exception, server.quit() is never called!

# ✅ CORRECT — Context manager guarantees cleanup
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
    server.login(email, password)
    server.send_message(msg)
# Connection is automatically closed even if an exception occurs

Mistake 5: Blasting Bulk Emails Without a Delay

# ❌ WRONG — Sending too fast triggers Gmail's rate limiter
for recipient in recipients:
    server.send_message(build_message(recipient))
    # No pause — Gmail will temporarily block your account

# ✅ CORRECT — Add a small delay between sends
import time
for recipient in recipients:
    server.send_message(build_message(recipient))
    time.sleep(1)  # 1 second between each email

Gmail’s free SMTP has a limit of approximately 500 emails per day and can temporarily throttle accounts that send too fast. A 1-second delay is a safe practice.

Mistake 6: Not Validating Email Addresses

import re

def is_valid_email(email: str) -> bool:
    """Basic email format validation."""
    pattern = r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

# Always validate before sending
if is_valid_email(receiver_email):
    send_email(receiver_email, subject, body)
else:
    logging.warning(f"Skipped invalid email address: {receiver_email}")

Sending to malformed addresses wastes quota and can trigger SMTP errors that crash unhandled scripts.

Mistake 7: Committing .env Files or Config Files to Git

# Create .gitignore in your project root
echo ".env" >> .gitignore
echo "*.env" >> .gitignore
echo "config_secrets.py" >> .gitignore
git add .gitignore
git commit -m "Add .gitignore to protect credentials"

Check your repository history if you have already pushed sensitive data — you may need to remove it from git history using git filter-branch or git-filter-repo.


Best Practices for Secure and Reliable Email Automation

Security Best Practices

  • Use TLS (port 587) or SSL (port 465) — never send credentials or content on port 25 (unencrypted)
  • Store all credentials in environment variables — never hardcode them
  • Use Gmail App Passwords — not your real account password
  • Add .env to .gitignore — keep secrets out of version control
  • Use ssl.create_default_context() — never disable certificate verification
  • Rotate App Passwords periodically — especially if you suspect exposure
  • For production apps, use an Email API — SendGrid, Mailgun, or Amazon SES with API keys

Reliability Best Practices

  • Wrap every send in try/except — no unhandled exceptions in production
  • Use logging instead of print() — gives you timestamped, searchable records
  • Add retry logic — transient network failures should not permanently fail your script
  • Add time delays for bulk sends — avoid rate limiting and spam flagging
  • Validate email addresses before attempting delivery

Email Content Best Practices

  • Include both HTML and plain text — for compatibility and spam filter compliance
  • Write clear, honest subject lines — avoid spam trigger words (FREE!!!, URGENT, Click NOW)
  • Include an unsubscribe option in any bulk or marketing email (legally required in many regions)
  • Set up SPF and DKIM records if sending from a custom domain — dramatically improves deliverability
  • Test your emails before deploying — use Mailtrap for safe inbox testing

Code Organization Best Practices

  • Wrap email logic in reusable functions with clear names and docstrings
  • Separate configuration from logic — keep SMTP settings in one place
  • Use type hints — makes code easier to maintain and debug
  • Write modular code — one function to build the email, one to send it

To apply these patterns across your Python projects, explore our Python Automation Scripts Guide.


Real-World Mini Projects to Build Today

Theory is useful, but building real projects is how you master email automation. Here are five practical projects of increasing complexity.


Mini Project 1: Daily Weather Alert Email

What it does: Fetches today’s weather from a free API and emails it to you every morning.

How to build it:

  1. Get a free API key from OpenWeatherMap
  2. Use Python’s requests library to fetch weather data
  3. Format it as a clean HTML email
  4. Schedule it with Python’s schedule library or a system cron job
import requests
import os

def get_weather_html(city: str, api_key: str) -> str:
    url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric"
    data = requests.get(url).json()
    temp = data["main"]["temp"]
    desc = data["weather"][0]["description"].title()
    return f"<h2>Weather in {city}</h2><p><strong>{temp}°C</strong> — {desc}</p>"

Mini Project 2: Automated CSV Report Sender

What it does: Reads a data CSV, generates a summary, and emails it with the original file attached.

Skills used: File handling, CSV reading, email attachments, scheduling

How to build it:

  1. Use Python’s csv or pandas library to read the data
  2. Generate a summary (totals, averages, highest/lowest values)
  3. Format the summary as HTML
  4. Attach the original CSV file
  5. Schedule to run every Monday at 8:00 AM

For CSV reading and file manipulation, visit our Python File Handling Guide.


Mini Project 3: Server Health Monitor with Email Alerts

What it does: Monitors CPU, memory, or disk usage. If any threshold is exceeded, it immediately emails the system administrator.

import psutil  # pip install psutil
from datetime import datetime

def check_and_alert(threshold_cpu=80, threshold_mem=85):
    cpu = psutil.cpu_percent(interval=1)
    mem = psutil.virtual_memory().percent
    
    alerts = []
    if cpu > threshold_cpu:
        alerts.append(f"⚠️ CPU usage is at {cpu}% (threshold: {threshold_cpu}%)")
    if mem > threshold_mem:
        alerts.append(f"⚠️ Memory usage is at {mem}% (threshold: {threshold_mem}%)")
    
    if alerts:
        body = f"Server Alert — {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"
        body += "\n".join(alerts)
        send_email("admin@yourserver.com", "🚨 Server Alert", body)

Mini Project 4: Birthday Email Automation

What it does: Checks a contacts CSV daily. If today is someone’s birthday, it sends them a personalized greeting.

from datetime import date
import csv

def send_birthday_emails():
    today = date.today()
    
    with open("contacts.csv") as f:
        reader = csv.DictReader(f)  # Columns: name, email, birthday (MM-DD)
        
        for row in reader:
            month, day = map(int, row["birthday"].split("-"))
            if today.month == month and today.day == day:
                body = (
                    f"Happy Birthday, {row['name']}! 🎂\n\n"
                    f"Wishing you an amazing day filled with joy!\n\n"
                    f"Warmly,\nYour Automated Greeter"
                )
                send_email(row["email"], f"Happy Birthday, {row['name']}! 🎉", body)
                print(f"Birthday email sent to {row['name']}")

Mini Project 5: Contact Form Email Notifier (Flask)

What it does: Builds a web form. When a visitor submits it, you receive an email notification immediately.

from flask import Flask, request, jsonify
import os

app = Flask(__name__)

@app.route("/contact", methods=["POST"])
def contact():
    data = request.json  # or request.form for HTML forms
    name = data.get("name", "Unknown")
    email = data.get("email", "Not provided")
    message = data.get("message", "")
    
    email_body = (
        f"New contact form submission!\n\n"
        f"Name: {name}\n"
        f"Email: {email}\n\n"
        f"Message:\n{message}"
    )
    
    success = send_email(
        receiver_email=os.environ.get("EMAIL_SENDER"),
        subject=f"New Contact from {name}",
        body_plain=email_body
    )
    
    if success:
        return jsonify({"status": "success", "message": "Your message was received!"})
    else:
        return jsonify({"status": "error", "message": "Failed to process submission."}), 500

if __name__ == "__main__":
    app.run(debug=True)

Frequently Asked Questions (FAQs)

Q: Can I send emails using Python completely for free?

Yes. Python’s smtplib is free and built-in. Gmail allows up to approximately 500 emails per day via SMTP at no cost. For higher volumes, email APIs like SendGrid and Mailgun offer free tiers (typically 100–300 emails per day).

Q: Do I need to install any external libraries to send emails in Python?

No installation is required for basic email automation. The smtplib, email, and ssl modules are all part of Python’s standard library. External libraries like python-dotenv or sendgrid are only needed for optional features.

Q: Why does Gmail keep rejecting my login (SMTPAuthenticationError)?

The most common cause since May 2022: you are using your regular Gmail password. Google now requires a Gmail App Password for SMTP access. Enable 2-Step Verification in your Google Account Security settings, then generate an App Password.

Q: What is the difference between port 465 and port 587?

Port 465 uses SSL and encrypts the connection immediately. Port 587 starts unencrypted and upgrades to TLS using STARTTLS. Both result in a secure, encrypted session. Port 587 + STARTTLS is the modern recommended standard.

Q: Can I send the same email to hundreds of people at once?

Yes, but with care. Use a loop and add a time.sleep(1) delay between sends to avoid triggering Gmail’s rate limiter. For true bulk sending (thousands of emails), an Email API like SendGrid or Mailgun is much better suited.

Q: Is it safe to store my App Password in a .env file?

Yes, as long as the .env file is in your .gitignore and never pushed to version control. On a production server, prefer system environment variables or a secrets management tool (AWS Secrets Manager, HashiCorp Vault) over .env files.

Q: What is the difference between smtplib and a service like SendGrid?

smtplib directly connects to an SMTP server — great for personal scripts and learning. SendGrid is a cloud email service accessed via HTTP API — better for production apps needing analytics, high deliverability, bounce management, and scale.

Q: Can I schedule Python emails to send automatically at specific times?

Absolutely. Use Python’s schedule library (pip install schedule) to run your email function at set intervals. For server environments, cron (Linux/Mac) or Task Scheduler (Windows) are more robust options.


Conclusion

You now have everything you need to send emails automatically using Python — from understanding the SMTP protocol and setting up secure Gmail credentials, to sending plain text emails, HTML messages, file attachments, and personalized bulk emails.

Here is a summary of the key principles to carry forward:

  • smtplib + ssl + email are your core tools — no installation needed
  • Always use Gmail App Passwords — regular passwords do not work via SMTP since 2022
  • Store credentials in environment variables — never hardcode sensitive data
  • Use port 587 (TLS) or port 465 (SSL) — both are secure, never use port 25
  • Wrap everything in try/except — and use logging for production-quality scripts
  • Graduate to an Email API (SendGrid, Mailgun, Amazon SES) when building production systems

The fastest way to cement these skills is to build one of the mini projects above. Start with the daily weather alert (Mini Project 1) — it uses almost everything covered in this guide and gives you something genuinely useful at the end.

Ready to take your Python automation further? Explore our Python Automation Scripts Guide for more real-world projects you can build and deploy today.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *