Python Central HubSign in

Automation and Scripting / Phase 4: Communication Automation

Sending HTML Emails and Attachments

Build MIME emails with HTML bodies and attachments.

diagram the shape of an HTML email with an attachment mermaid
An email that has a plain-text fallback, an HTML body and a file is three nested containers, not one message with extras bolted on. multipart/alternative holds the two versions of the same content and lets the client choose; multipart/mixed wraps that together with the attachment. Building it in the wrong order is why an attachment sometimes replaces the body.

HTML email + attachment

Python
import os
import smtplib
from email.message import EmailMessage
from pathlib import Path
 
 
def send_html_with_attachment(to: str, subject: str, html: str, attachment: Path):
    user = os.environ["SMTP_USER"]
    password = os.environ["SMTP_PASS"]
 
    msg = EmailMessage()
    msg["From"] = user
    msg["To"] = to
    msg["Subject"] = subject
 
    msg.set_content("Your email client does not support HTML.")
    msg.add_alternative(html, subtype="html")
 
    data = attachment.read_bytes()
    msg.add_attachment(
        data,
        maintype="application",
        subtype="octet-stream",
        filename=attachment.name,
    )
 
    with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
        smtp.starttls()
        smtp.login(user, password)
        smtp.send_message(msg)
 
 
# send_html_with_attachment("to@example.com", "Report", "<h1>Hi</h1>", Path("report.pdf"))

Building the message step by step

The nesting diagram above corresponds to three calls, and the order you make them in matters.

  1. set_content(text) creates the plain-text body. This is the fallback.
  2. add_alternative(html, subtype="html") adds the HTML version next to it. The message becomes multipart/alternative.
  3. add_attachment(...) wraps everything in multipart/mixed and adds the file.

If you call add_attachment before add_alternative, the tree is built differently and some clients show the attachment instead of the body. Follow the order in the example.

Choosing the attachment type

The example sends every file as application/octet-stream, which works but tells the recipient's client nothing about the file. Guess the type from the file name and fall back to octet-stream:

Python
import mimetypes
from pathlib import Path
 
 
def mime_for(path: Path) -> tuple[str, str]:
    kind, _ = mimetypes.guess_type(path.name)
    if kind is None:
        return "application", "octet-stream"
    maintype, subtype = kind.split("/", 1)
    return maintype, subtype
 
 
print(mime_for(Path("report.pdf")))   # application, pdf
print(mime_for(Path("photo.png")))    # image, png
print(mime_for(Path("notes.xyz")))    # application, octet-stream

Then call msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=path.name).

Writing the HTML

Email clients render HTML much less well than browsers. Outlook uses a Word-based engine. So keep the HTML simple.

  • Use inline style attributes. Many clients ignore <style> blocks and external stylesheets.
  • Use tables for layout, not flexbox or grid.
  • Avoid JavaScript, forms, and web fonts. They are stripped or blocked.
  • Put real text in the plain-text part, ideally the same content, not a note saying "your client does not support HTML". Screen readers and spam filters read it.
  • Escape any user data you insert into the HTML with html.escape, or a name like <b> will change the layout.

To show an image inside the message, either link to a hosted image (many clients block remote images until the reader allows them) or embed it with add_related on the HTML part and refer to it with a cid: URL.

Size and safety

Most providers reject messages over about 25 MB, and encoding adds roughly a third. Link to large files instead. Attachments are how malware spreads, so do not attach executables or scripts, and expect some scripts and archives to be blocked by the recipient's server. Read the credentials from environment variables, as in the previous lesson, and test by sending to yourself.

๐Ÿงช Try It Yourself

Exercise 1 โ€“ Inspect the Message Structure

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Check your understanding

quizCheck yourself
  1. Which call order builds a correct HTML email with a plain-text fallback and an attachment?

    Show answer

    B โ€” set_content, then add_alternative, then add_attachment โ€” The plain-text part must exist first, the HTML alternative comes next, and the attachment wraps them. A different order can hide the body.

  2. Why put real content in the plain-text part instead of a note saying HTML is not supported?

    Show answer

    A โ€” Screen readers, text-only clients and spam filters read it โ€” The plain-text part is what some readers and filters actually use, so it should carry the same information.

  3. What should you do with user-supplied text that you insert into the HTML body?

    Show answer

    B โ€” Escape it, for example with html.escape โ€” Unescaped text such as <b> or & changes the markup. Escaping keeps the layout intact and prevents injected HTML.

Was this page helpful?

Let us know how we did