MIME for developers: alternative, related and mixed
Most email bugs with images and attachments are MIME tree bugs. Learn how multipart/alternative, related and mixed nest, with a correct example.

On this page(9 sections)
- MIME in thirty seconds
- The three multipart types that matter
- multipart/alternative: same content, different formats
- multipart/related: one document with its resources
- multipart/mixed: independent parts in sequence
- The canonical tree for a rich message
- A minimal raw example
- The bugs this explains
- "My inline image shows up as an attachment"
- "The PDF is missing in some clients"
- "Some recipients see only the plain text"
- "Both versions show, one after the other"
- "The body shows as garbled text"
- Encoding rules worth remembering
- Inspecting a real message
- Two more types you will meet
- message/rfc822: a whole email inside an email
- multipart/report: delivery notifications
- Key takeaways
When an inline logo shows up as a mysterious attachment, or an invoice PDF disappears in one client but not another, the bug is almost always in the MIME tree. Understanding three multipart types, and how they nest, explains most of these problems.
MIME in thirty seconds
An email message is headers plus a body. MIME (defined in RFCs 2045 through 2049) lets that body contain multiple parts, each with its own headers and content type. A multipart body is divided by a boundary string declared in the Content-Type header:
Content-Type: multipart/alternative; boundary="alt-7f3a"
Each part begins with --alt-7f3a on its own line, and the whole multipart ends with --alt-7f3a--. Parts can themselves be multiparts, which is how a message becomes a tree.
The three multipart types that matter
multipart/alternative: same content, different formats
Each child is a complete representation of the same content. The client picks the best one it can display. Typical children are text/plain and text/html. RFC 2046 says alternatives go in order of increasing preference, so plain text first, HTML last.
Clients show one child, never all of them. If you put an attachment inside multipart/alternative, some clients will never show it, because they chose the HTML alternative instead.
multipart/related: one document with its resources
The first child is the root document, usually HTML. The remaining children are resources it references, such as inline images, each with a Content-ID header. The HTML refers to them with cid: URLs:
<img src="cid:[email protected]" alt="Example Inc." width="120" height="40">
Content-Type: image/png
Content-ID: <[email protected]>
Content-Disposition: inline; filename="logo.png"
Resources in multipart/related are part of the document, not separate files for the user.
multipart/mixed: independent parts in sequence
The children are separate things shown or offered one after another. This is where attachments live. The first child is usually the message body (which may itself be a multipart), followed by attachments with Content-Disposition: attachment.
The canonical tree for a rich message
A message with an HTML body, a plain-text alternative, an inline logo and a PDF attachment nests like this:
multipart/mixed
├── multipart/alternative
│ ├── text/plain
│ └── multipart/related
│ ├── text/html
│ └── image/png (Content-ID: [email protected], inline)
└── application/pdf (attachment; filename="invoice-A-10293.pdf")
Read it from the outside in:
- mixed at the top holds "the message" and "the attachment."
- alternative inside offers plain text or the rich version.
- related groups the HTML with the image it embeds, so only clients that choose HTML load the image.
Drop any layer you do not need. A message without attachments has no mixed layer. A message without inline images has no related layer. A plain-text-only message is just text/plain.
A minimal raw example
Here is the structure without attachments or images, which covers most transactional mail:
From: Example <[email protected]>
To: [email protected]
Subject: Your code is 482913
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="alt-7f3a"
--alt-7f3a
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
Your sign-in code is 482913. It expires in 10 minutes.
--alt-7f3a
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<p>Your sign-in code is <strong>482913</strong>. It expires in 10 minutes.</p>
--alt-7f3a--
The bugs this explains
"My inline image shows up as an attachment"
The image was placed in multipart/mixed instead of multipart/related, or it lacks a Content-ID that matches the cid: reference, or its disposition is attachment. Some clients also list inline images as attachments regardless; you cannot fully control that.
"The PDF is missing in some clients"
The attachment was placed inside multipart/alternative. A client that selected the HTML alternative ignores siblings. Move attachments to an outer multipart/mixed.
"Some recipients see only the plain text"
The HTML part came first in the alternative. Clients that follow the "last is best" rule may display the plain-text part as the preferred version. Put HTML last.
"Both versions show, one after the other"
The parts were put in multipart/mixed instead of multipart/alternative, so the client treats them as two separate things to display.
"The body shows as garbled text"
A part has the wrong Content-Transfer-Encoding declared, or declares base64 while containing raw text. Make the declared encoding match what you actually wrote.
Encoding rules worth remembering
| Header | Purpose | Common values |
|---|---|---|
| Content-Type | What the part is | text/plain; charset="UTF-8", text/html, image/png, application/pdf |
| Content-Transfer-Encoding | How bytes are represented for transport | 7bit, quoted-printable, base64, 8bit |
| Content-Disposition | How to present it | inline, attachment; filename="..." |
| Content-ID | Reference target for cid: |
<[email protected]> |
Non-ASCII text should be quoted-printable or base64 unless you know every hop supports 8BITMIME. Binary attachments are base64. Lines must stay under 998 characters, which encoding guarantees.
Boundaries must not appear anywhere in the content they delimit. Libraries generate random boundaries for this reason. Never build MIME by hand-concatenating strings in production; use your language's mail library, which handles boundaries, encoding and line endings.
Inspecting a real message
Most clients offer "Show original" or "View source." Look for the Content-Type lines and indent them mentally by nesting depth. Or parse it:
import email, sys
from email import policy
msg = email.message_from_binary_file(open(sys.argv[1], "rb"), policy=policy.default)
def walk(part, depth=0):
disp = part.get_content_disposition() or ""
print(" " * depth + f"{part.get_content_type()} {disp} {part.get_filename() or ''}")
if part.is_multipart():
for child in part.iter_parts():
walk(child, depth + 1)
walk(msg)
Run it against a message that misbehaves and the tree usually makes the bug obvious.
Two more types you will meet
message/rfc822: a whole email inside an email
When a client forwards a message "as attachment," or a bounce report includes the original message, the embedded message travels as a part with type message/rfc822. Its content is a complete email, with its own headers and possibly its own multipart tree. Treat it as a separate message when parsing: do not merge its headers with the outer message, and be careful when displaying it, since its From and Subject were written by someone else.
multipart/report: delivery notifications
Delivery status notifications, defined in RFC 3464, use multipart/report. The first part is a human-readable explanation, the second is a machine-readable message/delivery-status part with fields such as the final recipient, the action taken and the status code, and an optional third part contains the original message or its headers. If you process bounces yourself, parse the delivery-status part rather than scraping the human-readable text, which varies wildly between servers.
Key takeaways
alternativeoffers the same content in different formats, and clients show only one child.relatedbinds an HTML document to inline resources referenced bycid:.mixedholds independent parts, which is where attachments belong.- The standard nesting is mixed, then alternative, then related; omit layers you do not need.
- Put plain text before HTML in alternatives, and keep attachments out of alternatives.
Start with Koltrix
Your domain, one inbox, and an API that sends.
A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.


