Introduction
Email is the API everything already has. Reports, alerts, reminders, weekly digests — if it can be emailed, it can be automated, and Python’s built-in smtplib does it in a few dozen lines. This tutorial builds a reusable mailer that sends plain text, styled HTML, and file attachments through Gmail (or any SMTP provider), with credentials handled the secure way.
This is the delivery half of your automation stack: pair it with the web scraper to mail yourself scraped data, or the Excel report generator to ship formatted reports on a schedule.
Features
- Plain and HTML emails — styled bodies with fallback text.
- Attachments — any file type, MIME-encoded correctly.
- Multiple recipients — To, CC, and BCC support.
- Secure auth — app passwords, never your real password.
- Reusable function — one
send_email()call from any script.
Prerequisites
- Python 3.8+ —
smtplibandemailare built in. - A Gmail App Password — Google Account → Security → 2-Step Verification → App passwords. (Regular passwords no longer work with Gmail SMTP.)
Step 1: Create the Script
Save as mailer.py:
import smtplib
import os
from email.message import EmailMessage
from email.utils import formataddr
SMTP_HOST = "smtp.gmail.com"
SMTP_PORT = 587
def send_email(to, subject, body, html=None, attachments=None,
cc=None, bcc=None, sender_name="Python Automation"):
user = os.environ["MAIL_USER"]
password = os.environ["MAIL_PASSWORD"]
msg = EmailMessage()
msg["From"] = formataddr((sender_name, user))
msg["To"] = ", ".join(to)
msg["Subject"] = subject
if cc:
msg["Cc"] = ", ".join(cc)
msg.set_content(body)
if html:
msg.add_alternative(html, subtype="html")
for path in attachments or []:
with open(path, "rb") as f:
data = f.read()
msg.add_attachment(
data, maintype="application", subtype="octet-stream",
filename=os.path.basename(path),
)
recipients = to + (cc or []) + (bcc or [])
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(user, password)
server.send_message(msg, to_addrs=recipients)
print(f"Sent '{subject}' to {len(recipients)} recipient(s)")
if __name__ == "__main__":
send_email(
to=["[email protected]"],
subject="Automated report",
body="Hi!\n\nToday's report is attached.\n\n- The robot",
html="""<h2 style='color:#0F766E'>Daily Report</h2>
<p>Everything ran <b>successfully</b>.</p>""",
attachments=["report.xlsx"],
)
Step 2: Configure and Run
export MAIL_USER="[email protected]"
export MAIL_PASSWORD="your-16-char-app-password"
python mailer.py
The email arrives with styled HTML, a plain-text fallback, and the attachment.
How It Works
SMTP is a conversation: connect to the server on port 587, upgrade to TLS with starttls(), log in, hand over the message. smtplib handles the protocol; your job is building the message correctly — which is what the email.message.EmailMessage class is for.
EmailMessage manages MIME so you don’t have to. set_content sets the plain text; add_alternative(html) attaches the styled version — mail clients that render HTML show it, everything else falls back to plain text. Attachments are base64-encoded automatically with correct headers; before EmailMessage existed, this was thirty lines of manual MIME.
The credentials discipline is the security lesson: the app password comes from environment variables, never from code. App passwords (not your real password) are revocable per-app, so a leaked script doesn’t compromise your account — the same principle as the file encryption tool’s key handling: secrets never live in source.
BCC works through the to_addrs argument, not the headers: BCC’d recipients receive the mail but don’t appear in any header — that separation is exactly what the parameter provides.
Common Errors & Fixes
SMTPAuthenticationErrorwith a correct password — Gmail requires an app password with 2FA enabled; regular passwords are rejected outright.- Emails land in spam — automated mail from personal accounts often does; keep volume low, avoid ALL-CAPS subjects, and for real volume use a transactional service (Resend, SendGrid) with proper SPF/DKIM.
SMTPServerDisconnected— the connection dropped mid-send; port 587 +starttls()is the correct combo, port 465 needsSMTP_SSLinstead.- Attachment arrives corrupt — you opened the file in text mode; always
'rb'for binary data.
Key Concepts
- SMTP flow — connect, TLS, login, send.
- MIME alternatives — plain text plus HTML in one message.
- Environment secrets — credentials via env vars, revocable app passwords.
to_addrsvs headers — envelope recipients control BCC invisibility.
What to Try Next
- Schedule it with cron on your home server for a weekly digest.
- Mail yourself scraped price drops from the web scraper.
- Add inline images with
add_alternative+ CID attachments for branded newsletters. - Build a mail-merge script — one template, a CSV of recipients, personalized sends with a delay.
FAQ
Can I send from a non-Gmail address?
Yes — change SMTP_HOST/SMTP_PORT to your provider’s (Outlook: smtp.office365.com:587; Yahoo: smtp.mail.yahoo.com:587). The rest is identical.
How many emails can I send per day?
Gmail allows ~500/day for personal accounts. For anything beyond notifications and digests, use a transactional email service.
Is starttls() secure enough?
Yes — it upgrades the connection to TLS before credentials are sent. The riskier pattern is port 25 without TLS, which you should never use with authentication.