///|
/// Decode explicitly supported MIME charsets; unsupported charsets never silently corrupt text.
pub fn decode_charset(
data : Bytes,
charset : String,
) -> String raise MailboxError {
if data.length() > 1048576 {
raise Invalid("charset input limit")
}
match charset.trim().to_owned().to_lower() {
"utf-8" | "utf8" =>
@utf8.decode(data) catch {
_ => raise Invalid("invalid UTF-8 MIME text")
}
"us-ascii" | "ascii" => {
let out = StringBuilder()
for b in data {
if b.to_int() > 127 {
raise Invalid("non-ASCII MIME text")
}
out.write_char(b.to_int().unsafe_to_char())
}
out.to_string()
}
"iso-8859-1" | "latin1" | "latin-1" => {
let out = StringBuilder()
for b in data {
out.write_char(b.to_int().unsafe_to_char())
}
out.to_string()
}
_ => raise Invalid("unsupported MIME charset")
}
}
///|
pub fn MimePart::text(self : MimePart) -> String raise MailboxError {
if !self.media_type.has_prefix("text/") {
raise Invalid("MIME part is not text")
}
decode_charset(
self.body,
match self.parameters.get("charset") {
Some(c) => c
None => "us-ascii"
},
)
}
///|
/// Decode an unstructured header/display phrase. Not an address parser or wire encoder.
pub fn decode_header(value : String) -> String raise MailboxError {
if value.length() > 65536 {
raise Invalid("header decoding limit")
}
let chars = value.to_array()
let out = StringBuilder()
let whitespace = StringBuilder()
let mut at = 0
let mut previous_encoded = false
while at < chars.length() {
if chars[at] == ' ' ||
chars[at] == '\t' ||
chars[at] == '\r' ||
chars[at] == '\n' {
whitespace.write_char(chars[at])
at += 1
continue
}
if at + 1 < chars.length() && chars[at] == '=' && chars[at + 1] == '?' {
let start = at
let mut end = at + 2
let mut separators = 0
while end < chars.length() && separators < 2 && end - start < 75 {
if chars[end] == '?' {
separators += 1
}
end += 1
}
while end + 1 < chars.length() &&
!(chars[end] == '?' && chars[end + 1] == '=') &&
end - start < 75 {
end += 1
}
if end + 1 >= chars.length() ||
end - start + 2 > 75 ||
chars[end] != '?' ||
chars[end + 1] != '=' {
raise Invalid("unterminated or oversized encoded word")
}
let inner = String::from_array(chars[at + 2:end])
let pieces = inner.split("?").map(x => x.to_owned()).collect()
if pieces.length() != 3 ||
pieces[0].is_empty() ||
pieces[2].is_empty() ||
inner.to_array().iter().any(c => c.to_int() <= 32 || c.to_int() > 126) {
raise Invalid("invalid encoded word")
}
let bytes = match pieces[1].to_lower() {
"b" =>
@base64.decode(pieces[2]) catch {
_ => raise Invalid("invalid header base64")
}
"q" =>
decode_transfer(
@utf8.encode(pieces[2].replace_all(old="_", new=" ")),
"quoted-printable",
)
_ => raise Invalid("unsupported encoded word method")
}
if !previous_encoded {
out.write_string(whitespace.to_string())
}
whitespace.reset()
out.write_string(decode_charset(bytes, pieces[0]))
previous_encoded = true
at = end + 2
} else {
out.write_string(whitespace.to_string())
whitespace.reset()
out.write_char(chars[at])
at += 1
previous_encoded = false
}
}
out.write_string(whitespace.to_string())
out.to_string()
}