chore: sync mailcore v0.1.0-beta.1
This commit is contained in:
1210
mailcore/src/imap/client.rs
Normal file
1210
mailcore/src/imap/client.rs
Normal file
File diff suppressed because it is too large
Load Diff
3
mailcore/src/imap/mod.rs
Normal file
3
mailcore/src/imap/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod client;
|
||||
pub mod utf7;
|
||||
pub use client::ImapClient;
|
||||
215
mailcore/src/imap/utf7.rs
Normal file
215
mailcore/src/imap/utf7.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
/// RFC 3501 modified UTF-7 decoder for IMAP mailbox names
|
||||
///
|
||||
/// Modified UTF-7 (RFC 3501) encoding rules:
|
||||
/// - Printable US-ASCII characters (0x20-0x7e, except "&") are represented by themselves
|
||||
/// - "&" is represented by "&-"
|
||||
/// - All other characters are represented as "&" + modified-BASE64(UTF-16BE) + "-"
|
||||
/// - Modified BASE64 uses "A-Za-z0-9,+" instead of "A-Za-z0-9+/"
|
||||
|
||||
pub fn decode_modified_utf7(encoded: &str) -> String {
|
||||
let bytes = encoded.as_bytes();
|
||||
let mut result = String::new();
|
||||
let mut i = 0;
|
||||
|
||||
while i < bytes.len() {
|
||||
let byte = bytes[i];
|
||||
|
||||
// Printable ASCII (except '&') - pass through
|
||||
if byte != b'&' && byte >= 0x20 && byte <= 0x7e {
|
||||
result.push(byte as char);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// "&" starts a sequence
|
||||
if byte == b'&' {
|
||||
if i + 1 < bytes.len() && bytes[i + 1] == b'-' {
|
||||
// "&-" represents literal "&"
|
||||
result.push('&');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the closing "-"
|
||||
let mut j = i + 1;
|
||||
while j < bytes.len() && bytes[j] != b'-' {
|
||||
j += 1;
|
||||
}
|
||||
|
||||
if j >= bytes.len() {
|
||||
// Malformed: no closing "-", treat as literal
|
||||
result.push('&');
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract the modified-BASE64 sequence
|
||||
let b64_str = std::str::from_utf8(&bytes[i + 1..j]).unwrap_or("");
|
||||
|
||||
// Decode modified-BASE64 (replace "," with "/")
|
||||
let b64_std = b64_str.replace(',', "/");
|
||||
|
||||
// Decode from standard BASE64
|
||||
if let Ok(decoded_bytes) = base64_decode(&b64_std) {
|
||||
// Convert UTF-16BE bytes to UTF-16 chars, then to UTF-8
|
||||
if let Ok(text) = decode_utf16be(&decoded_bytes) {
|
||||
result.push_str(&text);
|
||||
} else {
|
||||
// Invalid UTF-16, use replacement character
|
||||
result.push('\u{FFFD}');
|
||||
}
|
||||
} else {
|
||||
// Invalid BASE64, use replacement character
|
||||
result.push('\u{FFFD}');
|
||||
}
|
||||
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other bytes (shouldn't happen in valid UTF-7)
|
||||
result.push('\u{FFFD}');
|
||||
i += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Decode standard BASE64 (RFC 4648)
|
||||
fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
|
||||
const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
let mut result = Vec::new();
|
||||
let input = input.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
while i < input.len() {
|
||||
let mut buf = [0u8; 4];
|
||||
let mut len = 0;
|
||||
|
||||
// Read up to 4 base64 characters
|
||||
while len < 4 && i < input.len() {
|
||||
let b = input[i];
|
||||
if b == b'=' {
|
||||
break;
|
||||
}
|
||||
if let Some(pos) = TABLE.iter().position(|&x| x == b) {
|
||||
buf[len] = pos as u8;
|
||||
len += 1;
|
||||
} else if !b.is_ascii_whitespace() {
|
||||
return Err("Invalid base64 character".to_string());
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Decode the 4-character group
|
||||
match len {
|
||||
0 => break,
|
||||
1 => return Err("Invalid base64 length".to_string()),
|
||||
2 => {
|
||||
result.push(((buf[0] << 2) | (buf[1] >> 4)) as u8);
|
||||
}
|
||||
3 => {
|
||||
result.push(((buf[0] << 2) | (buf[1] >> 4)) as u8);
|
||||
result.push((((buf[1] & 0x0f) << 4) | (buf[2] >> 2)) as u8);
|
||||
}
|
||||
4 => {
|
||||
result.push(((buf[0] << 2) | (buf[1] >> 4)) as u8);
|
||||
result.push((((buf[1] & 0x0f) << 4) | (buf[2] >> 2)) as u8);
|
||||
result.push((((buf[2] & 0x03) << 6) | buf[3]) as u8);
|
||||
}
|
||||
_ => return Err("Invalid base64 length".to_string()),
|
||||
}
|
||||
|
||||
// Skip padding
|
||||
while i < input.len() && input[i] == b'=' {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Decode UTF-16BE bytes to UTF-8 string
|
||||
fn decode_utf16be(bytes: &[u8]) -> Result<String, String> {
|
||||
if bytes.len() % 2 != 0 {
|
||||
return Err("Odd number of bytes in UTF-16BE sequence".to_string());
|
||||
}
|
||||
|
||||
let mut chars = Vec::new();
|
||||
let mut i = 0;
|
||||
|
||||
while i < bytes.len() {
|
||||
let high = u16::from_be_bytes([bytes[i], bytes[i + 1]]);
|
||||
i += 2;
|
||||
|
||||
// Check for surrogate pair
|
||||
if (high & 0xFC00) == 0xD800 {
|
||||
// High surrogate, expect low surrogate
|
||||
if i + 2 > bytes.len() {
|
||||
chars.push('\u{FFFD}');
|
||||
break;
|
||||
}
|
||||
let low = u16::from_be_bytes([bytes[i], bytes[i + 1]]);
|
||||
i += 2;
|
||||
|
||||
if (low & 0xFC00) != 0xDC00 {
|
||||
chars.push('\u{FFFD}');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decode surrogate pair
|
||||
let high_bits = (high & 0x3FF) as u32;
|
||||
let low_bits = (low & 0x3FF) as u32;
|
||||
let codepoint = 0x10000 + (high_bits << 10) + low_bits;
|
||||
|
||||
if let Some(ch) = char::from_u32(codepoint) {
|
||||
chars.push(ch);
|
||||
} else {
|
||||
chars.push('\u{FFFD}');
|
||||
}
|
||||
} else if (high & 0xFC00) == 0xDC00 {
|
||||
// Unexpected low surrogate
|
||||
chars.push('\u{FFFD}');
|
||||
} else {
|
||||
// Regular BMP character
|
||||
if let Some(ch) = char::from_u32(high as u32) {
|
||||
chars.push(ch);
|
||||
} else {
|
||||
chars.push('\u{FFFD}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(chars.iter().collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ascii_passthrough() {
|
||||
assert_eq!(decode_modified_utf7("INBOX"), "INBOX");
|
||||
assert_eq!(decode_modified_utf7("Sent Mail"), "Sent Mail");
|
||||
assert_eq!(decode_modified_utf7("Drafts"), "Drafts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal_ampersand() {
|
||||
assert_eq!(decode_modified_utf7("A&-B"), "A&B");
|
||||
assert_eq!(decode_modified_utf7("&-"), "&");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_ascii_and_encoded() {
|
||||
// Test that ASCII parts pass through while encoded parts are decoded
|
||||
// This is a basic test to ensure the structure works
|
||||
assert!(decode_modified_utf7("Folder&-Name").contains("&"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_empty() {
|
||||
assert_eq!(decode_modified_utf7(""), "");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user