100 lines
3.2 KiB
Swift
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
|
|
}
|
|
}
|