use super::{params, LENIENT_BASE64};
use base64::Engine as _;
pub(crate) fn decode_encoded_words(input: &str) -> String {
let mut result = String::new();
let mut remaining = input;
while !remaining.is_empty() {
if let Some(start) = remaining.find("=?") {
result.push_str(&remaining[..start]);
let candidate = &remaining[start..];
let decoded = if has_linear_whitespace_boundary_before(&remaining[..start]) {
try_decode_encoded_word(candidate).filter(|(_, consumed)| {
has_linear_whitespace_boundary_after(candidate, *consumed)
})
} else {
None
};
if let Some((decoded, consumed)) = decoded {
result.push_str(&decoded);
remaining = &candidate[consumed..];
let trimmed = remaining.trim_start_matches([' ', '\t']);
let next_is_valid = trimmed.starts_with("=?")
&& try_decode_encoded_word(trimmed).is_some_and(|(_, consumed)| {
has_linear_whitespace_boundary_after(trimmed, consumed)
});
if next_is_valid {
remaining = trimmed;
}
} else {
result.push_str("=?");
remaining = &candidate[2..];
}
} else {
result.push_str(remaining);
break;
}
}
result
}
fn has_linear_whitespace_boundary_before(prefix: &str) -> bool {
prefix.is_empty() || prefix.ends_with([' ', '\t'])
}
fn has_linear_whitespace_boundary_after(candidate: &str, consumed: usize) -> bool {
match candidate[consumed..].chars().next() {
None => true,
Some(c) => c == ' ' || c == '\t',
}
}
fn try_decode_encoded_word(input: &str) -> Option<(String, usize)> {
let rest = input.strip_prefix("=?")?;
let q1 = rest.find('?')?;
let charset_raw = &rest[..q1];
let charset = charset_raw.split('*').next().unwrap_or(charset_raw);
let rest2 = &rest[q1 + 1..];
let q2 = rest2.find('?')?;
let encoding = &rest2[..q2];
let rest3 = &rest2[q2 + 1..];
let q3 = rest3.find("?=")?;
let encoded_text = &rest3[..q3];
if charset.is_empty() || encoded_text.is_empty() {
return None;
}
let encoded_text_clean: String = encoded_text.chars().filter(|&c| c != ' ').collect();
let encoded_text = encoded_text_clean.as_str();
if encoded_text
.bytes()
.any(|b| b <= 0x20 || b >= 0x7F || b == b'?')
{
return None;
}
let consumed = 2 + q1 + 1 + q2 + 1 + q3 + 2;
let bytes = match encoding.to_ascii_uppercase().as_str() {
"B" => {
let cleaned: Vec<u8> = encoded_text
.bytes()
.filter(|b| b.is_ascii_alphanumeric() || *b == b'+' || *b == b'/' || *b == b'=')
.collect();
LENIENT_BASE64.decode(&cleaned).ok()?
}
"Q" => {
if !is_valid_q_encoded_word_text(encoded_text) {
return None;
}
decode_q_encoding(encoded_text)
}
_ => return None,
};
Some((decode_charset(charset, &bytes), consumed))
}
fn is_valid_q_encoded_word_text(input: &str) -> bool {
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'=' {
i += 1;
continue;
}
if i + 2 >= bytes.len() || params::decode_hex_pair(bytes[i + 1], bytes[i + 2]).is_none() {
return false;
}
i += 3;
}
true
}
pub(crate) fn decode_q_encoding(input: &str) -> Vec<u8> {
let bytes = input.as_bytes();
let mut result = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'=' && i + 2 < bytes.len() {
if let Some(val) = params::decode_hex_pair(bytes[i + 1], bytes[i + 2]) {
result.push(val);
i += 3;
continue;
}
}
if bytes[i] == b'_' {
result.push(b' ');
} else {
result.push(bytes[i]);
}
i += 1;
}
result
}
pub(crate) fn decode_charset(charset: &str, bytes: &[u8]) -> String {
let charset_lower = charset.to_lowercase();
if charset_lower == "utf-8" || charset_lower == "utf8" {
return String::from_utf8_lossy(bytes).into_owned();
}
let encoding =
encoding_rs::Encoding::for_label(charset.as_bytes()).unwrap_or(encoding_rs::UTF_8);
let (decoded, _, _) = encoding.decode(bytes);
decoded.into_owned()
}