A Swift package for parsing MIME formatted multipart data. This library provides a clean, type-safe API for working with MIME messages, making it easy to extract headers, parts, and content from multipart messages.
- ✅ Parse MIME messages (both multipart and non-multipart) according to RFC 2045 and RFC 2046
- ✅ Optional MIME-Version header (not required for parsing)
- ✅ Case-insensitive header access
- ✅ Full support for duplicate headers (e.g., multiple
Receivedheaders) - ✅ Support for quoted and unquoted boundaries
- ✅ Automatic charset detection
- ✅ Generalized header attribute parsing (e.g.,
charset,boundary,filename) - ✅ Type-safe API with
Sendablesupport for Swift 6 - ✅ Convenient helper methods for common operations
- ✅ No external dependencies
- iOS 18.0+ / macOS 15.0+
- Swift 6.2+
Add this package to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/yourusername/swift-mime-generated.git", from: "1.0.0")
]import MIME
let mimeString = """
From: sender@example.com
Date: Mon, 01 Jan 2024 12:00:00 -0800
Content-Type: multipart/mixed; boundary="simple"
--simple
Content-Type: text/plain
Hello, World!
--simple
Content-Type: text/html
<h1>Hello, World!</h1>
--simple--
"""
let message = try MIMEDecoder().decode(mimeString)
// Access top-level headers
print(message.headers["From"]) // "sender@example.com"
print(message.headers["Date"]) // "Mon, 01 Jan 2024 12:00:00 -0800"
// Access parts
print(message.parts.count) // 2
for part in message.parts {
print(part.headers["Content-Type"]) // "text/plain", "text/html"
print(part.body)
}Non-multipart messages (like text/plain, text/html, application/json, etc.) are automatically treated as a single part:
let simpleMessage = """
From: sender@example.com
To: recipient@example.com
Subject: Simple Text Message
Content-Type: text/plain; charset="utf-8"
This is a simple text message without multipart formatting.
It will be parsed as a single part.
"""
let message = try MIMEDecoder().decode(simpleMessage)
// Convenient access to body for non-multipart messages
if let body = message.body {
print(body) // "This is a simple text message..."
}
// Or access via parts array
print(message.parts.count) // 1
print(message.parts[0].headers["Content-Type"]) // "text/plain"
print(message.parts[0].body) // "This is a simple text message..."The primary parsing method accepts Data objects. This is the recommended approach when working with network responses or file data:
// Parse from Data (primary method)
let data = mimeString.data(using: .utf8)!
let message = try MIMEDecoder().decode(data)
// The Data will be decoded as UTF-8
print(message.headers["From"])
print(message.parts.count)You can also use the convenience method that accepts a String directly:
// Convenience method for String input
let message = try MIMEDecoder().decode(mimeString)If the Data cannot be decoded as UTF-8, a MIMEError.invalidUTF8 error will be thrown.
// Find all parts with a specific content type
let plainParts = message.parts(withContentType: "text/plain")
// Find the first part with a specific content type
if let htmlPart = message.firstPart(withContentType: "text/html") {
print(htmlPart.body)
}
// Check if a message contains a specific content type
if message.hasPart(withContentType: "application/json") {
print("Message contains JSON data")
}MIME messages and parts have mutable properties, making them easy to edit. Once edited, you can encode them back to MIME format.
var message = try MIMEDecoder().decode(mimeString)
// Edit top-level headers
message.headers["From"] = "new-sender@example.com"
message.headers["Subject"] = "Updated subject"
// Edit part headers
message.parts[0].headers["Content-Type"] = "text/html"var message = try MIMEDecoder().decode(mimeString)
// Edit part body
message.parts[0].body = "This is the new content!"
// For non-multipart messages, edit the single part
if message.parts.count == 1 {
message.parts[0].body = "New simple message content"
}var message = try MIMEDecoder().decode(mimeString)
// Add a new part
var newPartHeaders = MIMEHeaders()
newPartHeaders["Content-Type"] = "text/plain"
let newPart = MIMEPart(headers: newPartHeaders, body: "New part content")
message.parts.append(newPart)
// Remove a part
message.parts.remove(at: 1)After editing, encode the message back to data:
var message = try MIMEDecoder().decode(mimeString)
// Make some edits
message.headers["From"] = "updated@example.com"
message.parts[0].body = "Updated content"
// Encode back to MIME format
let encoder = MIMEEncoder()
let encodedData = encoder.encode(message)
let encodedString = String(data: encodedData, encoding: .utf8) ?? ""
print(encodedString)
// Output:
// From: updated@example.com
// Content-Type: multipart/mixed; boundary="simple"
//
// --simple
// Content-Type: text/plain
//
// Updated content
// --simple--You can also encode individual parts:
var part = message.parts[0]
part.body = "Modified part content"
part.headers["Custom-Header"] = "Custom Value"
let encoder = MIMEEncoder()
let encodedData = encoder.encode(part)
let encodedString = String(data: encodedData, encoding: .utf8) ?? ""
print(encodedString)
// Output:
// Content-Type: text/plain
// Custom-Header: Custom Value
//
// Modified part contentHeaders are case-insensitive:
// All of these work (case-insensitive)
let contentType1 = message.headers["Content-Type"]
let contentType2 = message.headers["content-type"]
let contentType3 = message.headers["CONTENT-TYPE"]
// MIME-Version header is optional
let mimeVersion = message.headers["MIME-Version"] // May be nilPart-specific headers:
let part = message.parts[0]
print(part.headers["Content-Type"]) // "text/plain"
print(part.headerAttributes("Content-Type")["charset"]) // "utf-8"
print(part.headers["Custom-Header"])Many MIME headers contain a primary value followed by semicolon-separated attributes (e.g., Content-Type: text/plain; charset=utf-8; format=flowed). The library provides convenient access to these attributes:
let message = try MIMEDecoder().decode(mimeString)
// Access Content-Type attributes on the message
let attrs = message.headerAttributes("Content-Type")
print(attrs.value) // "multipart/mixed"
print(attrs["boundary"]) // "simple"
print(attrs["charset"]) // "utf-8" (if present)
print(attrs.all) // Dictionary of all attributes
// Access Content-Type attributes on a part
let part = message.parts[0]
let partAttrs = part.headerAttributes("Content-Type")
print(partAttrs.value) // "text/plain"
print(partAttrs["charset"]) // "utf-8"
print(partAttrs["format"]) // "flowed"
// Parse attributes from any header
let disposition = part.headerAttributes("Content-Disposition")
print(disposition.value) // "attachment"
print(disposition["filename"]) // "document.pdf"
print(disposition["size"]) // "1024"You can also parse attributes directly:
let attrs = MIMEHeaderAttributes.parse("text/plain; charset=utf-8; format=flowed")
print(attrs.value) // "text/plain"
print(attrs["charset"]) // "utf-8" (case-insensitive)
print(attrs["CHARSET"]) // "utf-8" (also works)
print(attrs["format"]) // "flowed"Common use cases:
// Extract charset for text content
if let charset = part.headerAttributes("Content-Type")["charset"] {
print("Charset: \(charset)")
}
// Check Content-Disposition for attachments
let disposition = part.headerAttributes("Content-Disposition")
if disposition.value == "attachment", let filename = disposition["filename"] {
print("Attachment: \(filename)")
}
// Access boundary for multipart messages
if let boundary = message.headerAttributes("Content-Type")["boundary"] {
print("Boundary: \(boundary)")
}Complete Example:
let mimeContent = """
From: sender@example.com
Content-Type: multipart/mixed; boundary="docs"; charset="utf-8"
--docs
Content-Type: text/plain; charset=utf-8; format=flowed
Hello! Please find the document attached.
--docs
Content-Type: application/pdf; name="report.pdf"
Content-Disposition: attachment; filename="report.pdf"; size=2048
[PDF content here]
--docs--
"""
let message = try MIMEDecoder().decode(mimeContent)
// Parse message-level Content-Type attributes
let msgAttrs = message.headerAttributes("Content-Type")
print(msgAttrs.value) // "multipart/mixed"
print(msgAttrs["boundary"]) // "docs"
print(msgAttrs["charset"]) // "utf-8"
// Parse first part (text/plain)
let textPart = message.parts[0]
let textAttrs = textPart.headerAttributes("Content-Type")
print(textAttrs.value) // "text/plain"
print(textAttrs["charset"]) // "utf-8"
print(textAttrs["format"]) // "flowed"
// Parse second part (attachment)
let pdfPart = message.parts[1]
let pdfAttrs = pdfPart.headerAttributes("Content-Type")
print(pdfAttrs.value) // "application/pdf"
print(pdfAttrs["name"]) // "report.pdf"
// Parse Content-Disposition for attachment metadata
let disposition = pdfPart.headerAttributes("Content-Disposition")
print(disposition.value) // "attachment"
print(disposition["filename"]) // "report.pdf"
print(disposition["size"]) // "2048"Some headers can appear multiple times in a MIME message (e.g., Received headers in email). The library fully supports this:
// Subscript returns the first value
let firstReceived = message.headers["Received"]
// Get all values for a header
let allReceived = message.headers.values(for: "Received")
for received in allReceived {
print(received)
}
// Add a header without replacing existing ones
var headers = MIMEHeaders()
headers.add("Received", value: "from server1.example.com")
headers.add("Received", value: "from server2.example.com")
headers.add("Received", value: "from server3.example.com")
// Setting via subscript replaces all occurrences
headers["X-Custom"] = "new-value" // Replaces all X-Custom headers
// Remove all headers with a name
headers.removeAll("Received")Parsing preserves all duplicate headers:
let mimeContent = """
From: sender@example.com
Received: from server1.example.com
Received: from server2.example.com
Received: from server3.example.com
Content-Type: text/plain
Body
"""
let message = try MIMEDecoder().decode(mimeContent)
let received = message.headers.values(for: "Received")
print(received.count) // 3Here's a more complex example showing a bookmark tracking system:
let bookmarkData = """
From: Nathan Borror <zV6nZFTyrypSgXo1mxC02yg6PKeXv8gWpKWa1/AzAPw=>
Date: Wed, 15 Oct 2025 18:42:00 -0700
MIME-Version: 1.0
Content-Type: multipart/bookmark; boundary="bookmark"
--bookmark
Content-Type: text/book-info
Title: Why Greatness Cannot Be Planned
Subtitle: The Myth of the Objective
Authors: Kenneth O. Stanley, Joel Lehman
ISBN-13: 978-3319155234
Published: 18 May 2015
Language: en
Pages: 135
--bookmark
Content-Type: text/quote; charset="utf-8"
Page: 10
Date: Thu, 29 May 2025 16:20:00 -0700
"Sometimes the best way to achieve something great is to stop trying to achieve a particular great thing."
--bookmark
Content-Type: text/note; charset="utf-8"
Page: 10
Date: Thu, 29 May 2025 16:20:00 -0700
This book is turning out to be very cathartic!
--bookmark
Content-Type: text/progress
Page: 65
Date: Wed, 13 Oct 2025 18:42:00 -0700
--bookmark
Content-Type: text/review; charset="utf-8"
Date: Wed, 15 Oct 2025 18:42:00 -0700
Rating: 4.5
Spoilers: false
I enjoyed this book!
--bookmark--
"""
let message = try MIMEDecoder().decode(bookmarkData)
// Get book info
if let bookInfo = message.firstPart(withContentType: "text/book-info") {
print(bookInfo.headers["Title"]) // "Why Greatness Cannot Be Planned"
print(bookInfo.headers["Authors"]) // "Kenneth O. Stanley, Joel Lehman"
print(bookInfo.headers["ISBN-13"]) // "978-3319155234"
}
// Get all quotes
let quotes = message.parts(withContentType: "text/quote")
for quote in quotes {
print("Page \(quote.headers["Page"] ?? "?"): \(quote.body)")
}
// Get all notes
let notes = message.parts(withContentType: "text/note")
for note in notes {
print(note.body)
}
// Get progress
if let progress = message.firstPart(withContentType: "text/progress") {
print("Currently on page \(progress.headers["Page"] ?? "?")")
}
// Get review
if let review = message.firstPart(withContentType: "text/review") {
print("Rating: \(review.headers["Rating"] ?? "N/A")")
print("Review: \(review.body)")
}The MIME validator allows you to validate MIME messages according to RFC 2045/2046 and custom rules. You can set header expectations for specific content types, ensuring that messages meet your requirements.
import MIME
let mimeString = """
Content-Type: text/plain
Hello, World!
"""
let validator = MIMEValidator()
let result = try validator.validate(mimeString)
if result.isValid {
print("✓ Message is valid")
} else {
print("✗ Message is invalid")
for error in result.errors {
print(" • \(error)")
}
}You can define custom header expectations for specific content types:
let expectation = MIMEHeaderExpectation(
contentType: "text/plain",
requiredHeaders: ["Content-Type", "Content-Transfer-Encoding"],
recommendedHeaders: ["Content-Disposition"],
expectedValues: ["Content-Transfer-Encoding": "7bit"]
)
let validator = MIMEValidator(expectations: [expectation])
let result = try validator.validate(mimeString)
print(result.summary) // "✓ Validation passed" or "✗ Validation failed with N error(s)"
print(result.description) // Detailed report with errors and warningsConfigure validation behavior with various options:
// Require MIME-Version header
let validator = MIMEValidator(requireMimeVersion: true)
// Strict multipart validation (requires boundary and non-empty parts)
let validator = MIMEValidator(strictMultipart: true)
// Use default expectations for common content types
let validator = MIMEValidator.withDefaults()The validator automatically validates all parts in multipart messages:
let multipartMessage = """
Content-Type: multipart/mixed; boundary="boundary123"
--boundary123
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
First part
--boundary123
Content-Type: text/html
Content-Transfer-Encoding: quoted-printable
<p>Second part</p>
--boundary123--
"""
let plainExpectation = MIMEHeaderExpectation(
contentType: "text/plain",
requiredHeaders: ["Content-Type", "Content-Transfer-Encoding"]
)
let htmlExpectation = MIMEHeaderExpectation(
contentType: "text/html",
requiredHeaders: ["Content-Type", "Content-Transfer-Encoding"]
)
let validator = MIMEValidator(expectations: [plainExpectation, htmlExpectation])
let result = try validator.validate(multipartMessage)
if result.isValid {
print("All parts are valid!")
}You can add custom validation logic using a closure:
let expectation = MIMEHeaderExpectation(
contentType: "text/plain",
customValidator: { headers in
// Check for custom header
guard let customHeader = headers["X-Custom-Header"],
customHeader == "required-value" else {
return [.custom("X-Custom-Header must be 'required-value'")]
}
return []
}
)
let validator = MIMEValidator(expectations: [expectation])
let result = try validator.validate(mimeString)The library includes preset expectations for common content types:
// Available presets:
// - MIMEHeaderExpectation.textPlain
// - MIMEHeaderExpectation.textHtml
// - MIMEHeaderExpectation.applicationJson
// - MIMEHeaderExpectation.multipartMixed
// - MIMEHeaderExpectation.multipartAlternative
let validator = MIMEValidator(expectations: [
.textPlain,
.textHtml,
.applicationJson
])The MIMEValidationResult provides detailed information about validation:
let result = try validator.validate(mimeString)
// Check if valid
if result.isValid {
print("Valid!")
}
// Get errors (empty if valid)
for error in result.errors {
print("Error: \(error)")
}
// Get warnings (non-fatal issues)
for warning in result.warnings {
print("Warning: \(warning)")
}
// Get summary
print(result.summary) // "✓ Validation passed" or "✗ Validation failed with 2 error(s)"
// Get full description
print(result.description) // Includes summary, errors, and warningsYou can validate individual parts separately:
let message = try MIMEDecoder().decode(multipartMessage)
let part = message.parts[0]
let expectation = MIMEHeaderExpectation(
contentType: "text/plain",
requiredHeaders: ["Content-Type"]
)
let validator = MIMEValidator(expectations: [expectation])
let result = validator.validatePart(part, index: 0)
if result.isValid {
print("Part is valid!")
}Validates MIME messages according to RFC 2045/2046 and custom rules.
init(expectations: [MIMEHeaderExpectation] = [], requireMimeVersion: Bool = false, strictMultipart: Bool = true)- Creates a validator with custom expectations
static func withDefaults(requireMimeVersion: Bool = false, strictMultipart: Bool = true) -> MIMEValidator- Creates a validator with default expectations for common content types
func validate(_ message: MIMEMessage) -> MIMEValidationResult- Validates a parsed MIME message
func validate(_ content: String) throws -> MIMEValidationResult- Parses and validates a MIME message string
func validatePart(_ part: MIMEPart, index: Int = 0) -> MIMEValidationResult- Validates a specific part of a message
Defines header expectations for a specific content type.
init(contentType: String, requiredHeaders: Set<String> = [], recommendedHeaders: Set<String> = [], expectedValues: [String: String] = [:], customValidator: ((MIMEHeaders) -> [MIMEValidationError])? = nil)- Creates a header expectation for a content type
.textPlain- Expectation for text/plain content.textHtml- Expectation for text/html content.applicationJson- Expectation for application/json content.multipartMixed- Expectation for multipart/mixed content.multipartAlternative- Expectation for multipart/alternative content
The result of a validation operation.
isValid: Bool- Whether the validation passederrors: [MIMEValidationError]- List of validation errors (empty if valid)warnings: [String]- List of validation warnings (non-fatal issues)summary: String- A human-readable summarydescription: String- A detailed description including errors and warnings
static func success(warnings: [String] = []) -> MIMEValidationResult- Creates a successful validation result
static func failure(errors: [MIMEValidationError], warnings: [String] = []) -> MIMEValidationResult- Creates a failed validation result
Errors that can occur during validation.
missingRequiredHeader(String)- A required header is missinginvalidHeaderValue(header: String, expected: String, actual: String?)- A header value doesn't match expected formatinvalidContentType(String)- Content-Type header is missing or invalidmissingBoundary- Multipart message is missing boundary parameteremptyMultipart- Multipart message has no partsinvalidPartIndex(Int)- Part index is out of boundspartMissingHeader(partIndex: Int, header: String)- A part is missing required headerspartInvalidHeaderValue(partIndex: Int, header: String, expected: String, actual: String?)- A part has an invalid header valuecustom(String)- Custom validation error
The main entry point for parsing MIME messages. Supports both multipart messages (with boundaries) and non-multipart messages.
decode(_: Data) throws -> MIMEMessage- Decode a MIME message from datadecode(_: String) throws -> MIMEMessage- Decode a MIME message from a string
Represents parsed attributes from a header value. Many MIME headers contain a primary value followed by semicolon-separated attributes.
value: String- The primary value before any attributesall: [String: String]- Dictionary of all parsed attributes (keys are lowercased)
static func parse(_ headerValue: String?) -> MIMEHeaderAttributes- Parses a header value into its primary value and attributes
- Handles quoted and unquoted attribute values
- Normalizes attribute names to lowercase for case-insensitive access
subscript(key: String) -> String?- Access an attribute by name (case-insensitive)
- Returns nil if the attribute doesn't exist
let attrs = MIMEHeaderAttributes.parse("text/plain; charset=utf-8; format=flowed")
print(attrs.value) // "text/plain"
print(attrs["charset"]) // "utf-8"
print(attrs["CHARSET"]) // "utf-8" (case-insensitive)
print(attrs["format"]) // "flowed"
print(attrs.all) // ["charset": "utf-8", "format": "flowed"]Represents a complete MIME message with headers and parts.
headers: MIMEHeaders- The top-level headersparts: [MIMEPart]- The individual parts of the messagebody: String?- The body content for non-multipart messages (returns nil for multipart messages)
func parts(withContentType contentType: String) -> [MIMEPart]- Returns all parts with a specific content type
func firstPart(withContentType contentType: String) -> MIMEPart?- Returns the first part with a specific content type
func hasPart(withContentType contentType: String) -> Bool- Returns true if any part has the specified content type
func headerAttributes(_ headerName: String) -> MIMEHeaderAttributes- Parses attributes from any header value
func encode() -> Data- Encodes the message back to MIME format data
Represents a single part of a multipart MIME message.
headers: MIMEHeaders- The headers for this partbody: String- The body contentdecodedBody: String- The decoded body content
func headerAttributes(_ headerName: String) -> MIMEHeaderAttributes- Parses attributes from any header value
func encode() -> Data- Encodes the part back to MIME format data
A case-insensitive collection for MIME headers with support for duplicate header names.
subscript(key: String) -> String?- Access headers by name (case-insensitive). Returns the first value when multiple headers with the same name exist. Setting a value replaces all existing headers with that name.func values(for key: String) -> [String]- Returns all values for a given header name, useful for headers that can appear multiple times (e.g.,Received)func add(_ key: String, value: String)- Adds a header without replacing existing headers with the same namefunc removeAll(_ key: String)- Removes all headers with the given namefunc contains(_ key: String) -> Bool- Check if a header exists- Conforms to
Collection, so you can iterate over headers
Encodes MIME messages to data.
encode(_: MIMEMessage) -> Data- Encode a MIME message to dataencode(_: MIMEPart) -> Data- Encode a MIME part to data
Example:
let encoder = MIMEEncoder()
let data = encoder.encode(message)Errors that can occur during parsing.
invalidFormat- The MIME message format is invalidinvalidEncoding- The character encoding is invalid or unsupportedinvalidUTF8- The data cannot be decoded as UTF-8noHeaders- The MIME message has no headers
Run the test suite:
swift test[Your License Here]
Contributions are welcome! Please feel free to submit a Pull Request.