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
| Term | What It Means |
|---|---|
| SMTP Server | The mail server that processes and routes outgoing emails |
| Port 587 | Used for TLS/STARTTLS connections (modern recommended standard) |
| Port 465 | Used for direct SSL connections (encrypted from the first byte) |
| EHLO | Extended Hello — how Python introduces itself to the SMTP server |
| STARTTLS | A command that upgrades a plain connection to an encrypted one |
| Authentication | Verifying your identity with a username and password (or App Password) |
Common SMTP Server Addresses
| Email Provider | SMTP Server Address |
|---|---|
| Gmail | smtp.gmail.com |
| Outlook / Hotmail | smtp.office365.com |
| Yahoo Mail | smtp.mail.yahoo.com |
| Custom domain | Provided 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:
| Method | What 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.
| Submodule | Purpose |
|---|---|
email.mime.text.MIMEText | Creates plain text or HTML email bodies |
email.mime.multipart.MIMEMultipart | Combines multiple parts (body + attachments) |
email.mime.base.MIMEBase | Base class for encoding file attachments |
email.mime.application | For PDFs and application files |
email.encoders | Encodes binary attachment data (Base64) |
email.message.EmailMessage | Modern, 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
- Go to your Google Account at myaccount.google.com
- Click on Security in the left sidebar
- Enable 2-Step Verification if you have not already (this is required)
- In the search bar at the top of the Security page, type “App Passwords”
- Click the result and sign in again if prompted
- Under “App name”, type something descriptive like
Python Email Script - Click Create
- A 16-character password will appear — copy it immediately (you will not see it again)
- 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
| Code | Purpose |
|---|---|
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 server | Opens 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 — 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:
MIMEMultipart("mixed")— a container that holds both the email body and the attached fileMIMEBase— a generic wrapper for the binary file dataencoders.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:
| Feature | SSL (Port 465) | TLS / STARTTLS (Port 587) |
|---|---|---|
| Connection start | Encrypted from the very first byte | Starts plain, upgrades to encrypted |
| Python method | smtplib.SMTP_SSL() | smtplib.SMTP() + .starttls() |
| Security level | Both are secure for modern use | Considered the modern standard |
| Use case | Legacy systems, simpler code | Modern 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
| Factor | Raw SMTP (smtplib) | Email API (SendGrid, Mailgun, etc.) |
|---|---|---|
| Setup | Minimal — built into Python | Requires account signup and API key |
| Best for | Learning, scripts, personal use | Production apps, business systems |
| Deliverability | Depends on your IP/domain | High — professionally managed |
| Analytics | None | Opens, clicks, bounces, unsubscribes |
| Daily limit (free) | ~500 (Gmail) | Varies — typically 100–300/day free |
| Scalability | Limited | Handles millions of emails |
| Bounce handling | Manual | Automatic |
| Cost | Free | Free 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
| Exception | What It Means | How to Fix It |
|---|---|---|
SMTPAuthenticationError | Credentials rejected | Use Gmail App Password; check 2FA is enabled |
SMTPConnectError | Cannot reach the server | Check internet connection, port, firewall |
SMTPRecipientsRefused | Recipient address invalid | Validate email format before sending |
SMTPSenderRefused | Sender not authorized | Ensure “From” matches your login email |
ConnectionRefusedError | Port blocked or wrong | Try port 465 if 587 fails (or vice versa) |
ssl.SSLError | Certificate or TLS issue | Update Python; check ssl.create_default_context() |
TimeoutError | Connection timed out | Check 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
.envto.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
logginginstead ofprint()— 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:
- Get a free API key from OpenWeatherMap
- Use Python’s
requestslibrary to fetch weather data - Format it as a clean HTML email
- Schedule it with Python’s
schedulelibrary 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:
- Use Python’s
csvorpandaslibrary to read the data - Generate a summary (totals, averages, highest/lowest values)
- Format the summary as HTML
- Attach the original CSV file
- 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+emailare 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
loggingfor 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.
