Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read

Sending Email and Alerts from Scripts with Python smtplib (AI-Drafted, Human-Hardened)

Scripts still need to email reports and alerts. Use AI to draft smtplib senders, then verify TLS, escape user content, and keep SMTP credentials out of code.

  • #bash
  • #python
  • #email
  • #smtp
  • #automation

Slack and PagerDuty get all the attention, but a startling amount of ops still runs on email. The nightly backup report, the certificate-expiry warning, the “your build is red” nudge — half of these are a cron job that needs to send a message and exit. Python’s smtplib and email modules do this in the standard library with no dependencies, which is exactly why they keep showing up on locked-down boxes. The code is boilerplate-heavy and easy to get subtly wrong (TLS, encoding, headers), so it’s a good thing to have an AI draft and a human harden.

I treat the assistant as the fast junior who knows the SMTP dance. I own the parts that, done wrong, leak credentials or send mangled mail to a hundred people.

The modern way: EmailMessage, not string-stuffing

Old smtplib tutorials build the message by concatenating header strings, which breaks the moment a subject has a non-ASCII character or someone slips a newline into a field. The modern email.message.EmailMessage API handles encoding and headers for you.

import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["Subject"] = "Nightly backup report"
msg["From"] = "ops-bot@example.com"
msg["To"] = "team@example.com"
msg.set_content("All 14 volumes backed up successfully.")

with smtplib.SMTP("smtp.example.com", 587) as smtp:
    smtp.starttls()
    smtp.login("ops-bot@example.com", os.environ["SMTP_PASSWORD"])
    smtp.send_message(msg)

When an AI drafts email code, the first thing to check is whether it uses EmailMessage or hand-builds header strings — the latter is a 2010-era pattern that will eventually corrupt a message. Steer the prompt toward the modern API explicitly.

STARTTLS is not optional

The line that matters most for security is smtp.starttls(). Without it, your SMTP credentials and the message travel in plaintext across the network. An AI draft sometimes omits it, especially if you asked for a “simple” version, and “simple” plus “plaintext auth” is how a password ends up on the wire.

with smtplib.SMTP("smtp.example.com", 587) as smtp:
    smtp.starttls()  # upgrade to TLS before login — never skip
    smtp.login(user, password)

For implicit-TLS servers (port 465) you’d use smtplib.SMTP_SSL instead, which is encrypted from the first byte. Either way, verify the generated code encrypts before it authenticates. This is a hard review gate, not a nice-to-have.

Pro Tip: Test against a local capture server before you point a script at a real mailbox. python -m aiosmtpd -n -l localhost:8025 runs a debug SMTP server that prints messages instead of sending them. Point your script at localhost:8025, confirm the message looks right, and you’ll never accidentally blast a test alert to a real distribution list.

HTML reports without the injection hole

A formatted report often wants HTML, and EmailMessage supports a multipart alternative cleanly:

msg.set_content("Plain-text fallback: 3 services degraded.")
msg.add_alternative(
    f"<h2>Status</h2><p>{html.escape(summary)}</p>",
    subtype="html",
)

The critical detail is html.escape(summary). If summary contains anything derived from user input, a log line, or an external API — and in ops it usually does — unescaped content can inject markup or, worse, break rendering in subtle ways. When an AI generates HTML email, check that every interpolated value is escaped. The model frequently forgets, because in a happy-path example the content is always benign.

Header injection: the bug nobody mentions

If a script builds the To or Subject from input that could contain a newline, an attacker can inject extra headers — adding BCC recipients, for instance. The good news: EmailMessage rejects newlines in header values, which is another reason to use it over string concatenation. But if an AI draft ever does msg["Subject"] = "Alert: " + user_text where user_text is untrusted, validate that user_text is single-line first. Defense in depth: use the safe API and sanitize the inputs.

Don’t get rate-limited into a corner

A loop that sends one email per recipient will trip spam throttles and may get your sending IP blocked. Ask the AI for the batch-friendly pattern: one connection, many messages, with a small delay, or a single message with the recipients in Bcc.

with smtplib.SMTP("smtp.example.com", 587) as smtp:
    smtp.starttls()
    smtp.login(user, password)
    for msg in messages:
        smtp.send_message(msg)
        time.sleep(0.5)  # be polite to the relay

Reusing one connection across the batch is both faster and far friendlier to the relay than reconnecting each time. When a generated script opens a fresh SMTP() inside the loop, that’s the cue to restructure.

The credentials rule is absolute

The SMTP password is a credential, and it lives in an environment variable or a secrets manager — never in the script, never in a committed config, never in the AI prompt. When you paste email code to an assistant for help, the username, password, and internal mail-relay hostname all get scrubbed to placeholders first. The model writes identical code whether you give it os.environ["SMTP_PASSWORD"] or a real password, so there is zero reason to ever show it the real one. A leaked SMTP credential lets someone send mail as you, which is a phishing engine handed to a stranger.

Where this fits

Email senders are the quiet backbone of a lot of the scheduled automation in the Bash and Python automation category — the cron jobs that report and warn. For richer alerting you’d graduate to a proper channel feeding your monitoring alerts pipeline, but email remains the reliable fallback that works everywhere. I draft the senders with Claude or ChatGPT, test against a local debug server, and keep the vetted templates in a prompt workspace. The reusable prompts are in our prompt library and the prompt packs.

The rule

Email-sending code looks trivial and hides three real hazards: plaintext credentials, content injection, and a password that wants to leak. So let the AI draft the smtplib boilerplate fast, but you enforce STARTTLS before login, html.escape on every interpolated value, and SMTP credentials that never touch source or a prompt. Quick junior writes the sender; the human makes sure it’s encrypted, escaped, and credential-clean before it mails anyone real.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.