Loading content...
When you send an email with a styled body, three attached PDFs, and an inline company logo, what actually travels through the Internet? Not four separate transmissions—just one message. That single RFC 822 message somehow contains HTML content, references to images displayed inline, and multiple binary attachments, all correctly identified and extractable by the recipient's email client.
This magic is multipart message construction—one of MIME's most powerful capabilities. Understanding multipart construction is essential for anyone working with email systems, file uploads, API design, or any protocol that needs to transmit structured compound content.
Multipart messages are deceptively simple in concept but require careful attention to detail in implementation. A missed boundary, an improper line ending, or an incorrectly computed Content-Length can corrupt an entire message. This page provides the complete technical foundation for constructing, parsing, and debugging multipart content.
By the end of this page, you will understand the boundary protocol and delimiter syntax, how to construct multipart messages from scratch, parsing strategies for extracting individual parts, handling nested multipart structures, and the critical differences between multipart subtypes (mixed, alternative, related, form-data). You'll also learn common pitfalls and debugging techniques.
The fundamental challenge of multipart messages is delimitation: how do you mark where one part ends and another begins, when the parts themselves might contain any arbitrary byte sequence?
MIME solves this with the boundary protocol—a carefully designed mechanism using unique delimiter strings that cannot be confused with content.
Boundary String Requirements
A boundary string must satisfy several constraints:
The allowed characters are: alphanumerics, plus some special characters like '()+_,-./:=? and space (except at the end).
123456789101112131415161718
# Valid boundary stringsboundary="simple"boundary="----=_Part_123_456.789"boundary="----WebKitFormBoundary7MA4YWxkTrZu0gW"boundary="=_NextPart_000_0000_01D8A9B0.27C9F3E0"boundary="------------boundary123" # In the message body, boundaries appear with -- prefix:--simple <- Part delimiter--simple-- <- Final delimiter (end marker) # Full boundary line includes CRLF:
--simple
<- Between parts
--simple--
<- After last partDelimiter Syntax
Boundaries appear in the message body with specific syntax:
| Delimiter Type | Syntax | Purpose |
|---|---|---|
| Start/Encapsulation | CRLF--boundary | Precedes each part (including first) |
| Close | CRLF--boundary-- | Marks end of all parts |
| Preamble | Text before first boundary | Optional, ignored by parsers |
| Epilogue | Text after close boundary | Optional, ignored by parsers |
The -- prefix is always required before the boundary string. The -- suffix is added only for the close delimiter.
Why CRLF Matters
Boundary lines must use CRLF (Carriage Return + Line Feed, \r ) rather than just LF ( ). This is one of the most common implementation bugs:
# WRONG - LF only (Unix style)
--boundary
# CORRECT - CRLF (Internet standard)
\r
--boundary\r
Most parsing libraries handle this gracefully, but strict implementations will reject messages with LF-only delimiters.
If a boundary string appears within content, the parser will incorrectly split the message. Malicious content could exploit this to inject parts or corrupt messages. Always generate boundaries using cryptographically random data or UUIDs, never predictable patterns like 'boundary1'. Modern libraries generate boundaries like '----=NextPart' followed by random hex or UUID.
Building a multipart message requires careful attention to structure. Let's construct a complete example step by step.
Step 1: Generate a Unique Boundary
Never hardcode boundaries. Generate them dynamically:
// Good: Random boundary generation
const boundary = '----=_Part_' + crypto.randomUUID();
// Result: "----=_Part_550e8400-e29b-41d4-a716-446655440000"
// Also good: Random hex string
const boundary = '----=_NextPart_' + crypto.randomBytes(16).toString('hex');
// Result: "----=_NextPart_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
Step 2: Set the Content-Type Header
The top-level Content-Type declares the multipart type and boundary:
Content-Type: multipart/mixed; boundary="----=_Part_550e8400"
The boundary value should be quoted if it contains special characters (quoting is always safe).
1234567891011121314151617181920212223242526272829303132333435363738394041
From: sender@example.comTo: recipient@example.comSubject: Document SubmissionMIME-Version: 1.0Content-Type: multipart/mixed; boundary="----=_Part_001" This is a multipart message in MIME format.(This preamble is optional and ignored by MIME-aware clients) ------=_Part_001Content-Type: text/plain; charset=utf-8Content-Transfer-Encoding: 7bit Dear Colleague, Please find the attached documents for your review. Best regards,Sender ------=_Part_001Content-Type: application/pdf; name="report.pdf"Content-Transfer-Encoding: base64Content-Disposition: attachment; filename="report.pdf" JVBERi0xLjcKJeLjz9MNCjEgMCBvYmoKPDwKL1R5cGUgL0NhdGFsb2cKL1BhZ2VzIDIgMCBSCj4+CmVuZG9iago1IDAgb2JqCjw8Ci9MZW5ndGggNDQKL0ZpbHRlciAvRmxhdGVEZWNvZGUKPj4Kc3RyZWFtCnicMzcw...(Base64 encoded PDF content continues...) ------=_Part_001Content-Type: image/png; name="signature.png"Content-Transfer-Encoding: base64Content-Disposition: attachment; filename="signature.png" iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg== ------=_Part_001-- (This epilogue after the close delimiter is also optional and ignored)Step 3: Assemble Each Part
Each part consists of:
CRLF--boundary (or just --boundary for first part after preamble)Critical Details:
-- after the boundary string123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
// TypeScript: Constructing a multipart message programmatically interface Part { contentType: string; headers?: Record<string, string>; body: string | Buffer; encoding?: 'base64' | '7bit' | 'quoted-printable';} function buildMultipartMessage(parts: Part[]): { contentType: string; body: string } { // Generate unique boundary const boundary = '----=_Part_' + crypto.randomUUID(); const CRLF = '\r'; let body = ''; // Optional preamble (ignored by parsers but helps legacy clients) body += 'This is a multi-part message in MIME format.' + CRLF; for (const part of parts) { // Boundary line body += CRLF + '--' + boundary + CRLF; // Part headers body += 'Content-Type: ' + part.contentType + CRLF; if (part.encoding) { body += 'Content-Transfer-Encoding: ' + part.encoding + CRLF; } // Additional headers if (part.headers) { for (const [key, value] of Object.entries(part.headers)) { body += key + ': ' + value + CRLF; } } // Blank line separating headers from body body += CRLF; // Part body if (Buffer.isBuffer(part.body)) { body += part.body.toString('base64'); } else { body += part.body; } } // Close delimiter body += CRLF + '--' + boundary + '--' + CRLF; return { contentType: `multipart/mixed; boundary="${boundary}"`, body };} // Usage exampleconst message = buildMultipartMessage([ { contentType: 'text/plain; charset=utf-8', body: 'Hello, please see the attachment.', encoding: '7bit' }, { contentType: 'application/pdf; name="report.pdf"', headers: { 'Content-Disposition': 'attachment; filename="report.pdf"' }, body: pdfBuffer, // Buffer containing PDF bytes encoding: 'base64' }]); console.log('Content-Type:', message.contentType);console.log('Body:', message.body);While all multipart types share the boundary mechanism, their semantics differ significantly. Choosing the correct subtype is essential for proper rendering.
multipart/mixed: Independent Parts
The default for heterogeneous content. Parts are independent and displayed sequentially. Use cases:
Clients display/offer each part in order because there's no semantic relationship between parts beyond their sequence.
multipart/alternative: Same Content, Different Formats
Used when the same semantic content is provided in multiple formats. Parts are ordered from simplest to richest—clients select the best format they support:
multipart/alternative
├── text/plain (simplest - for legacy clients)
├── text/html (richer - for modern clients)
└── application/pdf (richest - if client supports inline PDF)
The recipient should see only ONE representation—not all of them.
123456789101112131415161718192021222324252627282930313233
Content-Type: multipart/alternative; boundary="alt_boundary" --alt_boundaryContent-Type: text/plain; charset=utf-8 Welcome to our newsletter! Visit our website for more: https://example.com --alt_boundaryContent-Type: text/html; charset=utf-8 <!DOCTYPE html><html><head> <style> body { font-family: Arial, sans-serif; color: #333; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; } .cta { background: #667eea; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; } </style></head><body> <div class="header"> <h1>Welcome to our newsletter!</h1> </div> <p>We're excited to share our latest updates with you.</p> <a href="https://example.com" class="cta">Visit our Website</a></body></html> --alt_boundary--multipart/related: Aggregated Components
Used when parts form a composite whole, with a "root" part that references other parts. The classic use case is HTML email with inline images:
multipart/related; type="text/html"
├── text/html (root) - references images via Content-ID
├── image/png (Content-ID: <logo@example.com>)
└── image/jpeg (Content-ID: <banner@example.com>)
The HTML part uses cid: URLs to reference other parts:
<img src="cid:logo@example.com" alt="Logo">
The type parameter identifies which part is the root (starting point for rendering).
12345678910111213141516171819202122232425262728293031323334353637383940
Content-Type: multipart/related; boundary="related_boundary"; type="text/html" --related_boundaryContent-Type: text/html; charset=utf-8 <!DOCTYPE html><html><body> <div style="text-align: center;"> <img src="cid:company-logo@example.com" alt="Company Logo" style="max-width: 200px;"> <h1>Monthly Report</h1> </div> <p>Our performance this month has been exceptional.</p> <img src="cid:chart-image@example.com" alt="Performance Chart" style="max-width: 100%;"></body></html> --related_boundaryContent-Type: image/pngContent-ID: <company-logo@example.com>Content-Transfer-Encoding: base64Content-Disposition: inline; filename="logo.png" iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAA...(Base64 encoded logo image) --related_boundaryContent-Type: image/jpegContent-ID: <chart-image@example.com>Content-Transfer-Encoding: base64Content-Disposition: inline; filename="chart.jpg" /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUE...(Base64 encoded chart image) --related_boundary--Content-ID values follow email address syntax: unique-id@domain. The angle brackets are part of the header value. When referenced in cid: URLs, omit the brackets: src="cid:image@example.com" references Content-ID: image@example.com.
multipart/form-data: Web Form Uploads
Specifically designed for HTML form submissions that include file uploads. Each form field becomes a part:
multipart/form-data
├── name="username" (text field)
├── name="email" (text field)
├── name="avatar"; filename="photo.jpg" (file field)
└── name="documents[]"; filename="doc1.pdf" (array file field)
Unlike other multipart types, form-data uses Content-Disposition to identify fields by name rather than Content-Type to describe semantics.
1234567891011121314151617181920212223242526272829
POST /api/register HTTP/1.1Host: api.example.comContent-Type: multipart/form-data; boundary="----WebKitFormBoundary7MA4"Content-Length: 12847 ------WebKitFormBoundary7MA4Content-Disposition: form-data; name="username" tanaka_yuki------WebKitFormBoundary7MA4Content-Disposition: form-data; name="email" tanaka@example.com------WebKitFormBoundary7MA4Content-Disposition: form-data; name="bio" Software engineer with 10+ years experience in distributed systems.Passionate about performance optimization and clean architecture.------WebKitFormBoundary7MA4Content-Disposition: form-data; name="avatar"; filename="profile.jpg"Content-Type: image/jpeg [Binary JPEG data - the actual file content]------WebKitFormBoundary7MA4Content-Disposition: form-data; name="resume"; filename="resume.pdf"Content-Type: application/pdf [Binary PDF data - the actual file content]------WebKitFormBoundary7MA4--| Subtype | Purpose | Part Order | Client Behavior |
|---|---|---|---|
| mixed | Independent parts | Display order | Show/offer all parts sequentially |
| alternative | Same content, different formats | Simplest to richest | Display only the best supported format |
| related | Components of composite whole | Root first (usually) | Render root, resolve cid: references |
| form-data | Web form submission | Field order | Parse as form name-value pairs |
| digest | Collection of messages | Any order | Display as message list |
| parallel | Simultaneous display | Any order | Display/play all parts together |
Real-world email often requires nesting multipart types to achieve complex structures. The most common pattern is an HTML email with inline images AND separate attachments.
The Challenge
Consider an email that needs:
No single multipart type handles all these requirements:
multipart/mixed → Attachments, but no alternative formatsmultipart/alternative → Text/HTML versions, but no inline imagesmultipart/related → Inline images, but no separate attachmentsThe Solution: Nested Structure
Combine types through nesting:
multipart/mixed
├── multipart/alternative
│ ├── text/plain (fallback)
│ └── multipart/related
│ ├── text/html (with cid: references)
│ └── image/png (the logo)
├── application/pdf (attachment 1)
└── application/pdf (attachment 2)
The outer multipart/mixed wraps everything. First part is multipart/alternative for format selection. The HTML alternative itself is multipart/related to include inline images.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
From: marketing@example.comTo: customer@example.comSubject: Your Monthly StatementMIME-Version: 1.0Content-Type: multipart/mixed; boundary="==MIXED_BOUNDARY==" --==MIXED_BOUNDARY==Content-Type: multipart/alternative; boundary="==ALT_BOUNDARY==" --==ALT_BOUNDARY==Content-Type: text/plain; charset=utf-8 Your Monthly Statement - January 2024 Account: ****4532Balance: $1,234.56 View full statement in attached PDF. Questions? Contact support@example.com --==ALT_BOUNDARY==Content-Type: multipart/related; boundary="==RELATED_BOUNDARY=="; type="text/html" --==RELATED_BOUNDARY==Content-Type: text/html; charset=utf-8 <!DOCTYPE html><html><head> <style> .header { background: #1a365d; padding: 20px; text-align: center; } .logo { max-width: 150px; } .content { padding: 20px; font-family: Arial, sans-serif; } .balance { font-size: 24px; color: #1a365d; font-weight: bold; } </style></head><body> <div class="header"> <img src="cid:company-logo@statements.example.com" alt="Example Bank" class="logo"> </div> <div class="content"> <h1>Your Monthly Statement</h1> <p><strong>Account:</strong> ****4532</p> <p class="balance">Current Balance: $1,234.56</p> <p>Full statement attached as PDF.</p> </div></body></html> --==RELATED_BOUNDARY==Content-Type: image/pngContent-ID: <company-logo@statements.example.com>Content-Transfer-Encoding: base64Content-Disposition: inline; filename="logo.png" iVBORw0KGgoAAAANSUhEUgAAASwAAAB...[Base64 encoded logo PNG] --==RELATED_BOUNDARY==-- --==ALT_BOUNDARY==-- --==MIXED_BOUNDARY==Content-Type: application/pdf; name="statement-january-2024.pdf"Content-Transfer-Encoding: base64Content-Disposition: attachment; filename="statement-january-2024.pdf" JVBERi0xLjcKJeLjz9MNCjEgMCBvYmoK...[Base64 encoded PDF statement] --==MIXED_BOUNDARY==Content-Type: application/pdf; name="terms-update.pdf"Content-Transfer-Encoding: base64Content-Disposition: attachment; filename="terms-update.pdf" JVBERi0xLjcKJeLjz9MNCJ5FTA0K...[Base64 encoded terms PDF] --==MIXED_BOUNDARY==--When nesting multipart types, each level MUST use a different boundary string. If the same boundary appeared at multiple levels, parsers couldn't determine which level a delimiter belongs to. Use distinct, random boundaries for each nesting level.
Parsing Nested Structures
Parsing nested multipart messages requires recursive processing:
Implementation Considerations
Implementing a robust multipart parser requires handling edge cases and malformed input gracefully. Let's examine the parsing algorithm and common pitfalls.
The Parsing Algorithm
1. Extract boundary from Content-Type header
2. Find first delimiter: "--" + boundary
3. Discard preamble (everything before first delimiter)
4. Loop:
a. Read part headers until blank line
b. Read part body until next delimiter
c. If delimiter ends with "--", stop
d. Else, continue to next part
5. Discard epilogue (everything after close delimiter)
Critical Parsing Details
Boundary Matching: Match exactly --boundary at start of line. The boundary itself might contain regex special characters—escape or use literal matching.
Line Endings: Accept both CRLF and LF for robustness, but generate CRLF for compliance.
Whitespace: Some senders add trailing whitespace after boundary. Trim lines before matching.
Empty Parts: Parts can be empty (headers only, no body). Don't assume body exists.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
// TypeScript: Robust multipart message parser interface ParsedPart { headers: Map<string, string>; contentType: string; body: Buffer | string; parts?: ParsedPart[]; // For nested multipart} interface ParseResult { contentType: string; boundary: string; parts: ParsedPart[];} function parseMultipart(body: Buffer | string, contentType: string): ParseResult { // Extract boundary from Content-Type const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;\s]+))/i); if (!boundaryMatch) { throw new Error('No boundary found in Content-Type'); } const boundary = boundaryMatch[1] || boundaryMatch[2]; // Convert to string for parsing (assuming text-compatible encoding) const bodyStr = Buffer.isBuffer(body) ? body.toString('binary') : body; // Create delimiter patterns const delimiter = '--' + boundary; const closeDelimiter = delimiter + '--'; // Split on delimiters const parts: ParsedPart[] = []; // Find first delimiter (skip preamble) let startIdx = bodyStr.indexOf(delimiter); if (startIdx === -1) { throw new Error('No boundary delimiter found in body'); } // Move past first delimiter and its CRLF startIdx = bodyStr.indexOf('', startIdx) + 1; while (startIdx < bodyStr.length) { // Find next delimiter let endIdx = bodyStr.indexOf('' + delimiter, startIdx); if (endIdx === -1) { endIdx = bodyStr.length; } // Extract part content const partContent = bodyStr.substring(startIdx, endIdx); // Parse part headers and body const headerEndIdx = partContent.indexOf(' '); if (headerEndIdx === -1) { // Malformed part - no blank line break; } const headerSection = partContent.substring(0, headerEndIdx); const bodySection = partContent.substring(headerEndIdx + 2); // Parse headers const headers = new Map<string, string>(); for (const line of headerSection.split(/\r?/)) { const colonIdx = line.indexOf(':'); if (colonIdx > 0) { const name = line.substring(0, colonIdx).trim().toLowerCase(); const value = line.substring(colonIdx + 1).trim(); headers.set(name, value); } } const partContentType = headers.get('content-type') || 'text/plain'; // Check for nested multipart let parsedPart: ParsedPart; if (partContentType.toLowerCase().startsWith('multipart/')) { // Recursively parse nested multipart const nested = parseMultipart(bodySection, partContentType); parsedPart = { headers, contentType: partContentType, body: bodySection, parts: nested.parts }; } else { parsedPart = { headers, contentType: partContentType, body: bodySection.replace(/\r?$/, '') // Trim trailing newline }; } parts.push(parsedPart); // Check for close delimiter const nextDelimiterIdx = bodyStr.indexOf(delimiter, endIdx); if (nextDelimiterIdx === -1) break; const potentialClose = bodyStr.substring(nextDelimiterIdx, nextDelimiterIdx + closeDelimiter.length); if (potentialClose === closeDelimiter) { break; // End of all parts } // Move to next part startIdx = bodyStr.indexOf('', nextDelimiterIdx) + 1; } return { contentType, boundary, parts };} // Usageconst result = parseMultipart(rawMessageBody, 'multipart/mixed; boundary="----=_Part_001"');console.log(`Parsed ${result.parts.length} parts`);for (const part of result.parts) { console.log(`Part: ${part.contentType}`);}Parsers must defend against: (1) Infinite nesting depth - limit recursion, (2) Huge parts - stream instead of buffering, (3) Missing close delimiters - timeout or limit reads, (4) Malformed boundaries - sanitize or reject. Always set reasonable limits and handle exceptions gracefully.
The Content-Disposition header tells recipients how to handle content—whether to display it inline or offer it for download, and what filename to suggest. This header is crucial for attachment handling and security.
Disposition Types
# Inline - display within message
Content-Disposition: inline
# Attachment - prompt download
Content-Disposition: attachment; filename="report.pdf"
The filename Parameter
The filename parameter suggests a name for saving the content:
Content-Disposition: attachment; filename="annual-report-2024.pdf"
Encoding Non-ASCII Filenames
For international filenames, RFC 5987 defines extended parameter syntax:
# Basic ASCII filename
Content-Disposition: attachment; filename="report.pdf"
# UTF-8 filename with extended encoding
Content-Disposition: attachment;
filename="report.pdf";
filename*=utf-8''%E5%A0%B1%E5%91%8A%E6%9B%B8.pdf
The filename* parameter uses percent-encoding for non-ASCII characters. Clients should prefer filename* when present.
| Parameter | Purpose | Example |
|---|---|---|
| filename | Suggested filename (ASCII) | filename="report.pdf" |
| filename* | Suggested filename (encoded) | filename*=utf-8''%E5%A0%B1.pdf |
| name | Form field name (form-data) | name="document" |
| creation-date | File creation timestamp | creation-date="2024-01-15T10:00:00Z" |
| modification-date | Last modification timestamp | modification-date="2024-01-20T15:30:00Z" |
| size | File size in bytes | size=1048576 |
12345678910111213141516171819202122232425
# Simple inline imageContent-Type: image/jpegContent-Disposition: inline # Simple attachmentContent-Type: application/pdfContent-Disposition: attachment; filename="invoice.pdf" # Unicode filename (Japanese: 報告書.pdf = "report")Content-Type: application/pdfContent-Disposition: attachment; filename="hokoku.pdf"; filename*=utf-8''%E5%A0%B1%E5%91%8A%E6%9B%B8.pdf # Form field (multipart/form-data)Content-Disposition: form-data; name="user_profile" # File upload fieldContent-Disposition: form-data; name="avatar"; filename="profile.jpg" # With creation dateContent-Disposition: attachment; filename="backup.zip"; creation-date="Wed, 15 Jan 2024 10:00:00 GMT"; size=52428800Never trust Content-Disposition filenames for filesystem operations without sanitizing! Malicious filenames like '../../../etc/passwd' or 'CON' (Windows reserved name) or filenames with null bytes could cause security vulnerabilities. Always: (1) Strip directory components, (2) Reject reserved names, (3) Limit length, (4) Restrict characters to safe subset.
../../../etc/passwd — Path traversalC:\Windows\System32\config — Absolute path.htaccess — Server config overridefile.php.jpg — Extension confusionCON, NUL, LPT1 — Windows reservedfile\x00.txt.exe — Null byte injectionWhen multipart messages fail, the symptoms can be cryptic: missing attachments, corrupted files, garbled text, or outright parsing failures. Here's a systematic approach to debugging.
Symptom: Parts Not Parsed
Possible causes:
-- prefix on delimitersDiagnosis:
# Examine raw message with hex dump
xxd message.eml | head -100
# Look for boundary declarations
grep -n "boundary" message.eml
# Find all delimiter lines
grep -n "^--" message.eml
Symptom: Corrupted Binary Attachments
Possible causes:
-- followed by boundary string exactly-- (e.g., --boundary--)123456789101112131415161718192021222324252627282930313233343536373839404142
#!/bin/bash# Debug multipart message structure MESSAGE_FILE="$1" echo "=== Boundary Declarations ==="grep -i "boundary" "$MESSAGE_FILE" echo ""echo "=== Delimiter Lines ==="grep -n "^--" "$MESSAGE_FILE" echo ""echo "=== Line Ending Analysis ==="# Check for CRLF vs LFif file "$MESSAGE_FILE" | grep -q "CRLF"; then echo "Line endings: CRLF (correct for RFC)"else echo "Line endings: LF only (may cause issues with strict parsers)"fi echo ""echo "=== Part Headers ==="# Extract Content-Type and Content-Disposition headersgrep -E -i "^Content-(Type|Disposition|Transfer-Encoding|ID):" "$MESSAGE_FILE" echo ""echo "=== Hex Dump of First Delimiter ==="# Find first delimiter and show hexFIRST_DELIM_LINE=$(grep -n "^--" "$MESSAGE_FILE" | head -1 | cut -d: -f1)sed -n "${FIRST_DELIM_LINE}p" "$MESSAGE_FILE" | xxd echo ""echo "=== Structure Validation ==="# Count delimitersTOTAL_DELIM=$(grep -c "^--" "$MESSAGE_FILE")CLOSE_DELIM=$(grep -c "^--.*--$" "$MESSAGE_FILE")echo "Total delimiters: $TOTAL_DELIM"echo "Close delimiters: $CLOSE_DELIM"if [ "$CLOSE_DELIM" -eq 0 ]; then echo "WARNING: No close delimiter found!"fiWhile understanding multipart parsing is essential, in production use battle-tested libraries: Python's email.parser, Node.js's mailparser or formidable, Java's javax.mail, or Go's mime/multipart. These handle edge cases discovered over decades of real-world usage.
Multipart messages are the foundation of rich email and file uploads. Let's consolidate the essential knowledge:
-- prefix; close delimiters add -- suffix.What's Next
With multipart composition mastered, the next critical topic is encoding—specifically Base64 and other transfer encodings that make binary data safe for 7-bit channels. We'll explore the mathematics of Base64, when to use quoted-printable, and the performance implications of encoding overhead.
You now understand multipart message construction—the mechanism enabling emails with attachments, inline images, and format alternatives. From boundary protocols to nested structures, from parsing algorithms to security considerations, you have the knowledge to work with complex MIME messages confidently. Next, we'll explore Base64 and other transfer encodings.