use std::borrow::Cow;
use base64::Engine;
use super::shared::hex_val;
pub(super) fn escape_encoded_words_inside_quoted_strings(input: &str) -> Cow<'_, str> {
let bytes = input.as_bytes();
let mut needs_escape = false;
let mut i = 0;
let mut in_quotes = false;
let mut escaped_pair = false;
while i < bytes.len() {
let byte = bytes[i];
if escaped_pair {
escaped_pair = false;
i += 1;
continue;
}
match byte {
b'\\' if in_quotes => {
escaped_pair = true;
}
b'"' => {
in_quotes = !in_quotes;
}
b'=' if in_quotes && i + 1 < bytes.len() && bytes[i + 1] == b'?' => {
needs_escape = true;
break;
}
_ => {}
}
i += 1;
}
if !needs_escape {
return Cow::Borrowed(input);
}
let mut out = String::with_capacity(input.len() + 4);
in_quotes = false;
escaped_pair = false;
for (idx, byte) in bytes.iter().copied().enumerate() {
if escaped_pair {
escaped_pair = false;
out.push(byte as char);
continue;
}
if in_quotes && byte == b'=' && idx + 1 < bytes.len() && bytes[idx + 1] == b'?' {
out.push('\\');
out.push('=');
continue;
}
match byte {
b'\\' if in_quotes => {
escaped_pair = true;
out.push(byte as char);
}
b'"' => {
in_quotes = !in_quotes;
out.push(byte as char);
}
_ => out.push(byte as char),
}
}
Cow::Owned(out)
}
#[must_use]
pub fn decode_rfc2047_phrase(input: &str) -> Cow<'_, str> {
decode_rfc2047_words(input)
}
pub(super) fn decode_rfc2047_words(input: &str) -> Cow<'_, str> {
if !input.contains("=?") {
return Cow::Borrowed(input);
}
let mut out: Option<String> = None;
let mut idx = 0usize;
let mut prev_was_encoded_word = false;
while idx < input.len() {
let rest = &input[idx..];
let Some(start_rel) = rest.find("=?") else {
if let Some(buffer) = out.as_mut() {
buffer.push_str(rest);
}
break;
};
let plain = &rest[..start_rel];
let candidate = &rest[start_rel..];
if prev_was_encoded_word
&& !plain.is_empty()
&& plain.bytes().all(|byte| byte == b' ' || byte == b'\t')
&& try_decode_rfc2047_word(candidate).is_some()
{
idx += start_rel;
continue;
}
let buffer = out.get_or_insert_with(|| String::with_capacity(input.len()));
if buffer.is_empty() && idx > 0 {
buffer.push_str(&input[..idx]);
}
buffer.push_str(plain);
if let Some((decoded, consumed)) = try_decode_rfc2047_word(candidate) {
buffer.push_str(&decoded);
idx += start_rel + consumed;
prev_was_encoded_word = true;
} else {
buffer.push_str("=?");
idx += start_rel + 2;
prev_was_encoded_word = false;
}
}
match out {
Some(buffer) => Cow::Owned(buffer),
None => Cow::Borrowed(input),
}
}
fn try_decode_rfc2047_word(input: &str) -> Option<(String, usize)> {
let end_rel = input.find("?=")?;
let consumed = end_rel + 2;
let word = &input[..consumed];
Some((decode_rfc2047_word(word)?, consumed))
}
fn decode_rfc2047_word(word: &str) -> Option<String> {
if !word.starts_with("=?") || !word.ends_with("?=") {
return None;
}
let inner = &word[2..word.len() - 2];
let mut parts = inner.splitn(3, '?');
let charset = parts.next()?;
let encoding = parts.next()?;
let encoded = parts.next()?;
let bytes = if encoding.eq_ignore_ascii_case("B") {
base64::engine::general_purpose::STANDARD
.decode(encoded)
.ok()?
} else if encoding.eq_ignore_ascii_case("Q") {
decode_rfc2047_q(encoded)?
} else {
return None;
};
if charset.eq_ignore_ascii_case("utf-8") || charset.eq_ignore_ascii_case("us-ascii") {
return String::from_utf8(bytes).ok();
}
if charset.eq_ignore_ascii_case("iso-8859-1") || charset.eq_ignore_ascii_case("latin1") {
return Some(bytes.into_iter().map(char::from).collect());
}
None
}
fn decode_rfc2047_q(input: &str) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(input.len());
let bytes = input.as_bytes();
let mut idx = 0usize;
while idx < bytes.len() {
let byte = bytes[idx];
if byte == b'_' {
out.push(b' ');
idx += 1;
continue;
}
if byte == b'=' {
if idx + 2 >= bytes.len() {
return None;
}
let hi = hex_val(bytes[idx + 1])?;
let lo = hex_val(bytes[idx + 2])?;
out.push((hi << 4) | lo);
idx += 3;
continue;
}
out.push(byte);
idx += 1;
}
Some(out)
}
pub(super) fn encode_rfc2047_unstructured(input: &str) -> String {
if input.is_ascii() {
return input.to_owned();
}
encode_rfc2047_utf8_base64_words(input)
}
pub(super) fn encode_rfc2047_phrase(input: &str) -> String {
if input.is_ascii() {
return quote_phrase(input);
}
encode_rfc2047_utf8_base64_words(input)
}
fn encode_rfc2047_utf8_base64_words(input: &str) -> String {
const ENCODED_WORD_OVERHEAD: usize = 12; const MAX_ENCODED_WORD_LEN: usize = 75;
const MAX_BASE64_LEN: usize = MAX_ENCODED_WORD_LEN - ENCODED_WORD_OVERHEAD;
const MAX_CHUNK_BYTES: usize = (MAX_BASE64_LEN / 4) * 3;
let bytes = input.as_bytes();
let mut idx = 0usize;
let mut words = Vec::new();
while idx < bytes.len() {
let mut end = (idx + MAX_CHUNK_BYTES).min(bytes.len());
while end > idx && !input.is_char_boundary(end) {
end -= 1;
}
if end == idx {
end = bytes.len();
while end > idx && !input.is_char_boundary(end) {
end -= 1;
}
}
let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes[idx..end]);
words.push(format!("=?utf-8?B?{encoded}?="));
idx = end;
}
words.join(" ")
}
fn quote_phrase(input: &str) -> String {
let mut out = String::with_capacity(input.len() + 2);
out.push('"');
for ch in input.chars() {
if ch == '\\' || ch == '"' {
out.push('\\');
}
out.push(ch);
}
out.push('"');
out
}