use base64::alphabet::Alphabet;
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::engine::DecodePaddingMode;
use base64::Engine;
use std::sync::LazyLock;
const IMAP_B64_ALPHABET_STR: &str =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
#[allow(clippy::expect_used)] static IMAP_B64_ENGINE: LazyLock<GeneralPurpose> = LazyLock::new(|| {
let alphabet = Alphabet::new(IMAP_B64_ALPHABET_STR)
.expect("IMAP modified Base64 alphabet is a valid 64-char constant");
let config = GeneralPurposeConfig::new()
.with_encode_padding(false)
.with_decode_padding_mode(DecodePaddingMode::Indifferent)
.with_decode_allow_trailing_bits(true);
GeneralPurpose::new(&alphabet, config)
});
pub(crate) fn encode_utf7(input: &str) -> String {
let engine = &*IMAP_B64_ENGINE;
let mut out = String::with_capacity(input.len());
let mut utf16_buf: Vec<u8> = Vec::new();
for ch in input.chars() {
if ch == '&' {
flush_utf16(engine, &mut utf16_buf, &mut out);
out.push_str("&-");
} else if ch.is_ascii() && (0x20..=0x7E).contains(&(ch as u32)) {
flush_utf16(engine, &mut utf16_buf, &mut out);
out.push(ch);
} else {
let mut u16_buf = [0u16; 2];
let encoded = ch.encode_utf16(&mut u16_buf);
for code_unit in encoded.iter() {
utf16_buf.extend_from_slice(&code_unit.to_be_bytes());
}
}
}
flush_utf16(engine, &mut utf16_buf, &mut out);
out
}
fn flush_utf16(engine: &GeneralPurpose, utf16_buf: &mut Vec<u8>, out: &mut String) {
if utf16_buf.is_empty() {
return;
}
out.push('&');
out.push_str(&engine.encode(&utf16_buf));
out.push('-');
utf16_buf.clear();
}
pub(crate) fn decode_utf7(input: &[u8]) -> String {
let engine = &*IMAP_B64_ENGINE;
let mut out = String::with_capacity(input.len());
let mut i = 0;
while i < input.len() {
if input[i] == b'&' {
i += 1;
if i < input.len() && input[i] == b'-' {
out.push('&');
i += 1;
} else {
let start = i;
while i < input.len() && input[i] != b'-' {
i += 1;
}
let b64_slice = &input[start..i];
if b64_slice.is_empty() && i >= input.len() {
out.push('&');
continue;
}
if i < input.len() {
i += 1; }
if let Ok(utf16_bytes) = engine.decode(b64_slice) {
decode_utf16be(&utf16_bytes, &mut out);
} else {
out.push('&');
out.push_str(&String::from_utf8_lossy(b64_slice));
out.push('-');
}
}
} else if input[i] >= 0x80 {
let start = i;
while i < input.len() && input[i] >= 0x80 {
i += 1;
}
let raw = &input[start..i];
match std::str::from_utf8(raw) {
Ok(s) => out.push_str(s),
Err(_) => {
out.push_str(&String::from_utf8_lossy(raw));
}
}
} else if input[i] >= 0x20 && input[i] <= 0x7E {
out.push(input[i] as char);
i += 1;
} else {
out.push('\u{FFFD}');
i += 1;
}
}
out
}
fn decode_utf16be(bytes: &[u8], out: &mut String) {
let has_trailing = bytes.len() % 2 != 0;
for result in char::decode_utf16(bytes.chunks(2).filter_map(|chunk| {
if chunk.len() == 2 {
Some(u16::from_be_bytes([chunk[0], chunk[1]]))
} else {
None
}
})) {
match result {
Ok(ch) => out.push(ch),
Err(_) => out.push('\u{FFFD}'),
}
}
if has_trailing {
out.push('\u{FFFD}');
}
}
#[cfg(test)]
#[path = "utf7_tests.rs"]
mod tests;