Initial commit

This commit is contained in:
Maurus Decimus
2026-05-29 18:02:15 +02:00
commit 576073f8c9
263 changed files with 78638 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use super::error::ImapError;
const MODIFIED_UTF7_ALPHABET: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
pub fn canonicalise_inbox(name: &str) -> String {
if name.eq_ignore_ascii_case("INBOX") {
"INBOX".to_owned()
} else {
name.to_owned()
}
}
pub fn decode_mailbox_name(input: &str) -> Result<String, ImapError> {
decode_mailbox_name_with(input, false)
}
pub fn decode_mailbox_name_with(input: &str, utf8_accept: bool) -> Result<String, ImapError> {
if utf8_accept {
return Ok(input.to_owned());
}
if input.is_ascii() && !input.contains('&') {
return Ok(input.to_owned());
}
let bytes = input.as_bytes();
let mut out = String::with_capacity(input.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'&' {
out.push(bytes[i] as char);
i += 1;
continue;
}
if i + 1 < bytes.len() && bytes[i + 1] == b'-' {
out.push('&');
i += 2;
continue;
}
let mut end = i + 1;
while end < bytes.len() && bytes[end] != b'-' {
end += 1;
}
if end == bytes.len() {
return Err(ImapError::Parse(
"unterminated modified UTF-7 sequence".into(),
));
}
let encoded = &bytes[i + 1..end];
let decoded = decode_b64_modified(encoded)?;
let u16s: Vec<u16> = decoded
.chunks(2)
.filter(|c| c.len() == 2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
let s = String::from_utf16(&u16s).map_err(|e| ImapError::Parse(format!("utf-16: {e}")))?;
out.push_str(&s);
i = end + 1;
}
Ok(out)
}
pub fn encode_mailbox_name(input: &str) -> String {
encode_mailbox_name_with(input, false)
}
pub fn encode_mailbox_name_with(input: &str, utf8_accept: bool) -> String {
if utf8_accept {
return input.to_owned();
}
let mut out = String::with_capacity(input.len());
let mut pending: Vec<u16> = Vec::new();
let flush = |pending: &mut Vec<u16>, out: &mut String| {
if pending.is_empty() {
return;
}
out.push('&');
let mut raw = Vec::with_capacity(pending.len() * 2);
for w in pending.iter() {
raw.extend_from_slice(&w.to_be_bytes());
}
out.push_str(&encode_b64_modified(&raw));
out.push('-');
pending.clear();
};
for c in input.chars() {
let cp = c as u32;
if c == '&' {
flush(&mut pending, &mut out);
out.push_str("&-");
} else if (0x20..=0x7E).contains(&cp) {
flush(&mut pending, &mut out);
out.push(c);
} else {
let mut buf = [0u16; 2];
let units = c.encode_utf16(&mut buf);
pending.extend_from_slice(units);
}
}
flush(&mut pending, &mut out);
out
}
fn decode_b64_modified(bytes: &[u8]) -> Result<Vec<u8>, ImapError> {
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
let mut acc: u32 = 0;
let mut bits: u32 = 0;
for &b in bytes {
let v = match MODIFIED_UTF7_ALPHABET.iter().position(|&x| x == b) {
Some(idx) => idx as u32,
None => {
return Err(ImapError::Parse(format!(
"invalid modified UTF-7 byte {b:#04x}"
)));
}
};
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
let byte = ((acc >> bits) & 0xFF) as u8;
out.push(byte);
}
}
Ok(out)
}
fn encode_b64_modified(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 4 / 3 + 1);
let mut acc: u32 = 0;
let mut bits: u32 = 0;
for &b in bytes {
acc = (acc << 8) | b as u32;
bits += 8;
while bits >= 6 {
bits -= 6;
let idx = ((acc >> bits) & 0x3F) as usize;
out.push(MODIFIED_UTF7_ALPHABET[idx] as char);
}
}
if bits > 0 {
let idx = ((acc << (6 - bits)) & 0x3F) as usize;
out.push(MODIFIED_UTF7_ALPHABET[idx] as char);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_passes_through_decode() {
assert_eq!(decode_mailbox_name("INBOX").unwrap(), "INBOX");
assert_eq!(decode_mailbox_name("Sent/Items").unwrap(), "Sent/Items");
}
#[test]
fn ampersand_dash_decodes_to_literal_amp() {
assert_eq!(decode_mailbox_name("R&-D").unwrap(), "R&D");
assert_eq!(decode_mailbox_name("&-").unwrap(), "&");
}
#[test]
fn non_ascii_decode_japanese() {
let decoded = decode_mailbox_name("~peter/mail/&ZeVnLIqe-/&U,BTFw-").unwrap();
assert_eq!(decoded, "~peter/mail/日本語/台北");
}
#[test]
fn unterminated_sequence_errors() {
let err = decode_mailbox_name("&ZeVnLIqe").unwrap_err();
assert!(matches!(err, ImapError::Parse(_)));
}
#[test]
fn ascii_passes_through_encode() {
assert_eq!(encode_mailbox_name("INBOX"), "INBOX");
assert_eq!(encode_mailbox_name("Sent Items"), "Sent Items");
}
#[test]
fn literal_amp_encodes_to_amp_dash() {
assert_eq!(encode_mailbox_name("R&D"), "R&-D");
assert_eq!(encode_mailbox_name("&"), "&-");
}
#[test]
fn non_ascii_encode_japanese() {
let encoded = encode_mailbox_name("~peter/mail/日本語/台北");
assert_eq!(encoded, "~peter/mail/&ZeVnLIqe-/&U,BTFw-");
}
#[test]
fn encode_decode_roundtrip_various() {
for s in [
"INBOX",
"Sent Mail",
"R&D",
"日本語",
"Hé llo",
"INBOX.Projects.Alpha",
"Junk E-mail",
] {
let enc = encode_mailbox_name(s);
let dec = decode_mailbox_name(&enc).unwrap();
assert_eq!(dec, s, "roundtrip failed for {s:?}, enc={enc:?}");
}
}
#[test]
fn inbox_canonical_uppercase() {
assert_eq!(canonicalise_inbox("inbox"), "INBOX");
assert_eq!(canonicalise_inbox("Inbox"), "INBOX");
assert_eq!(canonicalise_inbox("INBOX"), "INBOX");
assert_eq!(canonicalise_inbox("Inbox/Sub"), "Inbox/Sub");
}
#[test]
fn utf8_accept_skips_mutf7_decode_so_ampersand_passes_through() {
assert_eq!(decode_mailbox_name_with("R&D", true).unwrap(), "R&D");
assert_eq!(decode_mailbox_name_with("日本語", true).unwrap(), "日本語");
}
#[test]
fn utf8_accept_skips_mutf7_encode_so_input_passes_through() {
assert_eq!(encode_mailbox_name_with("日本語", true), "日本語");
assert_eq!(encode_mailbox_name_with("R&D", true), "R&D");
}
}