Messages and MIME
Multipart messages and attachments
Use MIME boundaries to carry alternative bodies, related resources, and file attachments.
A message with an attachment is really several messages glued together. MIME calls this a multipart body. Each part has its own little header section and its own content.
The glue is a boundary: a string declared in the outer Content-Type and repeated between parts. The parser splits on it.
The two multipart types you’ll use
multipart/mixed groups independent parts. The typical case is a text body plus one or more attachments.
multipart/alternative offers the same content in different forms. Nearly every marketing email is plain text followed by HTML, and the client picks the one it can show.
A message with one attachment
Here is a complete multipart body. I’m sending a short note with a PDF:
Content-Type: multipart/mixed; boundary="mail-boundary-42"
--mail-boundary-42
Content-Type: text/plain; charset=utf-8
The report is attached.
--mail-boundary-42
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="report.pdf"
JVBERi0xLjQK
--mail-boundary-42--
Each part starts with -- and the boundary. The last one adds two more hyphens at the end: --mail-boundary-42--. That closing line tells the parser there’s nothing more.
Content-Disposition: attachment asks the client to show this part as a downloadable file named report.pdf. It’s a hint about presentation, nothing more.
Nesting
Multipart bodies nest. A real message from a mail app often looks like this:
multipart/mixed
├── multipart/alternative
│ ├── text/plain
│ └── text/html
└── application/pdf (attachment)
Each nested multipart gets its own boundary string. My advice when you build one of these by hand: draw the tree first, then write the boundaries. It’s much harder the other way around.
Two rules for the boundary
The boundary must never appear inside any part. Libraries generate a long random string for this reason.
And the closing delimiter is not optional. Forget the trailing -- and some parsers treat the message as truncated and drop the last attachment. That’s one of those bugs where SMTP delivery reports success and the user still says “the file isn’t there”.
Keep the alternatives equal
Clients usually pick the last alternative they understand, which means HTML. But every version should say the same thing. If you put the important information only in the HTML part, people reading plain text lose it.
Filenames are hostile input
The filename parameter comes from the sender. Someone can send filename="../../.ssh/authorized_keys". If your code writes attachments to disk using that name, you’ve got a serious problem.
Strip path components, pick your own storage name, cap the decoded size, and scan the bytes. Never let a header choose a filesystem path.
Try extending the example with a nested plain-text and HTML alternative. Draw the tree, then write the boundaries.
Lesson completed