Files
MagnumOpus/Packages/MagnumOpusCore/Sources/SMTPClient/MessageFormatter.swift
T
2026-03-14 05:25:03 +01:00

111 lines
3.2 KiB
Swift

import Foundation
import Models
/// Builds RFC 5322 formatted email messages.
public enum MessageFormatter: Sendable {
/// Format an OutgoingMessage into a complete RFC 5322 message string with CRLF line endings.
public static func format(_ message: OutgoingMessage) -> String {
var headers: [(String, String)] = []
headers.append(("From", formatAddress(message.from)))
headers.append(("To", message.to.map(formatAddress).joined(separator: ", ")))
if !message.cc.isEmpty {
headers.append(("Cc", message.cc.map(formatAddress).joined(separator: ", ")))
}
// BCC intentionally omitted from headers per RFC 5322
headers.append(("Subject", message.subject))
headers.append(("Date", formatRFC2822Date(Date())))
headers.append(("Message-ID", "<\(message.messageId)>"))
if let inReplyTo = message.inReplyTo {
headers.append(("In-Reply-To", "<\(inReplyTo)>"))
}
if let references = message.references {
headers.append(("References", references))
}
headers.append(("MIME-Version", "1.0"))
headers.append(("Content-Type", "text/plain; charset=utf-8"))
headers.append(("Content-Transfer-Encoding", "quoted-printable"))
var result = ""
for (name, value) in headers {
result += "\(name): \(value)\r\n"
}
result += "\r\n"
result += quotedPrintableEncode(message.bodyText)
return result
}
/// Generate a unique Message-ID for the given domain.
public static func generateMessageId(domain: String) -> String {
"\(UUID().uuidString)@\(domain)"
}
/// Extract the domain part from an email address.
public static func domainFromEmail(_ email: String) -> String {
guard let atIndex = email.lastIndex(of: "@") else { return email }
return String(email[email.index(after: atIndex)...])
}
/// Format an EmailAddress as RFC 5322 address: `"Name" <addr>` or bare `addr`.
public static func formatAddress(_ addr: EmailAddress) -> String {
if let name = addr.name, !name.isEmpty {
return "\"\(name)\" <\(addr.address)>"
}
return addr.address
}
/// Format a Date as RFC 2822 date string.
public static func formatRFC2822Date(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
formatter.timeZone = TimeZone.current
return formatter.string(from: date)
}
/// Encode a string using quoted-printable encoding (RFC 2045).
public static func quotedPrintableEncode(_ text: String) -> String {
var result = ""
let data = Array(text.utf8)
var lineLength = 0
for byte in data {
let encoded: String
if byte == 0x0A {
// LF CRLF
encoded = "\r\n"
result += encoded
lineLength = 0
continue
} else if byte == 0x0D {
// CR skip, we handle LF CRLF above
continue
} else if byte == 0x09 || (byte >= 0x20 && byte <= 0x7E && byte != 0x3D) {
// Printable ASCII (except =) and tab: literal
encoded = String(UnicodeScalar(byte))
} else {
// Everything else: =XX hex encoding
encoded = String(format: "=%02X", byte)
}
// Soft line break if line would exceed 76 chars
if lineLength + encoded.count > 76 {
result += "=\r\n"
lineLength = 0
}
result += encoded
lineLength += encoded.count
}
return result
}
}