From 14e3b3a85210c9e196801a739ec17b1a04b8e063 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Mon, 27 Jan 2025 19:21:36 +0100 Subject: [PATCH] Hashify imap-proto --- Cargo.lock | 13 +- crates/imap-proto/Cargo.toml | 1 + crates/imap-proto/src/parser/authenticate.rs | 46 +- crates/imap-proto/src/parser/create.rs | 54 +- crates/imap-proto/src/parser/enable.rs | 28 +- crates/imap-proto/src/parser/fetch.rs | 520 +++++++++-------- crates/imap-proto/src/parser/list.rs | 42 +- crates/imap-proto/src/parser/mod.rs | 164 +++--- crates/imap-proto/src/parser/search.rs | 584 +++++++++++-------- crates/imap-proto/src/parser/sort.rs | 33 +- crates/imap-proto/src/parser/status.rs | 37 +- crates/imap-proto/src/parser/store.rs | 30 +- crates/imap-proto/src/parser/thread.rs | 16 +- 13 files changed, 822 insertions(+), 746 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57f4260a..135152b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2699,9 +2699,9 @@ dependencies = [ [[package]] name = "hashify" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5772d10fb6ad55ec11f3ec9ae4e39c626a340cf996750689d8fd54938493acf1" +checksum = "0a194e6d22f060dada750b0c33c9c3d01dce0ba5212ea81e0a46834e065e932c" dependencies = [ "proc-macro2", "quote", @@ -3298,6 +3298,7 @@ version = "0.11.2" dependencies = [ "ahash 0.8.11", "chrono", + "hashify", "jmap_proto", "mail-parser", "store", @@ -5032,9 +5033,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.80" +version = "2.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ff66fe75e86ef6bb57a5e7c1af22cc3ff5368ec610559609ea08e304d7c772" +checksum = "5871e872678223987b84739333bf13e42f0c1fb102e30bba8dcdf1340d0bbcc9" dependencies = [ "psl-types", ] @@ -5972,9 +5973,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ "web-time", ] diff --git a/crates/imap-proto/Cargo.toml b/crates/imap-proto/Cargo.toml index 8b159895..03eea7f9 100644 --- a/crates/imap-proto/Cargo.toml +++ b/crates/imap-proto/Cargo.toml @@ -11,6 +11,7 @@ mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] ahash = { version = "0.8" } chrono = { version = "0.4"} trc = { path = "../trc" } +hashify = { version = "0.2" } [dev-dependencies] tokio = { version = "1.23", features = ["full"] } diff --git a/crates/imap-proto/src/parser/authenticate.rs b/crates/imap-proto/src/parser/authenticate.rs index a8d48875..5e111126 100644 --- a/crates/imap-proto/src/parser/authenticate.rs +++ b/crates/imap-proto/src/parser/authenticate.rs @@ -30,37 +30,27 @@ impl Request { impl Mechanism { pub fn parse(value: &[u8]) -> super::Result { - if value.eq_ignore_ascii_case(b"PLAIN") { - Ok(Self::Plain) - } else if value.eq_ignore_ascii_case(b"CRAM-MD5") { - Ok(Self::CramMd5) - } else if value.eq_ignore_ascii_case(b"DIGEST-MD5") { - Ok(Self::DigestMd5) - } else if value.eq_ignore_ascii_case(b"SCRAM-SHA-1") { - Ok(Self::ScramSha1) - } else if value.eq_ignore_ascii_case(b"SCRAM-SHA-256") { - Ok(Self::ScramSha256) - } else if value.eq_ignore_ascii_case(b"APOP") { - Ok(Self::Apop) - } else if value.eq_ignore_ascii_case(b"NTLM") { - Ok(Self::Ntlm) - } else if value.eq_ignore_ascii_case(b"GSSAPI") { - Ok(Self::Gssapi) - } else if value.eq_ignore_ascii_case(b"ANONYMOUS") { - Ok(Self::Anonymous) - } else if value.eq_ignore_ascii_case(b"EXTERNAL") { - Ok(Self::External) - } else if value.eq_ignore_ascii_case(b"OAUTHBEARER") { - Ok(Self::OAuthBearer) - } else if value.eq_ignore_ascii_case(b"XOAUTH2") { - Ok(Self::XOauth2) - } else { - Err(format!( + hashify::tiny_map_ignore_case!(value, + "PLAIN" => Self::Plain, + "CRAM-MD5" => Self::CramMd5, + "DIGEST-MD5" => Self::DigestMd5, + "SCRAM-SHA-1" => Self::ScramSha1, + "SCRAM-SHA-256" => Self::ScramSha256, + "APOP" => Self::Apop, + "NTLM" => Self::Ntlm, + "GSSAPI" => Self::Gssapi, + "ANONYMOUS" => Self::Anonymous, + "EXTERNAL" => Self::External, + "OAUTHBEARER" => Self::OAuthBearer, + "XOAUTH2" => Self::XOauth2, + ) + .ok_or_else(|| { + format!( "Unsupported mechanism '{}'.", String::from_utf8_lossy(value) ) - .into()) - } + .into() + }) } } diff --git a/crates/imap-proto/src/parser/create.rs b/crates/imap-proto/src/parser/create.rs index 683c5eb2..afa71a48 100644 --- a/crates/imap-proto/src/parser/create.rs +++ b/crates/imap-proto/src/parser/create.rs @@ -38,32 +38,34 @@ impl Request { } match tokens.next() { Some(Token::Argument(value)) => { - Some(if value.eq_ignore_ascii_case(b"\\Archive") { - "archive" - } else if value.eq_ignore_ascii_case(b"\\Drafts") { - "drafts" - } else if value.eq_ignore_ascii_case(b"\\Junk") { - "junk" - } else if value.eq_ignore_ascii_case(b"\\Sent") { - "sent" - } else if value.eq_ignore_ascii_case(b"\\Trash") { - "trash" - } else if value.eq_ignore_ascii_case(b"\\Important") { - "important" - } else if value.eq_ignore_ascii_case(b"\\All") { - return Err(bad( - self.tag, - "A mailbox with the \"\\All\" attribute already exists.", - )); - } else { - return Err(bad( - self.tag, - format!( - "Special use attribute {:?} is not supported.", - String::from_utf8_lossy(&value) - ), - )); - }) + let r = hashify::tiny_map_ignore_case!(value.as_slice(), + "\\Archive" => Some("archive"), + "\\Drafts" => Some("drafts"), + "\\Junk" => Some("junk"), + "\\Sent" => Some("sent"), + "\\Trash" => Some("trash"), + "\\Important" => Some("important"), + "\\All" => None, + ); + + match r { + Some(Some(tag)) => Some(tag), + Some(None) => { + return Err(bad( + self.tag, + "A mailbox with the \"\\All\" attribute already exists.", + )) + } + None => { + return Err(bad( + self.tag, + format!( + "Special use attribute {:?} is not supported.", + String::from_utf8_lossy(&value) + ), + )); + } + } } _ => { return Err(bad(self.tag, "Invalid SPECIAL-USE attribute.")); diff --git a/crates/imap-proto/src/parser/enable.rs b/crates/imap-proto/src/parser/enable.rs index 30bdb927..72c6ece9 100644 --- a/crates/imap-proto/src/parser/enable.rs +++ b/crates/imap-proto/src/parser/enable.rs @@ -33,25 +33,21 @@ impl Request { impl Capability { pub fn parse(value: &[u8]) -> super::Result { - if value.eq_ignore_ascii_case(b"IMAP4rev2") { - Ok(Self::IMAP4rev2) - } else if value.eq_ignore_ascii_case(b"STARTTLS") { - Ok(Self::StartTLS) - } else if value.eq_ignore_ascii_case(b"LOGINDISABLED") { - Ok(Self::LoginDisabled) - } else if value.eq_ignore_ascii_case(b"CONDSTORE") { - Ok(Self::CondStore) - } else if value.eq_ignore_ascii_case(b"QRESYNC") { - Ok(Self::QResync) - } else if value.eq_ignore_ascii_case(b"UTF8=ACCEPT") { - Ok(Self::Utf8Accept) - } else { - Err(format!( + hashify::tiny_map_ignore_case!(value, + "IMAP4rev2" => Self::IMAP4rev2, + "STARTTLS" => Self::StartTLS, + "LOGINDISABLED" => Self::LoginDisabled, + "CONDSTORE" => Self::CondStore, + "QRESYNC" => Self::QResync, + "UTF8=ACCEPT" => Self::Utf8Accept, + ) + .ok_or_else(|| { + format!( "Unsupported capability '{}'.", String::from_utf8_lossy(value) ) - .into()) - } + .into() + }) } } diff --git a/crates/imap-proto/src/parser/fetch.rs b/crates/imap-proto/src/parser/fetch.rs index 52c2ec5f..88f9f470 100644 --- a/crates/imap-proto/src/parser/fetch.rs +++ b/crates/imap-proto/src/parser/fetch.rs @@ -38,282 +38,306 @@ impl Request { while let Some(token) = tokens.next() { match token { Token::Argument(value) => { - if value.eq_ignore_ascii_case(b"ALL") { - attributes = vec![ - Attribute::Flags, - Attribute::InternalDate, - Attribute::Rfc822Size, - Attribute::Envelope, - ]; - break; - } else if value.eq_ignore_ascii_case(b"FULL") { - attributes = vec![ - Attribute::Flags, - Attribute::InternalDate, - Attribute::Rfc822Size, - Attribute::Envelope, - Attribute::Body, - ]; - break; - } else if value.eq_ignore_ascii_case(b"FAST") { - attributes = vec![ - Attribute::Flags, - Attribute::InternalDate, - Attribute::Rfc822Size, - ]; - break; - } else if value.eq_ignore_ascii_case(b"ENVELOPE") { - attributes.push_unique(Attribute::Envelope); - } else if value.eq_ignore_ascii_case(b"FLAGS") { - attributes.push_unique(Attribute::Flags); - } else if value.eq_ignore_ascii_case(b"INTERNALDATE") { - attributes.push_unique(Attribute::InternalDate); - } else if value.eq_ignore_ascii_case(b"BODYSTRUCTURE") { - attributes.push_unique(Attribute::BodyStructure); - } else if value.eq_ignore_ascii_case(b"UID") { - attributes.push_unique(Attribute::Uid); - } else if value.eq_ignore_ascii_case(b"RFC822") { - attributes.push_unique( - if tokens.peek().is_some_and(|token| token.is_dot()) { - tokens.next(); - let rfc822 = tokens - .next() - .ok_or_else(|| { - bad(self.tag.to_string(), "Missing RFC822 parameter.") - })? - .unwrap_bytes(); - if rfc822.eq_ignore_ascii_case(b"HEADER") { - Attribute::Rfc822Header - } else if rfc822.eq_ignore_ascii_case(b"SIZE") { - Attribute::Rfc822Size - } else if rfc822.eq_ignore_ascii_case(b"TEXT") { - Attribute::Rfc822Text + let attr_len = attributes.len(); + hashify::fnc_map_ignore_case!(value.as_slice(), + "ALL" => { + attributes = vec![ + Attribute::Flags, + Attribute::InternalDate, + Attribute::Rfc822Size, + Attribute::Envelope, + ]; + break; + }, + "FULL" => { + attributes = vec![ + Attribute::Flags, + Attribute::InternalDate, + Attribute::Rfc822Size, + Attribute::Envelope, + Attribute::Body, + ]; + break; + }, + "FAST" => { + attributes = vec![ + Attribute::Flags, + Attribute::InternalDate, + Attribute::Rfc822Size, + ]; + break; + }, + "ENVELOPE" => { + attributes.push_unique(Attribute::Envelope); + }, + "FLAGS" => { + attributes.push_unique(Attribute::Flags); + }, + "INTERNALDATE" => { + attributes.push_unique(Attribute::InternalDate); + }, + "BODYSTRUCTURE" => { + attributes.push_unique(Attribute::BodyStructure); + }, + "UID" => { + attributes.push_unique(Attribute::Uid); + }, + "RFC822" => { + attributes.push_unique( + if tokens.peek().is_some_and(|token| token.is_dot()) { + tokens.next(); + let rfc822 = tokens + .next() + .ok_or_else(|| { + bad(self.tag.to_string(), "Missing RFC822 parameter.") + })? + .unwrap_bytes(); + if rfc822.eq_ignore_ascii_case(b"HEADER") { + Attribute::Rfc822Header + } else if rfc822.eq_ignore_ascii_case(b"SIZE") { + Attribute::Rfc822Size + } else if rfc822.eq_ignore_ascii_case(b"TEXT") { + Attribute::Rfc822Text + } else { + return Err(bad( + self.tag, + format!( + "Invalid RFC822 parameter {:?}.", + String::from_utf8_lossy(&rfc822) + ), + )); + } } else { - return Err(bad( - self.tag, - format!( - "Invalid RFC822 parameter {:?}.", - String::from_utf8_lossy(&rfc822) - ), - )); + Attribute::Rfc822 + }, + ); + }, + "BODY" => { + let is_peek = match tokens.peek() { + Some(Token::BracketOpen) => { + tokens.next(); + false } - } else { - Attribute::Rfc822 - }, - ); - } else if value.eq_ignore_ascii_case(b"BODY") { - let is_peek = match tokens.peek() { - Some(Token::BracketOpen) => { - tokens.next(); - false - } - Some(Token::Dot) => { - tokens.next(); - if tokens - .next() - .map_or(true, |token| !token.eq_ignore_ascii_case(b"PEEK")) - { - return Err(bad( - self.tag.clone(), - "Expected 'PEEK' after '.'.", - )); + Some(Token::Dot) => { + tokens.next(); + if tokens + .next() + .map_or(true, |token| !token.eq_ignore_ascii_case(b"PEEK")) + { + return Err(bad( + self.tag.clone(), + "Expected 'PEEK' after '.'.", + )); + } + if tokens.next().map_or(true, |token| !token.is_bracket_open()) { + return Err(bad( + self.tag.clone(), + "Expected '[' after 'BODY.PEEK'", + )); + } + true } - if tokens.next().map_or(true, |token| !token.is_bracket_open()) { - return Err(bad( - self.tag.clone(), - "Expected '[' after 'BODY.PEEK'", - )); - } - true - } - _ => { - attributes.push_unique(Attribute::Body); - continue; - } - }; + _ => { + attributes.push_unique(Attribute::Body); - // Parse section-spect - let mut sections = Vec::new(); - while let Some(token) = tokens.next() { - match token { - Token::BracketClose => break, - Token::Argument(value) => { - let section = if value.eq_ignore_ascii_case(b"HEADER") { - if let Some(Token::Dot) = tokens.peek() { - tokens.next(); - if tokens.next().map_or(true, |token| { - !token.eq_ignore_ascii_case(b"FIELDS") - }) { - return Err(bad( - self.tag, - "Expected 'FIELDS' after 'HEADER.'.", - )); - } - let is_not = if let Some(Token::Dot) = tokens.peek() { + if !in_parentheses { + break; + } else { + continue; + } + } + }; + + // Parse section-spect + let mut sections = Vec::new(); + while let Some(token) = tokens.next() { + match token { + Token::BracketClose => break, + Token::Argument(value) => { + let section = if value.eq_ignore_ascii_case(b"HEADER") { + if let Some(Token::Dot) = tokens.peek() { tokens.next(); if tokens.next().map_or(true, |token| { - !token.eq_ignore_ascii_case(b"NOT") + !token.eq_ignore_ascii_case(b"FIELDS") }) { return Err(bad( self.tag, - "Expected 'NOT' after 'HEADER.FIELDS.'.", + "Expected 'FIELDS' after 'HEADER.'.", )); } - true - } else { - false - }; - if tokens - .next() - .map_or(true, |token| !token.is_parenthesis_open()) - { - return Err(bad( - self.tag, - "Expected '(' after 'HEADER.FIELDS'.", - )); - } - let mut fields = Vec::new(); - while let Some(token) = tokens.next() { - match token { - Token::ParenthesisClose => break, - Token::Argument(value) => { - fields.push(String::from_utf8(value).map_err( - |_| bad(self.tag.clone(), "Invalid UTF-8 in header field name."), - )?); - } - _ => { + let is_not = if let Some(Token::Dot) = tokens.peek() { + tokens.next(); + if tokens.next().map_or(true, |token| { + !token.eq_ignore_ascii_case(b"NOT") + }) { return Err(bad( self.tag, - "Expected field name.", - )) + "Expected 'NOT' after 'HEADER.FIELDS.'.", + )); + } + true + } else { + false + }; + if tokens + .next() + .map_or(true, |token| !token.is_parenthesis_open()) + { + return Err(bad( + self.tag, + "Expected '(' after 'HEADER.FIELDS'.", + )); + } + let mut fields = Vec::new(); + while let Some(token) = tokens.next() { + match token { + Token::ParenthesisClose => break, + Token::Argument(value) => { + fields.push(String::from_utf8(value).map_err( + |_| bad(self.tag.clone(), "Invalid UTF-8 in header field name."), + )?); + } + _ => { + return Err(bad( + self.tag, + "Expected field name.", + )) + } } } + Section::HeaderFields { + not: is_not, + fields, + } + } else { + Section::Header } - Section::HeaderFields { - not: is_not, - fields, - } + } else if value.eq_ignore_ascii_case(b"TEXT") { + Section::Text + } else if value.eq_ignore_ascii_case(b"MIME") { + Section::Mime } else { - Section::Header - } - } else if value.eq_ignore_ascii_case(b"TEXT") { - Section::Text - } else if value.eq_ignore_ascii_case(b"MIME") { - Section::Mime - } else { - Section::Part { - num: parse_number::(&value) - .map_err(|v| bad(self.tag.to_string(), v))?, - } - }; - sections.push(section); - } - Token::Dot => (), - _ => { - return Err(bad( - self.tag, - format!( - "Invalid token {:?} found in section-spect.", - token - ), - )) + Section::Part { + num: parse_number::(&value) + .map_err(|v| bad(self.tag.to_string(), v))?, + } + }; + sections.push(section); + } + Token::Dot => (), + _ => { + return Err(bad( + self.tag, + format!( + "Invalid token {:?} found in section-spect.", + token + ), + )) + } } } - } - attributes.push_unique(Attribute::BodySection { - peek: is_peek, - sections, - partial: parse_partial(&mut tokens) - .map_err(|v| bad(self.tag.to_string(), v))?, - }); - } else if value.eq_ignore_ascii_case(b"BINARY") { - let (is_peek, is_size) = if let Some(Token::Dot) = tokens.peek() { - tokens.next(); - let param = tokens - .next() - .ok_or({ - bad(self.tag.clone(), "Missing parameter after 'BINARY.'.") - })? - .unwrap_bytes(); - if param.eq_ignore_ascii_case(b"PEEK") { - (true, false) - } else if param.eq_ignore_ascii_case(b"SIZE") { - (false, true) - } else { - return Err(bad( - self.tag, - "Expected 'PEEK' or 'SIZE' after 'BINARY.'.", - )); - } - } else { - (false, false) - }; - - // Parse section-part - if tokens.next().map_or(true, |token| !token.is_bracket_open()) { - return Err(bad(self.tag.to_string(), "Expected '[' after 'BINARY'.")); - } - let mut sections = Vec::new(); - while let Some(token) = tokens.next() { - match token { - Token::Argument(value) => { - sections.push( - parse_number::(&value) - .map_err(|v| bad(self.tag.to_string(), v))?, - ); - } - Token::Dot => (), - Token::BracketClose => break, - _ => { - return Err(bad( - self.tag, - format!( - "Expected part section integer, got {:?}.", - token.to_string() - ), - )) - } - } - } - attributes.push_unique(if !is_size { - Attribute::Binary { + attributes.push_unique(Attribute::BodySection { peek: is_peek, sections, partial: parse_partial(&mut tokens) .map_err(|v| bad(self.tag.to_string(), v))?, - } - } else { - Attribute::BinarySize { sections } - }); - } else if value.eq_ignore_ascii_case(b"PREVIEW") { - attributes.push_unique(Attribute::Preview { - lazy: if let Some(Token::ParenthesisOpen) = tokens.peek() { + }); + }, + "BINARY" => { + let (is_peek, is_size) = if let Some(Token::Dot) = tokens.peek() { tokens.next(); - let mut is_lazy = false; - while let Some(token) = tokens.next() { - match token { - Token::ParenthesisClose => break, - Token::Argument(value) => { - if value.eq_ignore_ascii_case(b"LAZY") { - is_lazy = true; - } - } - _ => (), + let param = tokens + .next() + .ok_or({ + bad(self.tag.clone(), "Missing parameter after 'BINARY.'.") + })? + .unwrap_bytes(); + if param.eq_ignore_ascii_case(b"PEEK") { + (true, false) + } else if param.eq_ignore_ascii_case(b"SIZE") { + (false, true) + } else { + return Err(bad( + self.tag, + "Expected 'PEEK' or 'SIZE' after 'BINARY.'.", + )); + } + } else { + (false, false) + }; + + // Parse section-part + if tokens.next().map_or(true, |token| !token.is_bracket_open()) { + return Err(bad(self.tag.to_string(), "Expected '[' after 'BINARY'.")); + } + let mut sections = Vec::new(); + while let Some(token) = tokens.next() { + match token { + Token::Argument(value) => { + sections.push( + parse_number::(&value) + .map_err(|v| bad(self.tag.to_string(), v))?, + ); + } + Token::Dot => (), + Token::BracketClose => break, + _ => { + return Err(bad( + self.tag, + format!( + "Expected part section integer, got {:?}.", + token.to_string() + ), + )) } } - is_lazy + } + attributes.push_unique(if !is_size { + Attribute::Binary { + peek: is_peek, + sections, + partial: parse_partial(&mut tokens) + .map_err(|v| bad(self.tag.to_string(), v))?, + } } else { - false - }, - }); - } else if value.eq_ignore_ascii_case(b"MODSEQ") { - attributes.push_unique(Attribute::ModSeq); - } else if value.eq_ignore_ascii_case(b"EMAILID") { - attributes.push_unique(Attribute::EmailId); - } else if value.eq_ignore_ascii_case(b"THREADID") { - attributes.push_unique(Attribute::ThreadId); - } else { + Attribute::BinarySize { sections } + }); + }, + "PREVIEW" => { + attributes.push_unique(Attribute::Preview { + lazy: if let Some(Token::ParenthesisOpen) = tokens.peek() { + tokens.next(); + let mut is_lazy = false; + while let Some(token) = tokens.next() { + match token { + Token::ParenthesisClose => break, + Token::Argument(value) => { + if value.eq_ignore_ascii_case(b"LAZY") { + is_lazy = true; + } + } + _ => (), + } + } + is_lazy + } else { + false + }, + }); + }, + "MODSEQ" => { + attributes.push_unique(Attribute::ModSeq); + }, + "EMAILID" => { + attributes.push_unique(Attribute::EmailId); + }, + "THREADID" => { + attributes.push_unique(Attribute::ThreadId); + }, + ); + + if attr_len == attributes.len() { return Err(bad( self.tag, format!("Invalid attribute {:?}", String::from_utf8_lossy(&value)), diff --git a/crates/imap-proto/src/parser/list.rs b/crates/imap-proto/src/parser/list.rs index 16688e28..772ce56b 100644 --- a/crates/imap-proto/src/parser/list.rs +++ b/crates/imap-proto/src/parser/list.rs @@ -179,37 +179,31 @@ impl Request { impl SelectionOption { pub fn parse(value: &[u8]) -> super::Result { - if value.eq_ignore_ascii_case(b"subscribed") { - Ok(Self::Subscribed) - } else if value.eq_ignore_ascii_case(b"remote") { - Ok(Self::Remote) - } else if value.eq_ignore_ascii_case(b"recursivematch") { - Ok(Self::RecursiveMatch) - } else if value.eq_ignore_ascii_case(b"special-use") { - Ok(Self::SpecialUse) - } else { - Err(format!( - "Invalid selection option {:?}.", + hashify::tiny_map_ignore_case!(value, + "SUBSCRIBED" => Self::Subscribed, + "REMOTE" => Self::Remote, + "RECURSIVEMATCH" => Self::RecursiveMatch, + "SPECIAL-USE" => Self::SpecialUse, + ) + .ok_or_else(|| { + format!( + "Unsupported selection option '{}'.", String::from_utf8_lossy(value) ) - .into()) - } + .into() + }) } } impl ReturnOption { pub fn parse(value: &[u8]) -> super::Result { - if value.eq_ignore_ascii_case(b"subscribed") { - Ok(Self::Subscribed) - } else if value.eq_ignore_ascii_case(b"children") { - Ok(Self::Children) - } else if value.eq_ignore_ascii_case(b"status") { - Ok(Self::Status(Vec::with_capacity(2))) - } else if value.eq_ignore_ascii_case(b"special-use") { - Ok(Self::SpecialUse) - } else { - Err(format!("Invalid return option {:?}", String::from_utf8_lossy(value)).into()) - } + hashify::tiny_map_ignore_case!(value, + "SUBSCRIBED" => Self::Subscribed, + "CHILDREN" => Self::Children, + "STATUS" => Self::Status(Vec::with_capacity(2)), + "SPECIAL-USE" => Self::SpecialUse, + ) + .ok_or_else(|| format!("Invalid return option {:?}", String::from_utf8_lossy(value)).into()) } } diff --git a/crates/imap-proto/src/parser/mod.rs b/crates/imap-proto/src/parser/mod.rs index 5272e6f0..d3177c1f 100644 --- a/crates/imap-proto/src/parser/mod.rs +++ b/crates/imap-proto/src/parser/mod.rs @@ -38,47 +38,46 @@ pub type Result = std::result::Result>; impl CommandParser for Command { fn parse(value: &[u8], uid: bool) -> Option { - match value { - b"CAPABILITY" => Some(Command::Capability), - b"NOOP" => Some(Command::Noop), - b"LOGOUT" => Some(Command::Logout), - b"STARTTLS" => Some(Command::StartTls), - b"AUTHENTICATE" => Some(Command::Authenticate), - b"LOGIN" => Some(Command::Login), - b"ENABLE" => Some(Command::Enable), - b"SELECT" => Some(Command::Select), - b"EXAMINE" => Some(Command::Examine), - b"CREATE" => Some(Command::Create), - b"DELETE" => Some(Command::Delete), - b"RENAME" => Some(Command::Rename), - b"SUBSCRIBE" => Some(Command::Subscribe), - b"UNSUBSCRIBE" => Some(Command::Unsubscribe), - b"LIST" => Some(Command::List), - b"NAMESPACE" => Some(Command::Namespace), - b"STATUS" => Some(Command::Status), - b"APPEND" => Some(Command::Append), - b"IDLE" => Some(Command::Idle), - b"CLOSE" => Some(Command::Close), - b"UNSELECT" => Some(Command::Unselect), - b"EXPUNGE" => Some(Command::Expunge(uid)), - b"SEARCH" => Some(Command::Search(uid)), - b"FETCH" => Some(Command::Fetch(uid)), - b"STORE" => Some(Command::Store(uid)), - b"COPY" => Some(Command::Copy(uid)), - b"MOVE" => Some(Command::Move(uid)), - b"SORT" => Some(Command::Sort(uid)), - b"THREAD" => Some(Command::Thread(uid)), - b"LSUB" => Some(Command::Lsub), - b"CHECK" => Some(Command::Check), - b"SETACL" => Some(Command::SetAcl), - b"DELETEACL" => Some(Command::DeleteAcl), - b"GETACL" => Some(Command::GetAcl), - b"LISTRIGHTS" => Some(Command::ListRights), - b"MYRIGHTS" => Some(Command::MyRights), - b"UNAUTHENTICATE" => Some(Command::Unauthenticate), - b"ID" => Some(Command::Id), - _ => None, - } + hashify::tiny_map!(value, + "CAPABILITY" => Command::Capability, + "NOOP" => Command::Noop, + "LOGOUT" => Command::Logout, + "STARTTLS" => Command::StartTls, + "AUTHENTICATE" => Command::Authenticate, + "LOGIN" => Command::Login, + "ENABLE" => Command::Enable, + "SELECT" => Command::Select, + "EXAMINE" => Command::Examine, + "CREATE" => Command::Create, + "DELETE" => Command::Delete, + "RENAME" => Command::Rename, + "SUBSCRIBE" => Command::Subscribe, + "UNSUBSCRIBE" => Command::Unsubscribe, + "LIST" => Command::List, + "NAMESPACE" => Command::Namespace, + "STATUS" => Command::Status, + "APPEND" => Command::Append, + "IDLE" => Command::Idle, + "CLOSE" => Command::Close, + "UNSELECT" => Command::Unselect, + "EXPUNGE" => Command::Expunge(uid), + "SEARCH" => Command::Search(uid), + "FETCH" => Command::Fetch(uid), + "STORE" => Command::Store(uid), + "COPY" => Command::Copy(uid), + "MOVE" => Command::Move(uid), + "SORT" => Command::Sort(uid), + "THREAD" => Command::Thread(uid), + "LSUB" => Command::Lsub, + "CHECK" => Command::Check, + "SETACL" => Command::SetAcl, + "DELETEACL" => Command::DeleteAcl, + "GETACL" => Command::GetAcl, + "LISTRIGHTS" => Command::ListRights, + "MYRIGHTS" => Command::MyRights, + "UNAUTHENTICATE" => Command::Unauthenticate, + "ID" => Command::Id, + ) } #[inline(always)] @@ -89,61 +88,38 @@ impl CommandParser for Command { impl Flag { pub fn parse_imap(value: Vec) -> Result { - Ok( - match value - .first() - .ok_or_else(|| Cow::from("Null flags are not allowed."))? - { - b'\\' => { - if value.eq_ignore_ascii_case(b"\\Seen") { - Flag::Seen - } else if value.eq_ignore_ascii_case(b"\\Answered") { - Flag::Answered - } else if value.eq_ignore_ascii_case(b"\\Flagged") { - Flag::Flagged - } else if value.eq_ignore_ascii_case(b"\\Deleted") { - Flag::Deleted - } else if value.eq_ignore_ascii_case(b"\\Draft") { - Flag::Draft - } else if value.eq_ignore_ascii_case(b"\\Recent") { - Flag::Recent - } else if value.eq_ignore_ascii_case(b"\\Important") { - Flag::Important - } else { - Flag::Keyword( - String::from_utf8(value).map_err(|_| Cow::from("Invalid UTF-8."))?, - ) - } - } - b'$' => { - if value.eq_ignore_ascii_case(b"$Forwarded") { - Flag::Forwarded - } else if value.eq_ignore_ascii_case(b"$MDNSent") { - Flag::MDNSent - } else if value.eq_ignore_ascii_case(b"$Junk") { - Flag::Junk - } else if value.eq_ignore_ascii_case(b"$NotJunk") { - Flag::NotJunk - } else if value.eq_ignore_ascii_case(b"$Phishing") { - Flag::Phishing - } else if value.eq_ignore_ascii_case(b"$Important") { - Flag::Important - } else { - Flag::Keyword( - String::from_utf8(value).map_err(|_| Cow::from("Invalid UTF-8."))?, - ) - } - } - _ => Flag::Keyword( - String::from_utf8(value).map_err(|_| Cow::from("Invalid UTF-8."))?, - ), - }, - ) + if !value.is_empty() { + let flag = hashify::tiny_map_ignore_case!(value.as_slice(), + "\\Seen" => Flag::Seen, + "\\Answered" => Flag::Answered, + "\\Flagged" => Flag::Flagged, + "\\Deleted" => Flag::Deleted, + "\\Draft" => Flag::Draft, + "\\Recent" => Flag::Recent, + "\\Important" => Flag::Important, + "$Forwarded" => Flag::Forwarded, + "$MDNSent" => Flag::MDNSent, + "$Junk" => Flag::Junk, + "$NotJunk" => Flag::NotJunk, + "$Phishing" => Flag::Phishing, + "$Important" => Flag::Important, + ); + + if let Some(flag) = flag { + Ok(flag) + } else { + String::from_utf8(value) + .map_err(|_| Cow::from("Invalid UTF-8.")) + .map(Flag::Keyword) + } + } else { + Err(Cow::from("Null flags are not allowed.")) + } } pub fn parse_jmap(value: String) -> Self { if value.starts_with('$') { - match value.to_ascii_lowercase().as_str() { + hashify::tiny_map_ignore_case!(value.as_bytes(), "$seen" => Flag::Seen, "$draft" => Flag::Draft, "$flagged" => Flag::Flagged, @@ -156,8 +132,8 @@ impl Flag { "$deleted" => Flag::Deleted, "$forwarded" => Flag::Forwarded, "$mdnsent" => Flag::MDNSent, - _ => Flag::Keyword(value), - } + ) + .unwrap_or_else(|| Flag::Keyword(value)) } else { let mut keyword = String::with_capacity(value.len()); for c in value.chars() { diff --git a/crates/imap-proto/src/parser/search.rs b/crates/imap-proto/src/parser/search.rs index cbd42d92..d0f4f58b 100644 --- a/crates/imap-proto/src/parser/search.rs +++ b/crates/imap-proto/src/parser/search.rs @@ -105,238 +105,349 @@ pub fn parse_filters( let mut found_parenthesis = false; match token { Token::Argument(value) => { - if value.eq_ignore_ascii_case(b"ALL") { - filters.push(Filter::All); - } else if value.eq_ignore_ascii_case(b"ANSWERED") { - filters.push(Filter::Answered); - } else if value.eq_ignore_ascii_case(b"BCC") { - filters.push(Filter::Bcc(decode_argument(tokens, decoder)?)); - } else if value.eq_ignore_ascii_case(b"BEFORE") { - filters.push(Filter::Before(parse_date( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected date"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"BODY") { - filters.push(Filter::Body(decode_argument(tokens, decoder)?)); - } else if value.eq_ignore_ascii_case(b"CC") { - filters.push(Filter::Cc(decode_argument(tokens, decoder)?)); - } else if value.eq_ignore_ascii_case(b"DELETED") { - filters.push(Filter::Deleted); - } else if value.eq_ignore_ascii_case(b"DRAFT") { - filters.push(Filter::Draft); - } else if value.eq_ignore_ascii_case(b"FLAGGED") { - filters.push(Filter::Flagged); - } else if value.eq_ignore_ascii_case(b"FROM") { - filters.push(Filter::From(decode_argument(tokens, decoder)?)); - } else if value.eq_ignore_ascii_case(b"HEADER") { - filters.push(Filter::Header( - decode_argument(tokens, decoder)?, - decode_argument(tokens, decoder)?, - )); - } else if value.eq_ignore_ascii_case(b"KEYWORD") { - filters.push(Filter::Keyword(Flag::parse_imap( - tokens - .next() - .ok_or_else(|| Cow::from("Expected keyword"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"LARGER") { - filters.push(Filter::Larger(parse_number::( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected integer"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"ON") { - filters.push(Filter::On(parse_date( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected date"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"SEEN") { - filters.push(Filter::Seen); - } else if value.eq_ignore_ascii_case(b"SENTBEFORE") { - filters.push(Filter::SentBefore(parse_date( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected date"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"SENTON") { - filters.push(Filter::SentOn(parse_date( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected date"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"SENTSINCE") { - filters.push(Filter::SentSince(parse_date( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected date"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"SINCE") { - filters.push(Filter::Since(parse_date( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected date"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"SMALLER") { - filters.push(Filter::Smaller(parse_number::( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected integer"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"SUBJECT") { - filters.push(Filter::Subject(decode_argument(tokens, decoder)?)); - } else if value.eq_ignore_ascii_case(b"TEXT") { - filters.push(Filter::Text(decode_argument(tokens, decoder)?)); - } else if value.eq_ignore_ascii_case(b"TO") { - filters.push(Filter::To(decode_argument(tokens, decoder)?)); - } else if value.eq_ignore_ascii_case(b"UID") { - filters.push(Filter::Sequence( - parse_sequence_set( + let mut found_operator = false; + + hashify::fnc_map_ignore_case!(value.as_slice(), + "ALL" => { + filters.push(Filter::All); + found_operator = true; + }, + "ANSWERED" => { + filters.push(Filter::Answered); + found_operator = true; + }, + "BCC" => { + filters.push(Filter::Bcc(decode_argument(tokens, decoder)?)); + found_operator = true; + }, + "BEFORE" => { + filters.push(Filter::Before(parse_date( &tokens .next() - .ok_or_else(|| Cow::from("Missing sequence set."))? + .ok_or_else(|| Cow::from("Expected date"))? .unwrap_bytes(), - )?, - true, - )); - } else if value.eq_ignore_ascii_case(b"UNANSWERED") { - filters.push(Filter::Unanswered); - } else if value.eq_ignore_ascii_case(b"UNDELETED") { - filters.push(Filter::Undeleted); - } else if value.eq_ignore_ascii_case(b"UNDRAFT") { - filters.push(Filter::Undraft); - } else if value.eq_ignore_ascii_case(b"UNFLAGGED") { - filters.push(Filter::Unflagged); - } else if value.eq_ignore_ascii_case(b"UNKEYWORD") { - filters.push(Filter::Unkeyword(Flag::parse_imap( - tokens - .next() - .ok_or_else(|| Cow::from("Expected keyword"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"UNSEEN") { - filters.push(Filter::Unseen); - } else if value.eq_ignore_ascii_case(b"OLDER") { - filters.push(Filter::Older(parse_number::( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected integer"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"YOUNGER") { - filters.push(Filter::Younger(parse_number::( - &tokens - .next() - .ok_or_else(|| Cow::from("Expected integer"))? - .unwrap_bytes(), - )?)); - } else if value.eq_ignore_ascii_case(b"OLD") { - filters.push(Filter::Old); - } else if value.eq_ignore_ascii_case(b"NEW") { - filters.push(Filter::New); - } else if value.eq_ignore_ascii_case(b"RECENT") { - filters.push(Filter::Recent); - } else if value.eq_ignore_ascii_case(b"MODSEQ") { - let param = tokens - .next() - .ok_or_else(|| Cow::from("Missing MODSEQ parameters."))? - .unwrap_bytes(); - if param.is_empty() || param.iter().any(|ch| !ch.is_ascii_digit()) { - if param.len() <= 7 || !param.starts_with(b"/flags/") { - return Err(format!( - "Unsupported MODSEQ parameter '{}'.", - String::from_utf8_lossy(¶m) - ) - .into()); - } - let flag = Flag::parse_imap((param[7..]).to_vec())?; - let mod_seq_entry = match tokens.next() { - Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"all") => { - ModSeqEntry::All(flag) - } - Some(Token::Argument(value)) - if value.eq_ignore_ascii_case(b"shared") => - { - ModSeqEntry::Shared(flag) - } - Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"priv") => { - ModSeqEntry::Private(flag) - } - Some(token) => { - return Err( - format!("Unsupported MODSEQ parameter '{}'.", token).into() - ); - } - None => { - return Err("Missing MODSEQ entry-type-req parameter.".into()); - } - }; - filters.push(Filter::ModSeq(( - parse_number::( + )?)); + found_operator = true; + }, + "BODY" => { + filters.push(Filter::Body(decode_argument(tokens, decoder)?)); + found_operator = true; + + }, + "CC" => { + filters.push(Filter::Cc(decode_argument(tokens, decoder)?)); + found_operator = true; + + }, + "DELETED" => { + filters.push(Filter::Deleted); + found_operator = true; + + }, + "DRAFT" => { + filters.push(Filter::Draft); + found_operator = true; + + }, + "FLAGGED" => { + filters.push(Filter::Flagged); + found_operator = true; + + }, + "FROM" => { + filters.push(Filter::From(decode_argument(tokens, decoder)?)); + found_operator = true; + + }, + "HEADER" => { + filters.push(Filter::Header( + decode_argument(tokens, decoder)?, + decode_argument(tokens, decoder)?, + )); + found_operator = true; + + }, + "KEYWORD" => { + filters.push(Filter::Keyword(Flag::parse_imap( + tokens + .next() + .ok_or_else(|| Cow::from("Expected keyword"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "LARGER" => { + filters.push(Filter::Larger(parse_number::( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected integer"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "ON" => { + filters.push(Filter::On(parse_date( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected date"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "SEEN" => { + filters.push(Filter::Seen); + found_operator = true; + + }, + "SENTBEFORE" => { + filters.push(Filter::SentBefore(parse_date( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected date"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "SENTON" => { + filters.push(Filter::SentOn(parse_date( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected date"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "SENTSINCE" => { + filters.push(Filter::SentSince(parse_date( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected date"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "SINCE" => { + filters.push(Filter::Since(parse_date( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected date"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "SMALLER" => { + filters.push(Filter::Smaller(parse_number::( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected integer"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "SUBJECT" => { + filters.push(Filter::Subject(decode_argument(tokens, decoder)?)); + found_operator = true; + + }, + "TEXT" => { + filters.push(Filter::Text(decode_argument(tokens, decoder)?)); + found_operator = true; + + }, + "TO" => { + filters.push(Filter::To(decode_argument(tokens, decoder)?)); + found_operator = true; + + }, + "UID" => { + filters.push(Filter::Sequence( + parse_sequence_set( &tokens .next() - .ok_or_else(|| { - Cow::from("Missing MODSEQ mod-sequence-valzer parameter.") - })? + .ok_or_else(|| Cow::from("Missing sequence set."))? .unwrap_bytes(), )?, - mod_seq_entry, - ))); - } else { - filters.push(Filter::ModSeq(( - parse_number::(¶m)?, - ModSeqEntry::None, - ))); - } - } else if value.eq_ignore_ascii_case(b"EMAILID") { - filters.push(Filter::EmailId( - tokens - .next() - .ok_or_else(|| Cow::from("Expected an EMAILID value."))? - .unwrap_string()?, - )); - } else if value.eq_ignore_ascii_case(b"THREADID") { - filters.push(Filter::ThreadId( - tokens - .next() - .ok_or_else(|| Cow::from("Expected an THREADID value."))? - .unwrap_string()?, - )); - } else if value.eq_ignore_ascii_case(b"OR") { - if filters_stack.len() > 10 { - return Err(Cow::from("Too many nested filters")); - } + true, + )); + found_operator = true; - filters_stack.push((filters, operator, filters_len)); - filters_len = 0; - filters = Vec::with_capacity(2); - operator = Filter::Or; - continue; - } else if value.eq_ignore_ascii_case(b"NOT") { - if filters_stack.len() > 10 { - return Err(Cow::from("Too many nested filters")); - } + }, + "UNANSWERED" => { + filters.push(Filter::Unanswered); + found_operator = true; - filters_stack.push((filters, operator, filters_len)); - filters_len = 0; - filters = Vec::with_capacity(1); - operator = Filter::Not; - continue; - } else { - filters.push(Filter::Sequence(parse_sequence_set(&value)?, false)); - } + }, + "UNDELETED" => { + filters.push(Filter::Undeleted); + found_operator = true; + + }, + "UNDRAFT" => { + filters.push(Filter::Undraft); + found_operator = true; + + }, + "UNFLAGGED" => { + filters.push(Filter::Unflagged); + found_operator = true; + + }, + "UNKEYWORD" => { + filters.push(Filter::Unkeyword(Flag::parse_imap( + tokens + .next() + .ok_or_else(|| Cow::from("Expected keyword"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "UNSEEN" => { + filters.push(Filter::Unseen); + found_operator = true; + + }, + "OLDER" => { + filters.push(Filter::Older(parse_number::( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected integer"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "YOUNGER" => { + filters.push(Filter::Younger(parse_number::( + &tokens + .next() + .ok_or_else(|| Cow::from("Expected integer"))? + .unwrap_bytes(), + )?)); + found_operator = true; + + }, + "OLD" => { + filters.push(Filter::Old); + found_operator = true; + }, + "NEW" => { + filters.push(Filter::New); + found_operator = true; + }, + "RECENT" => { + filters.push(Filter::Recent); + found_operator = true; + }, + "MODSEQ" => { + let param = tokens + .next() + .ok_or_else(|| Cow::from("Missing MODSEQ parameters."))? + .unwrap_bytes(); + if param.is_empty() || param.iter().any(|ch| !ch.is_ascii_digit()) { + if param.len() <= 7 || !param.starts_with(b"/flags/") { + return Err(format!( + "Unsupported MODSEQ parameter '{}'.", + String::from_utf8_lossy(¶m) + ) + .into()); + } + let flag = Flag::parse_imap((param[7..]).to_vec())?; + let mod_seq_entry = match tokens.next() { + Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"all") => { + ModSeqEntry::All(flag) + } + Some(Token::Argument(value)) + if value.eq_ignore_ascii_case(b"shared") => + { + ModSeqEntry::Shared(flag) + } + Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"priv") => { + ModSeqEntry::Private(flag) + } + Some(token) => { + return Err( + format!("Unsupported MODSEQ parameter '{}'.", token).into() + ); + } + None => { + return Err("Missing MODSEQ entry-type-req parameter.".into()); + } + }; + filters.push(Filter::ModSeq(( + parse_number::( + &tokens + .next() + .ok_or_else(|| { + Cow::from("Missing MODSEQ mod-sequence-valzer parameter.") + })? + .unwrap_bytes(), + )?, + mod_seq_entry, + ))); + } else { + filters.push(Filter::ModSeq(( + parse_number::(¶m)?, + ModSeqEntry::None, + ))); + } + found_operator = true; + }, + "EMAILID" => { + filters.push(Filter::EmailId( + tokens + .next() + .ok_or_else(|| Cow::from("Expected an EMAILID value."))? + .unwrap_string()?, + )); + found_operator = true; + }, + "THREADID" => { + filters.push(Filter::ThreadId( + tokens + .next() + .ok_or_else(|| Cow::from("Expected an THREADID value."))? + .unwrap_string()?, + )); + found_operator = true; + }, + "OR" => { + if filters_stack.len() > 10 { + return Err(Cow::from("Too many nested filters")); + } + + filters_stack.push((filters, operator, filters_len)); + filters_len = 0; + filters = Vec::with_capacity(2); + operator = Filter::Or; + continue; + }, + "NOT" => { + if filters_stack.len() > 10 { + return Err(Cow::from("Too many nested filters")); + } + + filters_stack.push((filters, operator, filters_len)); + filters_len = 0; + filters = Vec::with_capacity(1); + operator = Filter::Not; + continue; + }, + ); filters_len += 1; + + if !found_operator { + filters.push(Filter::Sequence(parse_sequence_set(&value)?, false)); + } } Token::ParenthesisOpen => { if filters_stack.len() > 10 { @@ -407,21 +518,22 @@ pub fn decode_argument( impl ResultOption { pub fn parse(value: &[u8]) -> super::Result { - if value.eq_ignore_ascii_case(b"min") { - Ok(Self::Min) - } else if value.eq_ignore_ascii_case(b"max") { - Ok(Self::Max) - } else if value.eq_ignore_ascii_case(b"all") { - Ok(Self::All) - } else if value.eq_ignore_ascii_case(b"count") { - Ok(Self::Count) - } else if value.eq_ignore_ascii_case(b"save") { - Ok(Self::Save) - } else if value.eq_ignore_ascii_case(b"context") { - Ok(Self::Context) - } else { - Err(format!("Invalid result option {:?}", String::from_utf8_lossy(value)).into()) - } + hashify::tiny_map_ignore_case!( + value, + "min" => Self::Min, + "max" => Self::Max, + "all" => Self::All, + "count" => Self::Count, + "save" => Self::Save, + "context" => Self::Context, + ) + .ok_or_else(|| { + format!( + "Invalid result option '{}'.", + String::from_utf8_lossy(value) + ) + .into() + }) } } diff --git a/crates/imap-proto/src/parser/sort.rs b/crates/imap-proto/src/parser/sort.rs index 7c218a53..ec99e83e 100644 --- a/crates/imap-proto/src/parser/sort.rs +++ b/crates/imap-proto/src/parser/sort.rs @@ -92,27 +92,18 @@ impl Request { impl Sort { pub fn parse(value: &[u8]) -> super::Result { - if value.eq_ignore_ascii_case(b"ARRIVAL") { - Ok(Self::Arrival) - } else if value.eq_ignore_ascii_case(b"CC") { - Ok(Self::Cc) - } else if value.eq_ignore_ascii_case(b"DATE") { - Ok(Self::Date) - } else if value.eq_ignore_ascii_case(b"FROM") { - Ok(Self::From) - } else if value.eq_ignore_ascii_case(b"SIZE") { - Ok(Self::Size) - } else if value.eq_ignore_ascii_case(b"SUBJECT") { - Ok(Self::Subject) - } else if value.eq_ignore_ascii_case(b"TO") { - Ok(Self::To) - } else if value.eq_ignore_ascii_case(b"DISPLAYFROM") { - Ok(Self::DisplayFrom) - } else if value.eq_ignore_ascii_case(b"DISPLAYTO") { - Ok(Self::DisplayTo) - } else { - Err(format!("Invalid sort criteria {:?}", String::from_utf8_lossy(value)).into()) - } + hashify::tiny_map_ignore_case!(value, + "ARRIVAL" => Self::Arrival, + "CC" => Self::Cc, + "DATE" => Self::Date, + "FROM" => Self::From, + "SIZE" => Self::Size, + "SUBJECT" => Self::Subject, + "TO" => Self::To, + "DISPLAYFROM" => Self::DisplayFrom, + "DISPLAYTO" => Self::DisplayTo, + ) + .ok_or_else(|| format!("Invalid sort criteria {:?}", String::from_utf8_lossy(value)).into()) } } diff --git a/crates/imap-proto/src/parser/status.rs b/crates/imap-proto/src/parser/status.rs index 39f6270d..c37c3066 100644 --- a/crates/imap-proto/src/parser/status.rs +++ b/crates/imap-proto/src/parser/status.rs @@ -70,31 +70,24 @@ impl Request { impl Status { pub fn parse(value: &[u8]) -> super::Result { - if value.eq_ignore_ascii_case(b"messages") { - Ok(Self::Messages) - } else if value.eq_ignore_ascii_case(b"uidnext") { - Ok(Self::UidNext) - } else if value.eq_ignore_ascii_case(b"uidvalidity") { - Ok(Self::UidValidity) - } else if value.eq_ignore_ascii_case(b"unseen") { - Ok(Self::Unseen) - } else if value.eq_ignore_ascii_case(b"deleted") { - Ok(Self::Deleted) - } else if value.eq_ignore_ascii_case(b"size") { - Ok(Self::Size) - } else if value.eq_ignore_ascii_case(b"highestmodseq") { - Ok(Self::HighestModSeq) - } else if value.eq_ignore_ascii_case(b"mailboxid") { - Ok(Self::MailboxId) - } else if value.eq_ignore_ascii_case(b"recent") { - Ok(Self::Recent) - } else { - Err(format!( + hashify::tiny_map!(value, + "MESSAGES" => Self::Messages, + "UIDNEXT" => Self::UidNext, + "UIDVALIDITY" => Self::UidValidity, + "UNSEEN" => Self::Unseen, + "DELETED" => Self::Deleted, + "SIZE" => Self::Size, + "HIGHESTMODSEQ" => Self::HighestModSeq, + "MAILBOXID" => Self::MailboxId, + "RECENT" => Self::Recent, + ) + .ok_or_else(|| { + format!( "Invalid status option '{}'.", String::from_utf8_lossy(value) ) - .into()) - } + .into() + }) } } diff --git a/crates/imap-proto/src/parser/store.rs b/crates/imap-proto/src/parser/store.rs index 0c7ed471..21eb553f 100644 --- a/crates/imap-proto/src/parser/store.rs +++ b/crates/imap-proto/src/parser/store.rs @@ -64,27 +64,23 @@ impl Request { .next() .ok_or_else(|| bad(self.tag.to_string(), "Missing message data item name."))? .unwrap_bytes(); - let (is_silent, operation) = if operation.eq_ignore_ascii_case(b"FLAGS") { - (false, Operation::Set) - } else if operation.eq_ignore_ascii_case(b"FLAGS.SILENT") { - (true, Operation::Set) - } else if operation.eq_ignore_ascii_case(b"+FLAGS") { - (false, Operation::Add) - } else if operation.eq_ignore_ascii_case(b"+FLAGS.SILENT") { - (true, Operation::Add) - } else if operation.eq_ignore_ascii_case(b"-FLAGS") { - (false, Operation::Clear) - } else if operation.eq_ignore_ascii_case(b"-FLAGS.SILENT") { - (true, Operation::Clear) - } else { - return Err(bad( - self.tag, + let (is_silent, operation) = hashify::tiny_map!(operation.as_slice(), + "FLAGS" => (false, Operation::Set), + "FLAGS.SILENT" => (true, Operation::Set), + "+FLAGS" => (false, Operation::Add), + "+FLAGS.SILENT" => (true, Operation::Add), + "-FLAGS" => (false, Operation::Clear), + "-FLAGS.SILENT" => (true, Operation::Clear), + ) + .ok_or_else(|| { + bad( + self.tag.to_string(), format!( "Unsupported message data item name: {:?}", String::from_utf8_lossy(&operation) ), - )); - }; + ) + })?; // Flags let mut keywords = Vec::new(); diff --git a/crates/imap-proto/src/parser/thread.rs b/crates/imap-proto/src/parser/thread.rs index fecce30d..8325c397 100644 --- a/crates/imap-proto/src/parser/thread.rs +++ b/crates/imap-proto/src/parser/thread.rs @@ -52,17 +52,17 @@ impl Request { impl Algorithm { pub fn parse(value: &[u8]) -> super::Result { - if value.eq_ignore_ascii_case(b"ORDEREDSUBJECT") { - Ok(Self::OrderedSubject) - } else if value.eq_ignore_ascii_case(b"REFERENCES") { - Ok(Self::References) - } else { - Err(format!( + hashify::tiny_map_ignore_case!(value, + "ORDEREDSUBJECT" => Self::OrderedSubject, + "REFERENCES" => Self::References, + ) + .ok_or_else(|| { + format!( "Invalid threading algorithm {:?}", String::from_utf8_lossy(value) ) - .into()) - } + .into() + }) } }