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" ` 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 } }