pub(crate) fn decode_rfc2047(input: &[u8]) -> String {
let s = String::from_utf8_lossy(input);
decode_rfc2047_str(&s)
}
fn decode_rfc2047_str(input: &str) -> String {
let mut result = String::new();
let mut remaining = input;
let mut last_was_encoded = false;
while let Some(start) = remaining.find("=?") {
let before = &remaining[..start];
let ws_deferred = last_was_encoded && before.chars().all(|c| c == ' ' || c == '\t');
if !ws_deferred {
result.push_str(before);
}
let candidate_has_valid_prefix = before.is_empty() || before.ends_with([' ', '\t']);
let saved_after_prefix = &remaining[start + 2..];
remaining = saved_after_prefix;
if candidate_has_valid_prefix {
if let Some(decoded) = parse_encoded_word(&mut remaining) {
let candidate_has_valid_suffix = match remaining.chars().next() {
None => true,
Some(c) => c == ' ' || c == '\t',
};
if candidate_has_valid_suffix {
result.push_str(&decoded);
last_was_encoded = true;
continue;
}
}
}
remaining = saved_after_prefix;
if ws_deferred {
result.push_str(before);
}
result.push_str("=?");
last_was_encoded = false;
}
result.push_str(remaining);
result
}
fn parse_encoded_word(remaining: &mut &str) -> Option<String> {
let saved = *remaining;
let result = parse_encoded_word_inner(remaining);
if result.is_none() {
*remaining = saved;
}
result
}
fn parse_encoded_word_inner(remaining: &mut &str) -> Option<String> {
let q1 = remaining.find('?')?;
let charset_raw = &remaining[..q1];
let charset = match charset_raw.find('*') {
Some(pos) => &charset_raw[..pos],
None => charset_raw,
};
*remaining = &remaining[q1 + 1..];
let q2 = remaining.find('?')?;
let encoding = &remaining[..q2];
*remaining = &remaining[q2 + 1..];
let end = remaining.find("?=")?;
let encoded_text = &remaining[..end];
*remaining = &remaining[end + 2..];
if charset.is_empty()
|| encoding.is_empty()
|| !encoded_text
.bytes()
.all(|b| (33..=126).contains(&b) && b != b'?')
{
return None;
}
let raw_bytes = match encoding.to_ascii_uppercase().as_str() {
"B" => {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(encoded_text)
.ok()?
}
"Q" => decode_q_encoding(encoded_text),
_ => return None,
};
let charset_upper = charset.to_ascii_uppercase();
if charset_upper == "UTF-8" || charset_upper == "US-ASCII" || charset_upper == "ASCII" {
Some(String::from_utf8_lossy(&raw_bytes).into_owned())
} else {
let encoding = encoding_rs::Encoding::for_label(charset.as_bytes())?;
let (cow, _) = encoding.decode_without_bom_handling(&raw_bytes);
Some(cow.into_owned())
}
}
pub(super) fn decode_q_encoding(input: &str) -> Vec<u8> {
let mut result = Vec::new();
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'=' if i + 1 < bytes.len() => {
if bytes[i + 1] == b'\r' && i + 2 < bytes.len() && bytes[i + 2] == b'\n' {
i += 3;
} else if bytes[i + 1] == b'\n' {
i += 2;
} else if i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2]))
{
result.push(hi << 4 | lo);
i += 3;
} else {
result.push(b'=');
i += 1;
}
} else {
result.push(b'=');
i += 1;
}
}
b'_' => {
result.push(b' ');
i += 1;
}
b => {
result.push(b);
i += 1;
}
}
}
result
}
pub(super) fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'A'..=b'F' => Some(b - b'A' + 10),
b'a'..=b'f' => Some(b - b'a' + 10),
_ => None,
}
}