Files
MagnumOpus/Packages/MagnumOpusCore/Sources/MIMEParser/RFC2047Decoder.swift
2026-03-14 12:07:17 +01:00

100 lines
3.2 KiB
Swift

import Foundation
public enum RFC2047Decoder {
/// Decode RFC 2047 encoded words in a string.
/// Pattern: =?charset?encoding?encoded_text?=
public static func decode(_ input: String) -> String {
let pattern = #"=\?([^?]+)\?([BbQq])\?([^?]*)\?="#
guard let regex = try? NSRegularExpression(pattern: pattern) else {
return input
}
let nsInput = input as NSString
let matches = regex.matches(in: input, range: NSRange(location: 0, length: nsInput.length))
guard !matches.isEmpty else { return input }
var result = ""
var lastEnd = 0
for match in matches {
let matchRange = match.range
// Add any non-encoded text between matches (skip whitespace between adjacent encoded words)
let gap = nsInput.substring(with: NSRange(location: lastEnd, length: matchRange.location - lastEnd))
let trimmedGap = gap.trimmingCharacters(in: .whitespaces)
if !trimmedGap.isEmpty || lastEnd == 0 {
// Only add gap if it's not just whitespace between encoded words
if lastEnd == 0 && matchRange.location > 0 {
result += gap
} else if !trimmedGap.isEmpty {
result += gap
}
}
let charset = nsInput.substring(with: match.range(at: 1))
let encoding = nsInput.substring(with: match.range(at: 2)).uppercased()
let encodedText = nsInput.substring(with: match.range(at: 3))
let cfEncoding = CFStringConvertIANACharSetNameToEncoding(charset as CFString)
let nsEncoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding)
let decoded: String?
if encoding == "B" {
guard let data = Data(base64Encoded: encodedText) else {
result += nsInput.substring(with: matchRange)
lastEnd = matchRange.location + matchRange.length
continue
}
decoded = String(data: data, encoding: String.Encoding(rawValue: nsEncoding))
} else {
// Q encoding: like quoted-printable but underscores represent spaces
let withSpaces = encodedText.replacingOccurrences(of: "_", with: " ")
let data = decodeQuotedPrintableBytes(withSpaces)
decoded = String(data: data, encoding: String.Encoding(rawValue: nsEncoding))
}
result += decoded ?? nsInput.substring(with: matchRange)
lastEnd = matchRange.location + matchRange.length
}
// Append any trailing non-encoded text
if lastEnd < nsInput.length {
result += nsInput.substring(from: lastEnd)
}
return result
}
private static func decodeQuotedPrintableBytes(_ input: String) -> Data {
var data = Data()
var i = input.startIndex
while i < input.endIndex {
if input[i] == "=" {
let hexStart = input.index(after: i)
guard hexStart < input.endIndex else {
data.append(contentsOf: "=".utf8)
break
}
let hexEnd = input.index(hexStart, offsetBy: 1, limitedBy: input.endIndex) ?? input.endIndex
guard hexEnd < input.endIndex else {
data.append(contentsOf: String(input[i...]).utf8)
break
}
let nextAfterHex = input.index(after: hexEnd)
let hex = String(input[hexStart...hexEnd])
if let byte = UInt8(hex, radix: 16) {
data.append(byte)
i = nextAfterHex
} else {
data.append(contentsOf: "=".utf8)
i = hexStart
}
} else {
data.append(contentsOf: String(input[i]).utf8)
i = input.index(after: i)
}
}
return data
}
}