IMAP import failed with "LIST mailbox name missing" when a mailbox name is a purely numeric unquoted atom (closes #26)

This commit is contained in:
Maurus Decimus
2026-07-26 17:28:48 +02:00
parent 846fc88f7e
commit fe16f2b2fe
5 changed files with 194 additions and 128 deletions
+36
View File
@@ -225,6 +225,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
let name_val = self.parse_value()?;
let name = match name_val {
Value::Str(s) | Value::Atom(s) => s,
Value::Number(n) => n.to_string(),
Value::Bytes(b) => String::from_utf8_lossy(&b).into_owned(),
_ => return Err(ImapError::Parse("LIST mailbox name missing".into())),
};
@@ -246,6 +247,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
self.skip_ws();
let mailbox = match self.parse_value()? {
Value::Str(s) | Value::Atom(s) => s,
Value::Number(n) => n.to_string(),
Value::Bytes(b) => String::from_utf8_lossy(&b).into_owned(),
_ => return Err(ImapError::Parse("STATUS mailbox missing".into())),
};
@@ -862,6 +864,26 @@ mod tests {
}
}
#[test]
fn untagged_list_with_numeric_atom_name() {
let r = parse(b"* LIST (\\Subscribed \\HasNoChildren \\UnMarked) \"/\" 200\r\n");
match r {
Response::Untagged(Untagged::List {
attributes,
delimiter,
name,
}) => {
assert_eq!(
attributes,
vec!["\\Subscribed", "\\HasNoChildren", "\\UnMarked"]
);
assert_eq!(delimiter, Some('/'));
assert_eq!(name, "200");
}
_ => panic!("expected List"),
}
}
#[test]
fn untagged_status() {
let r = parse(b"* STATUS INBOX (UIDVALIDITY 12345 UIDNEXT 42 MESSAGES 7)\r\n");
@@ -876,6 +898,20 @@ mod tests {
}
}
#[test]
fn untagged_status_with_numeric_atom_mailbox() {
let r = parse(b"* STATUS 200 (MESSAGES 5 UIDNEXT 6 UIDVALIDITY 1778253936)\r\n");
match r {
Response::Untagged(Untagged::Status { mailbox, items }) => {
assert_eq!(mailbox, "200");
assert_eq!(items.get("MESSAGES"), Some(&5));
assert_eq!(items.get("UIDNEXT"), Some(&6));
assert_eq!(items.get("UIDVALIDITY"), Some(&1778253936));
}
_ => panic!("expected Status"),
}
}
#[test]
fn untagged_search_with_uids() {
let r = parse(b"* SEARCH 1 3 5 7\r\n");
+17 -3
View File
@@ -89,7 +89,14 @@ impl Logger {
eprintln!("{proto} {command} -> {status} ({} ms)", elapsed.as_millis());
}
pub fn trace_http_error(&self, proto: &str, method: &str, url: &str, error: &str, elapsed: Duration) {
pub fn trace_http_error(
&self,
proto: &str,
method: &str,
url: &str,
error: &str,
elapsed: Duration,
) {
if !self.enabled(LEVEL_METHOD) {
return;
}
@@ -222,7 +229,10 @@ mod tests {
#[test]
fn redact_wire_hides_login_arguments() {
assert_eq!(redact_wire("LOGIN \"alice\" \"s3cret\""), "LOGIN <redacted>");
assert_eq!(
redact_wire("LOGIN \"alice\" \"s3cret\""),
"LOGIN <redacted>"
);
}
#[test]
@@ -264,7 +274,11 @@ mod tests {
fn body_for_log_caps_long_text_without_scanning_whole_blob() {
let body = vec![b'a'; TRACE_BODY_CAP * 4];
let out = body_for_log(&body, Some("text/plain"));
assert!(out.contains("bytes total>"), "got tail {}", &out[out.len() - 40..]);
assert!(
out.contains("bytes total>"),
"got tail {}",
&out[out.len() - 40..]
);
assert!(out.len() < body.len());
}
+6 -2
View File
@@ -151,8 +151,12 @@ impl SieveClient {
let started = Instant::now();
self.write_all(command.as_bytes())?;
let block = read_response(&mut self.reader)?;
self.logger
.trace_cmd("SIEVE", command, &sieve_status(&block.status), started.elapsed());
self.logger.trace_cmd(
"SIEVE",
command,
&sieve_status(&block.status),
started.elapsed(),
);
finish_block(block, &mut self.closed)
}