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
send_email() call from any script.Prerequisites
smtplib and email are built in.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=["friend@example.com"],
subject="Automated report",
body="Hi!\n\nToday's report is attached.\n\n- The robot",
html="""<h2 style='color:#0F766E'>Daily Report</h2>
<p>Everything ran <b>successfully</b>.</p>""",
attachments=["report.xlsx"],
)Step 2: Configure and Run
export MAIL_USER="you@gmail.com"
export MAIL_PASSWORD="your-16-char-app-password"
python mailer.pyThe 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
starttls() is the correct combo, port 465 needs SMTP_SSL instead.'rb' for binary data.Key Concepts
What to Try Next
add_alternative + CID attachments for branded newsletters.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.