/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::utf7::utf7_encode; use super::{ ImapResponse, quoted_string, status::{Status, StatusItem}, }; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Arguments { Basic { tag: String, reference_name: String, mailbox_name: String, }, Extended { tag: String, reference_name: String, mailbox_name: Vec, selection_options: Vec, return_options: Vec, }, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { pub is_rev2: bool, pub is_utf8: bool, pub is_lsub: bool, pub list_items: Vec, pub status_items: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum SelectionOption { Subscribed, Remote, RecursiveMatch, SpecialUse, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum ReturnOption { Subscribed, Children, Status(Vec), SpecialUse, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Attribute { NoInferiors, NoSelect, Marked, Unmarked, NonExistent, HasChildren, HasNoChildren, Subscribed, Remote, All, Archive, Drafts, Flagged, Junk, Sent, Trash, Important, Memos, Scheduled, Snoozed, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum ChildInfo { Subscribed, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Tag { ChildInfo(Vec), OldName(String), } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListItem { pub mailbox_name: String, pub attributes: Vec, pub tags: Vec, } impl Arguments { pub fn is_separator_query(&self) -> bool { match self { Arguments::Basic { mailbox_name, reference_name, .. } => mailbox_name.is_empty() && reference_name.is_empty(), Arguments::Extended { mailbox_name, reference_name, .. } => mailbox_name.is_empty() && reference_name.is_empty(), } } pub fn unwrap_tag(self) -> String { match self { Arguments::Basic { tag, .. } => tag, Arguments::Extended { tag, .. } => tag, } } } impl Attribute { pub fn is_rev1(&self) -> bool { matches!( self, Attribute::NoInferiors | Attribute::NoSelect | Attribute::Marked | Attribute::Unmarked ) } pub fn serialize(&self, buf: &mut Vec) { buf.extend_from_slice(match self { Attribute::NoInferiors => b"\\NoInferiors", Attribute::NoSelect => b"\\NoSelect", Attribute::Marked => b"\\Marked", Attribute::Unmarked => b"\\Unmarked", Attribute::NonExistent => b"\\NonExistent", Attribute::HasChildren => b"\\HasChildren", Attribute::HasNoChildren => b"\\HasNoChildren", Attribute::Subscribed => b"\\Subscribed", Attribute::Remote => b"\\Remote", Attribute::All => b"\\All", Attribute::Archive => b"\\Archive", Attribute::Drafts => b"\\Drafts", Attribute::Flagged => b"\\Flagged", Attribute::Junk => b"\\Junk", Attribute::Sent => b"\\Sent", Attribute::Trash => b"\\Trash", Attribute::Important => b"\\Important", Attribute::Memos => b"\\Memos", Attribute::Scheduled => b"\\Scheduled", Attribute::Snoozed => b"\\Snoozed", }); } } impl TryFrom<&str> for Attribute { type Error = (); fn try_from(value: &str) -> Result { hashify::tiny_map!(value.as_bytes(), "archive" => Attribute::Archive, "drafts" => Attribute::Drafts, "junk" => Attribute::Junk, "sent" => Attribute::Sent, "trash" => Attribute::Trash, "important" => Attribute::Important, "memos" => Attribute::Memos, "scheduled" => Attribute::Scheduled, "snoozed" => Attribute::Snoozed, ) .ok_or(()) } } impl ChildInfo { pub fn serialize(&self, buf: &mut Vec) { buf.push(b'\"'); buf.extend_from_slice(match self { ChildInfo::Subscribed => b"SUBSCRIBED", }); buf.push(b'\"'); } } impl Tag { pub fn serialize(&self, buf: &mut Vec) { match self { Tag::ChildInfo(child_info) => { buf.extend_from_slice(b"\"CHILDINFO\" ("); for (pos, child_info) in child_info.iter().enumerate() { if pos > 0 { buf.push(b' '); } child_info.serialize(buf); } buf.push(b')'); } Tag::OldName(old_name) => { buf.extend_from_slice(b"\"OLDNAME\" ("); quoted_string(buf, old_name); buf.push(b')'); } } } } impl ListItem { pub fn new(name: impl Into) -> Self { ListItem { mailbox_name: name.into(), attributes: Vec::new(), tags: Vec::new(), } } pub fn serialize(&self, buf: &mut Vec, is_rev2: bool, is_utf8: bool, is_lsub: bool) { let normalized_mailbox_name = utf7_encode(&self.mailbox_name); if !is_lsub { buf.extend_from_slice(b"* LIST ("); } else { buf.extend_from_slice(b"* LSUB ("); } for (pos, attr) in self.attributes.iter().enumerate() { if pos > 0 { buf.push(b' '); } attr.serialize(buf); } buf.extend_from_slice(b") \"/\" "); let mut extra_tags = Vec::new(); if normalized_mailbox_name != self.mailbox_name { if is_rev2 || is_utf8 { quoted_string(buf, &self.mailbox_name); if is_rev2 { extra_tags.push(Tag::OldName(normalized_mailbox_name)); } } else { quoted_string(buf, &normalized_mailbox_name); } } else { quoted_string(buf, &self.mailbox_name); } if !extra_tags.is_empty() || !self.tags.is_empty() { buf.extend_from_slice(b" ("); for (pos, tag) in extra_tags.iter().chain(self.tags.iter()).enumerate() { if pos > 0 { buf.push(b' '); } tag.serialize(buf); } buf.extend_from_slice(b")\r\n"); } else { buf.extend_from_slice(b"\r\n"); } } } impl ImapResponse for Response { fn serialize(self) -> Vec { let mut buf = Vec::with_capacity(100); match (self.list_items.is_empty(), self.status_items.is_empty()) { (false, false) => { for (list_item, status_item) in self.list_items.iter().zip(self.status_items.iter()) { list_item.serialize(&mut buf, self.is_rev2, self.is_utf8, self.is_lsub); status_item.serialize(&mut buf, self.is_rev2); } } (false, true) => { for list_item in &self.list_items { list_item.serialize(&mut buf, self.is_rev2, self.is_utf8, self.is_lsub); } } (true, false) => { for status_item in &self.status_items { status_item.serialize(&mut buf, self.is_rev2); } } _ => (), } buf } } #[cfg(test)] mod tests { use crate::protocol::{ ImapResponse, status::{Status, StatusItem, StatusItemType}, }; use super::{Attribute, ChildInfo, ListItem, Tag}; #[test] fn serialize_list_item() { for (response, expected_v2, expected_v1) in [ ( super::ListItem { mailbox_name: "".into(), attributes: vec![], tags: vec![], }, "* LIST () \"/\" \"\"\r\n", "* LIST () \"/\" \"\"\r\n", ), ( super::ListItem { mailbox_name: "中國書店".into(), attributes: vec![Attribute::NoInferiors, Attribute::Drafts], tags: vec![], }, concat!( "* LIST (\\NoInferiors \\Drafts) \"/\" \"中國書店\" ", "(\"OLDNAME\" (\"&Ti1XC2b4Xpc-\"))\r\n" ), "* LIST (\\NoInferiors \\Drafts) \"/\" \"&Ti1XC2b4Xpc-\"\r\n", ), ( super::ListItem { mailbox_name: "☺".into(), attributes: vec![Attribute::Subscribed, Attribute::Remote], tags: vec![Tag::ChildInfo(vec![ChildInfo::Subscribed])], }, concat!( "* LIST (\\Subscribed \\Remote) \"/\" \"☺\" ", "(\"OLDNAME\" (\"&Jjo-\") \"CHILDINFO\" (\"SUBSCRIBED\"))\r\n" ), concat!( "* LIST (\\Subscribed \\Remote) \"/\" \"&Jjo-\" ", "(\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n" ), ), ( super::ListItem { mailbox_name: "foo".into(), attributes: vec![Attribute::HasNoChildren], tags: vec![Tag::ChildInfo(vec![ChildInfo::Subscribed])], }, "* LIST (\\HasNoChildren) \"/\" \"foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n", "* LIST (\\HasNoChildren) \"/\" \"foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n", ), ] { let mut buf_1 = Vec::with_capacity(100); let mut buf_2 = Vec::with_capacity(100); response.serialize(&mut buf_1, false, false, false); response.serialize(&mut buf_2, true, true, false); let response_v1 = String::from_utf8(buf_1).unwrap(); let response_v2 = String::from_utf8(buf_2).unwrap(); assert_eq!(response_v2, expected_v2); assert_eq!(response_v1, expected_v1); } } #[test] fn serialize_list() { let mut response = super::Response { list_items: vec![ ListItem { mailbox_name: "INBOX".into(), attributes: vec![Attribute::Subscribed], tags: vec![], }, ListItem { mailbox_name: "foo".into(), attributes: vec![], tags: vec![Tag::ChildInfo(vec![ChildInfo::Subscribed])], }, ], status_items: vec![ StatusItem { mailbox_name: "INBOX".into(), items: vec![(Status::Messages, StatusItemType::Number(17))], }, StatusItem { mailbox_name: "foo".into(), items: vec![ (Status::Messages, StatusItemType::Number(30)), (Status::Unseen, StatusItemType::Number(29)), ], }, ], is_lsub: false, is_rev2: true, is_utf8: true, }; let expected_v2 = concat!( "* LIST (\\Subscribed) \"/\" \"INBOX\"\r\n", "* STATUS \"INBOX\" (MESSAGES 17)\r\n", "* LIST () \"/\" \"foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n", "* STATUS \"foo\" (MESSAGES 30 UNSEEN 29)\r\n", ); let expected_v1 = concat!( "* LSUB (\\Subscribed) \"/\" \"INBOX\"\r\n", "* LSUB () \"/\" \"foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n", ); let response_v2 = String::from_utf8(response.clone().serialize()).unwrap(); response.is_rev2 = false; response.is_utf8 = false; response.is_lsub = true; response.status_items.clear(); let response_v1 = String::from_utf8(response.serialize()).unwrap(); assert_eq!(response_v2, expected_v2); assert_eq!(response_v1, expected_v1); } }