From 330887c4f5bad5357508b9fa6f723e45ab307d2a Mon Sep 17 00:00:00 2001 From: Damian Poddebniak Date: Mon, 5 Jun 2023 18:37:03 +0200 Subject: [PATCH] refactor: Introduce imap-codec. --- Cargo.lock | 51 +++++- melib/Cargo.toml | 19 ++- melib/src/backends/imap.rs | 149 ++++++++---------- melib/src/backends/imap/cache/sync.rs | 51 +++--- melib/src/backends/imap/connection.rs | 174 +++++++++++++++------ melib/src/backends/imap/managesieve.rs | 2 +- melib/src/backends/imap/operations.rs | 12 +- melib/src/backends/imap/protocol_parser.rs | 34 ---- melib/src/backends/imap/untagged.rs | 26 ++- melib/src/backends/imap/watch.rs | 43 ++--- melib/src/email.rs | 57 +++++++ melib/src/error.rs | 93 +++++++++++ 12 files changed, 481 insertions(+), 230 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 455c554d..2ba23646 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom", +] + [[package]] name = "adler" version = "1.0.2" @@ -185,6 +194,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "bincode" version = "1.3.3" @@ -869,6 +884,29 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "imap-codec" +version = "1.0.0-dev" +source = "git+https://github.com/duesee/imap-codec?rev=95acd5186dacc6c6892603954e3f28d18a9f1193#95acd5186dacc6c6892603954e3f28d18a9f1193" +dependencies = [ + "abnf-core", + "base64 0.21.2", + "chrono", + "imap-types", + "nom", +] + +[[package]] +name = "imap-types" +version = "1.0.0-dev" +source = "git+https://github.com/duesee/imap-codec?rev=95acd5186dacc6c6892603954e3f28d18a9f1193#95acd5186dacc6c6892603954e3f28d18a9f1193" +dependencies = [ + "base64 0.21.2", + "chrono", + "subtle", + "thiserror", +] + [[package]] name = "indexmap" version = "1.9.1" @@ -1087,7 +1125,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d0411d6d3cf6baacae37461dc5b0a32b9c68ae99ddef61bcd88174b8da890a" dependencies = [ - "base64", + "base64 0.13.0", "either", "log", "nom", @@ -1167,7 +1205,7 @@ name = "melib" version = "0.7.2" dependencies = [ "async-stream", - "base64", + "base64 0.13.0", "bincode", "bitflags", "data-encoding", @@ -1175,6 +1213,7 @@ dependencies = [ "encoding_rs", "flate2", "futures", + "imap-codec", "indexmap", "isahc", "libc", @@ -1712,7 +1751,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ - "base64", + "base64 0.13.0", ] [[package]] @@ -1959,6 +1998,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "svg" version = "0.10.0" diff --git a/melib/Cargo.toml b/melib/Cargo.toml index 982736e5..d5f5ee0f 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -53,6 +53,21 @@ uuid = { version = "^1", features = ["serde", "v4", "v5"] } xdg = "2.1.0" xdg-utils = "^0.4.0" +[dependencies.imap-codec] +# TODO(#222): Replace `git` and `rev` with `version`. +git = "https://github.com/duesee/imap-codec" +rev = "95acd5186dacc6c6892603954e3f28d18a9f1193" +features = [ + "ext_condstore_qresync", + "ext_enable", + "ext_idle", + "ext_literal", + "ext_move", + "ext_sasl_ir", + "ext_unselect" +] +optional = true + [dev-dependencies] mailin-embedded = { version = "0.7", features = ["rtls"] } stderrlog = "^0.5" @@ -61,11 +76,11 @@ stderrlog = "^0.5" default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp", "deflate_compression"] debug-tracing = [] -deflate_compression = ["flate2", ] +deflate_compression = ["flate2", "imap-codec/ext_compress"] gpgme = [] http = ["isahc"] http-static = ["isahc", "isahc/static-curl"] -imap_backend = ["tls"] +imap_backend = ["imap-codec", "tls"] jmap_backend = ["http", "serde_json"] maildir_backend = ["notify"] mbox_backend = ["notify"] diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index f3f1ba17..ec561dbc 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -49,6 +49,12 @@ use std::{ }; use futures::{lock::Mutex as FutureMutex, stream::Stream}; +use imap_codec::{ + command::CommandBody, + core::Literal, + flag::{Flag as ImapCodecFlag, StoreResponse, StoreType}, + sequence::{SequenceSet, ONE}, +}; use crate::{ backends::{ @@ -584,32 +590,18 @@ impl MailBackend for ImapType { .unwrap() .iter() .any(|cap| cap.eq_ignore_ascii_case(b"LITERAL+")); - if has_literal_plus { - conn.send_command( - format!( - "APPEND \"{}\" ({}) {{{}+}}", - &path, - flags_to_imap_list!(flags), - bytes.len() - ) - .as_bytes(), - ) - .await?; + let data = if has_literal_plus { + Literal::try_from(bytes)?.into_non_sync() } else { - conn.send_command( - format!( - "APPEND \"{}\" ({}) {{{}}}", - &path, - flags_to_imap_list!(flags), - bytes.len() - ) - .as_bytes(), - ) - .await?; - // wait for "+ Ready for literal data" reply - conn.wait_for_continuation_request().await?; - } - conn.send_literal(&bytes).await?; + Literal::try_from(bytes)? + }; + conn.send_command(CommandBody::append( + path, + flags.derive_imap_codec_flags(), + None, + data, + )?) + .await?; conn.read_response(&mut response, RequiredResponses::empty()) .await?; Ok(()) @@ -658,36 +650,24 @@ impl MailBackend for ImapType { conn.select_mailbox(source_mailbox_hash, &mut response, false) .await?; if has_move { - let command = { - let mut cmd = format!("UID MOVE {}", uids[0]); - for uid in uids.iter().skip(1) { - cmd = format!("{},{}", cmd, uid); - } - format!("{} \"{}\"", cmd, dest_path) - }; - conn.send_command(command.as_bytes()).await?; + conn.send_command(CommandBody::r#move(uids.as_slice(), dest_path, true)?) + .await?; conn.read_response(&mut response, RequiredResponses::empty()) .await?; } else { - let command = { - let mut cmd = format!("UID COPY {}", uids[0]); - for uid in uids.iter().skip(1) { - cmd = format!("{},{}", cmd, uid); - } - format!("{} \"{}\"", cmd, dest_path) - }; - conn.send_command(command.as_bytes()).await?; + conn.send_command(CommandBody::copy(uids.as_slice(), dest_path, true)?) + .await?; conn.read_response(&mut response, RequiredResponses::empty()) .await?; if move_ { - let command = { - let mut cmd = format!("UID STORE {}", uids[0]); - for uid in uids.iter().skip(1) { - cmd = format!("{},{}", cmd, uid); - } - format!("{} +FLAGS (\\Deleted)", cmd) - }; - conn.send_command(command.as_bytes()).await?; + conn.send_command(CommandBody::store( + uids.as_slice(), + StoreType::Add, + StoreResponse::Answer, + vec![ImapCodecFlag::Deleted], + true, + )?) + .await?; conn.read_response(&mut response, RequiredResponses::empty()) .await?; } @@ -782,7 +762,7 @@ impl MailBackend for ImapType { cmd.push(')'); cmd }; - conn.send_command(command.as_bytes()).await?; + conn.send_command_raw(command.as_bytes()).await?; conn.read_response(&mut response, RequiredResponses::empty()) .await?; if set_seen { @@ -847,7 +827,7 @@ impl MailBackend for ImapType { cmd.push(')'); cmd }; - conn.send_command(command.as_bytes()).await?; + conn.send_command_raw(command.as_bytes()).await?; conn.read_response(&mut response, RequiredResponses::empty()) .await?; if set_unseen { @@ -878,7 +858,7 @@ impl MailBackend for ImapType { flag_future.await?; let mut response = Vec::with_capacity(8 * 1024); let mut conn = connection.lock().await; - conn.send_command("EXPUNGE".as_bytes()).await?; + conn.send_command(CommandBody::Expunge).await?; conn.read_response(&mut response, RequiredResponses::empty()) .await?; debug!("EXPUNGE response: {}", &String::from_utf8_lossy(&response)); @@ -951,13 +931,13 @@ impl MailBackend for ImapType { conn_lck.unselect().await?; conn_lck - .send_command(format!("CREATE \"{}\"", path,).as_bytes()) + .send_command(CommandBody::create(path.as_str())?) .await?; conn_lck .read_response(&mut response, RequiredResponses::empty()) .await?; conn_lck - .send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes()) + .send_command(CommandBody::subscribe(path.as_str())?) .await?; conn_lck .read_response(&mut response, RequiredResponses::empty()) @@ -1013,7 +993,7 @@ impl MailBackend for ImapType { conn_lck.unselect().await?; if is_subscribed { conn_lck - .send_command(format!("UNSUBSCRIBE \"{}\"", &imap_path).as_bytes()) + .send_command(CommandBody::unsubscribe(imap_path.as_str())?) .await?; conn_lck .read_response(&mut response, RequiredResponses::empty()) @@ -1021,7 +1001,7 @@ impl MailBackend for ImapType { } conn_lck - .send_command(debug!(format!("DELETE \"{}\"", &imap_path,)).as_bytes()) + .send_command(debug!(CommandBody::delete(imap_path.as_str())?)) .await?; conn_lck .read_response(&mut response, RequiredResponses::empty()) @@ -1050,23 +1030,24 @@ impl MailBackend for ImapType { let uid_store = self.uid_store.clone(); let connection = self.connection.clone(); Ok(Box::pin(async move { - let command: String; - { + let imap_path = { let mailboxes = uid_store.mailboxes.lock().await; if mailboxes[&mailbox_hash].is_subscribed() == new_val { return Ok(()); } - command = format!("SUBSCRIBE \"{}\"", mailboxes[&mailbox_hash].imap_path()); - } + mailboxes[&mailbox_hash].imap_path().to_string() + }; let mut response = Vec::with_capacity(8 * 1024); { let mut conn_lck = connection.lock().await; if new_val { - conn_lck.send_command(command.as_bytes()).await?; + conn_lck + .send_command(CommandBody::subscribe(imap_path.as_str())?) + .await?; } else { conn_lck - .send_command(format!("UN{}", command).as_bytes()) + .send_command(CommandBody::unsubscribe(imap_path.as_str())?) .await?; } conn_lck @@ -1125,7 +1106,9 @@ impl MailBackend for ImapType { } { let mut conn_lck = connection.lock().await; - conn_lck.send_command(debug!(command).as_bytes()).await?; + conn_lck + .send_command_raw(debug!(command).as_bytes()) + .await?; conn_lck .read_response(&mut response, RequiredResponses::empty()) .await?; @@ -1191,8 +1174,10 @@ impl MailBackend for ImapType { let mut conn = connection.lock().await; conn.examine_mailbox(mailbox_hash, &mut response, false) .await?; - conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query_str.trim()).as_bytes()) - .await?; + conn.send_command_raw( + format!("UID SEARCH CHARSET UTF-8 {}", query_str.trim()).as_bytes(), + ) + .await?; conn.read_response(&mut response, RequiredResponses::SEARCH) .await?; debug!( @@ -1310,7 +1295,7 @@ impl ImapType { let mut res = Vec::with_capacity(8 * 1024); futures::executor::block_on(timeout( self.server_conf.timeout, - conn.send_command(b"NOOP"), + conn.send_command(CommandBody::Noop), )) .unwrap() .unwrap(); @@ -1330,7 +1315,7 @@ impl ImapType { Ok(_) => { futures::executor::block_on(timeout( self.server_conf.timeout, - conn.send_command(input.as_bytes()), + conn.send_command_raw(input.as_bytes()), )) .unwrap() .unwrap(); @@ -1373,7 +1358,7 @@ impl ImapType { .iter() .any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS")); if has_list_status { - conn.send_command(b"LIST \"\" \"*\" RETURN (STATUS (MESSAGES UNSEEN))") + conn.send_command_raw(b"LIST \"\" \"*\" RETURN (STATUS (MESSAGES UNSEEN))") .await?; conn.read_response( &mut res, @@ -1381,7 +1366,7 @@ impl ImapType { ) .await?; } else { - conn.send_command(b"LIST \"\" \"*\"").await?; + conn.send_command(CommandBody::list("", "*")?).await?; conn.read_response(&mut res, RequiredResponses::LIST_REQUIRED) .await?; } @@ -1433,7 +1418,7 @@ impl ImapType { } } mailboxes.retain(|_, v| !v.hash.is_null()); - conn.send_command(b"LSUB \"\" \"*\"").await?; + conn.send_command(CommandBody::lsub("", "*")?).await?; conn.read_response(&mut res, RequiredResponses::LSUB_REQUIRED) .await?; debug!("LSUB reply: {}", String::from_utf8_lossy(&res)); @@ -1721,20 +1706,20 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result> { .await?; if max_uid_left > 0 { debug!("{} max_uid_left= {}", mailbox_hash, max_uid_left); - let command = if max_uid_left == 1 { - "UID FETCH 1 (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \ - BODYSTRUCTURE)" - .to_string() + let sequence_set = if max_uid_left == 1 { + SequenceSet::from(ONE) } else { - format!( - "UID FETCH {}:{} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS \ - (REFERENCES)] BODYSTRUCTURE)", - std::cmp::max(max_uid_left.saturating_sub(chunk_size), 1), - max_uid_left - ) + let min = std::cmp::max(max_uid_left.saturating_sub(chunk_size), 1); + let max = max_uid_left; + + SequenceSet::try_from(min..=max)? }; - debug!("sending {:?}", &command); - conn.send_command(command.as_bytes()).await?; + conn.send_command(CommandBody::Fetch { + sequence_set, + attributes: common_attributes(), + uid: true, + }) + .await?; conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED) .await .chain_err_summary(|| { diff --git a/melib/src/backends/imap/cache/sync.rs b/melib/src/backends/imap/cache/sync.rs index 6e1d70de..a8cfe8af 100644 --- a/melib/src/backends/imap/cache/sync.rs +++ b/melib/src/backends/imap/cache/sync.rs @@ -19,6 +19,13 @@ * along with meli. If not, see . */ +use imap_codec::{ + fetch::{FetchAttribute, MacroOrFetchAttributes}, + search::SearchKey, + sequence::SequenceSet, + status::StatusAttribute, +}; + use super::*; impl ImapConnection { @@ -128,14 +135,11 @@ impl ImapConnection { cache_handle.update_mailbox(mailbox_hash, &select_response)?; // 2. tag1 UID FETCH :* - self.send_command( - format!( - "UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \ - BODYSTRUCTURE)", - max_uid + 1 - ) - .as_bytes(), - ) + self.send_command(CommandBody::fetch( + max_uid + 1.., + common_attributes(), + true, + )?) .await?; self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED) .await?; @@ -229,12 +233,17 @@ impl ImapConnection { } mailbox_exists.lock().unwrap().insert_set(payload_hash_set); // 3. tag2 UID FETCH 1: FLAGS - if max_uid == 0 { - self.send_command("UID FETCH 1:* FLAGS".as_bytes()).await?; + let sequence_set = if max_uid == 0 { + SequenceSet::from(..) } else { - self.send_command(format!("UID FETCH 1:{} FLAGS", max_uid).as_bytes()) - .await?; - } + SequenceSet::try_from(..=max_uid)? + }; + self.send_command(CommandBody::Fetch { + sequence_set, + attributes: MacroOrFetchAttributes::FetchAttributes(vec![FetchAttribute::Flags]), + uid: true, + }) + .await?; self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED) .await?; //1) update cached flags for old messages; @@ -414,7 +423,7 @@ impl ImapConnection { // "SEARCH MODSEQ ". // 2. tag1 UID FETCH :* - self.send_command( + self.send_command_raw( format!( "UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \ BODYSTRUCTURE) (CHANGEDSINCE {})", @@ -511,7 +520,7 @@ impl ImapConnection { mailbox_exists.lock().unwrap().insert_set(payload_hash_set); // 3. tag2 UID FETCH 1: FLAGS if cached_max_uid == 0 { - self.send_command( + self.send_command_raw( format!( "UID FETCH 1:* FLAGS (CHANGEDSINCE {})", cached_highestmodseq @@ -520,7 +529,7 @@ impl ImapConnection { ) .await?; } else { - self.send_command( + self.send_command_raw( format!( "UID FETCH 1:{} FLAGS (CHANGEDSINCE {})", cached_max_uid, cached_highestmodseq @@ -575,7 +584,8 @@ impl ImapConnection { let mut valid_envs = BTreeSet::default(); // This should be UID SEARCH 1: but it's difficult to compare to cached // UIDs at the point of calling this function - self.send_command(b"UID SEARCH ALL").await?; + self.send_command(CommandBody::search(None, SearchKey::All, true)) + .await?; self.read_response(&mut response, RequiredResponses::SEARCH) .await?; //1) update cached flags for old messages; @@ -683,8 +693,11 @@ impl ImapConnection { .await?; if select_response.uidnext == 0 { /* UIDNEXT shouldn't be 0, since exists != 0 at this point */ - self.send_command(format!("STATUS \"{}\" (UIDNEXT)", mailbox_path).as_bytes()) - .await?; + self.send_command(CommandBody::status( + mailbox_path, + [StatusAttribute::UidNext].as_slice(), + )?) + .await?; self.read_response(&mut response, RequiredResponses::STATUS) .await?; let (_, status) = protocol_parser::status_response(response.as_slice())?; diff --git a/melib/src/backends/imap/connection.rs b/melib/src/backends/imap/connection.rs index 609cf7e7..973751a7 100644 --- a/melib/src/backends/imap/connection.rs +++ b/melib/src/backends/imap/connection.rs @@ -39,6 +39,20 @@ use std::{ }; use futures::io::{AsyncReadExt, AsyncWriteExt}; +#[cfg(feature = "deflate_compression")] +use imap_codec::extensions::compress::CompressionAlgorithm; +use imap_codec::{ + auth::{AuthMechanism, AuthMechanismOther}, + codec::{Encode, Fragment}, + command::{Command, CommandBody}, + core::{AString, Atom, NonEmptyVec, Tag}, + extensions::enable::CapabilityEnable, + mailbox::Mailbox, + search::SearchKey, + secret::Secret, + sequence::SequenceSet, + status::StatusAttribute, +}; use native_tls::TlsConnector; pub use smol::Async as AsyncWrapper; @@ -272,21 +286,15 @@ impl ImapStream { timeout: server_conf.timeout, }; if let ImapProtocol::ManageSieve = server_conf.protocol { - use data_encoding::BASE64; ret.read_response(&mut res).await?; - ret.send_command( - format!( - "AUTHENTICATE \"PLAIN\" \"{}\"", - BASE64.encode( - format!( - "\0{}\0{}", - &server_conf.server_username, &server_conf.server_password - ) - .as_bytes() - ) - ) - .as_bytes(), - ) + let credentials = format!( + "\0{}\0{}", + &server_conf.server_username, &server_conf.server_password + ); + ret.send_command(CommandBody::authenticate( + AuthMechanism::Plain, + Some(credentials.as_bytes()), + )) .await?; ret.read_response(&mut res).await?; return Ok((Default::default(), ret)); @@ -298,7 +306,7 @@ impl ImapStream { message: "Negotiating server capabilities.".into(), }, ); - ret.send_command(b"CAPABILITY").await?; + ret.send_command(CommandBody::Capability).await?; ret.read_response(&mut res).await?; let capabilities: std::result::Result, _> = res .split_rn() @@ -364,30 +372,26 @@ impl ImapStream { .join(" ") ))); } - ret.send_command( - format!("AUTHENTICATE XOAUTH2 {}", &server_conf.server_password).as_bytes(), - ) + let xoauth2 = base64::decode(&server_conf.server_password) + .map_err(|_| Error::new("Bad XOAUTH2 in config"))?; + // TODO(#222): Improve this as soon as imap-codec supports XOAUTH2. + ret.send_command(CommandBody::authenticate( + AuthMechanism::Other( + AuthMechanismOther::try_from(Atom::unchecked("XOAUTH2")).unwrap(), + ), + Some(&xoauth2), + )) .await?; } _ => { - ret.send_command( - format!( - r#"LOGIN "{}" {{{}}}"#, - &server_conf - .server_username - .replace('\\', r#"\\"#) - .replace('"', r#"\""#) - .replace('{', r#"\{"#) - .replace('}', r#"\}"#), - &server_conf.server_password.as_bytes().len() - ) - .as_bytes(), - ) + let username = AString::try_from(server_conf.server_username.as_str())?; + let password = AString::try_from(server_conf.server_password.as_str())?; + + ret.send_command(CommandBody::Login { + username, + password: Secret::new(password), + }) .await?; - // wait for "+ Ready for literal data" reply - ret.wait_for_continuation_request().await?; - ret.send_literal(server_conf.server_password.as_bytes()) - .await?; } } let tag_start = format!("M{} ", (ret.cmd_id - 1)); @@ -425,7 +429,7 @@ impl ImapStream { /* sending CAPABILITY after LOGIN automatically is an RFC recommendation, so * check for lazy servers */ drop(capabilities); - ret.send_command(b"CAPABILITY").await?; + ret.send_command(CommandBody::Capability).await?; ret.read_response(&mut res).await.unwrap(); let capabilities = protocol_parser::capabilities(&res)?.1; let capabilities = HashSet::from_iter(capabilities.into_iter().map(|s| s.to_vec())); @@ -500,7 +504,40 @@ impl ImapStream { Ok(()) } - pub async fn send_command(&mut self, command: &[u8]) -> Result<()> { + pub async fn send_command(&mut self, body: CommandBody<'_>) -> Result<()> { + timeout(self.timeout, async { + let command = { + let tag = Tag::unchecked(format!("M{}", self.cmd_id.to_string())); + + Command { tag, body } + }; + + for action in command.encode() { + match action { + Fragment::Line { data } => { + self.stream.write_all(&data).await?; + } + Fragment::Literal { data, sync } => { + // We only need to wait for a continuation request when we are about to + // send a synchronizing literal, i.e., when not using LITERAL+. + if sync { + self.wait_for_continuation_request().await?; + } + self.stream.write_all(&data).await?; + } + } + // Note: This is required for compression to work... + self.stream.flush().await?; + } + + self.cmd_id += 1; + + Ok(()) + }) + .await? + } + + pub async fn send_command_raw(&mut self, command: &[u8]) -> Result<()> { _ = timeout( self.timeout, try_await(async move { @@ -589,7 +626,7 @@ impl ImapConnection { if self.stream.is_ok() { let mut ret = Vec::new(); if let Err(err) = try_await(async { - self.send_command(b"NOOP").await?; + self.send_command(CommandBody::Noop).await?; self.read_response(&mut ret, RequiredResponses::empty()) .await }) @@ -630,14 +667,27 @@ impl ImapConnection { /* Upgrade to Condstore */ let mut ret = Vec::new(); if capabilities.contains(&b"ENABLE"[..]) { - self.send_command(b"ENABLE CONDSTORE").await?; + self.send_command(CommandBody::Enable { + capabilities: NonEmptyVec::from( + CapabilityEnable::CondStore, + ), + }) + .await?; self.read_response(&mut ret, RequiredResponses::empty()) .await?; } else { - self.send_command( - b"STATUS INBOX (UIDNEXT UIDVALIDITY UNSEEN MESSAGES HIGHESTMODSEQ)", - ) - .await?; + self.send_command(CommandBody::Status { + mailbox: Mailbox::Inbox, + attributes: vec![ + StatusAttribute::UidNext, + StatusAttribute::UidValidity, + StatusAttribute::Unseen, + StatusAttribute::Messages, + StatusAttribute::HighestModSeq, + ] + .into(), + }) + .await?; self.read_response(&mut ret, RequiredResponses::empty()) .await?; } @@ -648,7 +698,8 @@ impl ImapConnection { #[cfg(feature = "deflate_compression")] if capabilities.contains(&b"COMPRESS=DEFLATE"[..]) && deflate { let mut ret = Vec::new(); - self.send_command(b"COMPRESS DEFLATE").await?; + self.send_command(CommandBody::compress(CompressionAlgorithm::Deflate)) + .await?; self.read_response(&mut ret, RequiredResponses::empty()) .await?; match ImapResponse::try_from(ret.as_slice())? { @@ -798,7 +849,7 @@ impl ImapConnection { Ok(()) } - pub async fn send_command(&mut self, command: &[u8]) -> Result<()> { + pub async fn send_command(&mut self, command: CommandBody<'_>) -> Result<()> { if let Err(err) = try_await(async { self.stream.as_mut()?.send_command(command).await }).await { @@ -813,6 +864,21 @@ impl ImapConnection { } } + pub async fn send_command_raw(&mut self, command: &[u8]) -> Result<()> { + if let Err(err) = + try_await(async { self.stream.as_mut()?.send_command_raw(command).await }).await + { + self.stream = Err(err.clone()); + if err.kind.is_network() { + self.connect().await?; + } + Err(err) + } else { + *self.uid_store.is_online.lock().unwrap() = (SystemTime::now(), Ok(())); + Ok(()) + } + } + pub async fn send_literal(&mut self, data: &[u8]) -> Result<()> { if let Err(err) = try_await(async { self.stream.as_mut()?.send_literal(data).await }).await { @@ -863,7 +929,7 @@ impl ImapConnection { )) .set_kind(crate::error::ErrorKind::Bug)); } - self.send_command(format!("SELECT \"{}\"", imap_path).as_bytes()) + self.send_command(CommandBody::select(imap_path.as_str())?) .await?; self.read_response(ret, RequiredResponses::SELECT_REQUIRED) .await?; @@ -949,7 +1015,7 @@ impl ImapConnection { )) .set_kind(crate::error::ErrorKind::Bug)); } - self.send_command(format!("EXAMINE \"{}\"", &imap_path).as_bytes()) + self.send_command(CommandBody::examine(imap_path.as_str())?) .await?; self.read_response(ret, RequiredResponses::EXAMINE_REQUIRED) .await?; @@ -985,7 +1051,7 @@ impl ImapConnection { .iter() .any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT")) { - self.send_command(b"UNSELECT").await?; + self.send_command(CommandBody::Unselect).await?; self.read_response(&mut response, RequiredResponses::empty()) .await?; } else { @@ -1000,8 +1066,7 @@ impl ImapConnection { nonexistent.push('p'); } } - self.send_command(format!("SELECT \"{}\"", nonexistent).as_bytes()) - .await?; + self.send_command(CommandBody::select(nonexistent)?).await?; self.read_response(&mut response, RequiredResponses::NO_REQUIRED) .await?; } @@ -1025,9 +1090,14 @@ impl ImapConnection { _select_response: &SelectResponse, ) -> Result<()> { debug_assert!(low > 0); + self.send_command(CommandBody::search( + None, + SearchKey::SequenceSet(SequenceSet::try_from(low..)?), + true, + )) + .await?; + let mut response = Vec::new(); - self.send_command(format!("UID SEARCH {}:*", low).as_bytes()) - .await?; self.read_response(&mut response, RequiredResponses::SEARCH) .await?; let mut msn_index_lck = self.uid_store.msn_index.lock().unwrap(); diff --git a/melib/src/backends/imap/managesieve.rs b/melib/src/backends/imap/managesieve.rs index 64932599..fe32ff53 100644 --- a/melib/src/backends/imap/managesieve.rs +++ b/melib/src/backends/imap/managesieve.rs @@ -369,7 +369,7 @@ impl ManageSieveConnection { pub async fn listscripts(&mut self) -> Result, bool)>> { let mut ret = Vec::new(); - self.inner.send_command(b"Listscripts").await?; + self.inner.send_command_raw(b"Listscripts").await?; self.inner .read_response(&mut ret, RequiredResponses::empty()) .await?; diff --git a/melib/src/backends/imap/operations.rs b/melib/src/backends/imap/operations.rs index e2488de8..80296b77 100644 --- a/melib/src/backends/imap/operations.rs +++ b/melib/src/backends/imap/operations.rs @@ -21,6 +21,8 @@ use std::sync::Arc; +use imap_codec::fetch::FetchAttribute; + use super::*; use crate::{backends::*, email::*, error::Error}; @@ -68,8 +70,12 @@ impl BackendOp for ImapOp { conn.connect().await?; conn.examine_mailbox(mailbox_hash, &mut response, false) .await?; - conn.send_command(format!("UID FETCH {} (FLAGS RFC822)", uid).as_bytes()) - .await?; + conn.send_command(CommandBody::fetch( + uid, + vec![FetchAttribute::Flags, FetchAttribute::Rfc822], + true, + )?) + .await?; conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED) .await?; } @@ -127,7 +133,7 @@ impl BackendOp for ImapOp { conn.connect().await?; conn.examine_mailbox(mailbox_hash, &mut response, false) .await?; - conn.send_command(format!("UID FETCH {} FLAGS", uid).as_bytes()) + conn.send_command(CommandBody::fetch(uid, vec![FetchAttribute::Flags], true)?) .await?; conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED) .await?; diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index 76e6153c..eba1bb98 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -763,40 +763,6 @@ pub fn uid_fetch_flags_response(input: &[u8]) -> IResult<&[u8], (UID, (Flag, Vec Ok((input, (uid_flags.0, uid_flags.1))) } -macro_rules! flags_to_imap_list { - ($flags:ident) => {{ - let mut ret = String::new(); - if !($flags & Flag::REPLIED).is_empty() { - ret.push_str("\\Answered"); - } - if !($flags & Flag::FLAGGED).is_empty() { - if !ret.is_empty() { - ret.push(' '); - } - ret.push_str("\\Flagged"); - } - if !($flags & Flag::TRASHED).is_empty() { - if !ret.is_empty() { - ret.push(' '); - } - ret.push_str("\\Deleted"); - } - if !($flags & Flag::SEEN).is_empty() { - if !ret.is_empty() { - ret.push(' '); - } - ret.push_str("\\Seen"); - } - if !($flags & Flag::DRAFT).is_empty() { - if !ret.is_empty() { - ret.push(' '); - } - ret.push_str("\\Draft"); - } - ret - }}; -} - /* Input Example: * ============== * diff --git a/melib/src/backends/imap/untagged.rs b/melib/src/backends/imap/untagged.rs index ac058b72..744346eb 100644 --- a/melib/src/backends/imap/untagged.rs +++ b/melib/src/backends/imap/untagged.rs @@ -19,7 +19,9 @@ * along with meli. If not, see . */ -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; + +use imap_codec::{command::CommandBody, search::SearchKey, sequence::SequenceSet}; use super::{ImapConnection, MailboxSelection, UID}; use crate::{ @@ -32,6 +34,7 @@ use crate::{ RefreshEventKind::{self, *}, TagHash, }, + email::common_attributes, error::*, }; @@ -89,7 +92,12 @@ impl ImapConnection { n, self.uid_store.msn_index.lock().unwrap().get(&mailbox_hash) ); - self.send_command("UID SEARCH 1:*".as_bytes()).await?; + self.send_command(CommandBody::search( + None, + SearchKey::SequenceSet(SequenceSet::from(..)), + true, + )) + .await?; self.read_response(&mut response, RequiredResponses::SEARCH) .await?; let results = super::protocol_parser::search_results(&response)? @@ -199,7 +207,7 @@ impl ImapConnection { debug!("exists {}", n); try_fail!( mailbox_hash, - self.send_command(format!("FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", n).as_bytes()).await + self.send_command(CommandBody::fetch(n, common_attributes(), false)?).await self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await ); let mut v = match super::protocol_parser::fetch_responses(&response) { @@ -307,7 +315,7 @@ impl ImapConnection { UntaggedResponse::Recent(_) => { try_fail!( mailbox_hash, - self.send_command(b"UID SEARCH RECENT").await + self.send_command(CommandBody::search(None, SearchKey::Recent, true)).await self.read_response(&mut response, RequiredResponses::SEARCH).await ); match super::protocol_parser::search_results_raw(&response) @@ -334,7 +342,7 @@ impl ImapConnection { }; try_fail!( mailbox_hash, - self.send_command(command.as_bytes()).await + self.send_command_raw(command.as_bytes()).await self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await ); let mut v = match super::protocol_parser::fetch_responses(&response) { @@ -465,8 +473,12 @@ impl ImapConnection { } else { try_fail!( mailbox_hash, - self.send_command(format!("UID SEARCH {}", msg_seq).as_bytes()) - .await, + self.send_command(CommandBody::search( + None, + SearchKey::SequenceSet(SequenceSet::try_from(msg_seq)?), + true + )) + .await, self.read_response(&mut response, RequiredResponses::SEARCH) .await, ); diff --git a/melib/src/backends/imap/watch.rs b/melib/src/backends/imap/watch.rs index 61f37040..4fe0ca20 100644 --- a/melib/src/backends/imap/watch.rs +++ b/melib/src/backends/imap/watch.rs @@ -20,6 +20,8 @@ */ use std::sync::Arc; +use imap_codec::search::SearchKey; + use super::*; use crate::backends::SpecialUsageMailbox; @@ -120,7 +122,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> { } examine_updates(mailbox, &mut conn, &uid_store).await?; } - conn.send_command(b"IDLE").await?; + conn.send_command(CommandBody::Idle).await?; let mut blockn = ImapBlockingConnection::from(conn); let mut watch = std::time::Instant::now(); /* duration interval to send heartbeat */ @@ -144,7 +146,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> { .conn .read_response(&mut response, RequiredResponses::empty()) .await?; - blockn.conn.send_command(b"IDLE").await?; + blockn.conn.send_command(CommandBody::Idle).await?; let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?; main_conn_lck.connect().await?; continue; @@ -192,7 +194,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> { } blockn.conn.process_untagged(l).await?; } - blockn.conn.send_command(b"IDLE").await?; + blockn.conn.send_command(CommandBody::Idle).await?; } } } @@ -259,7 +261,7 @@ pub async fn examine_updates( .iter() .any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS")); if has_list_status { - conn.send_command( + conn.send_command_raw( format!( "LIST \"{}\" \"\" RETURN (STATUS (MESSAGES UNSEEN))", mailbox.imap_path() @@ -299,7 +301,8 @@ pub async fn examine_updates( } } } else { - conn.send_command(b"SEARCH UNSEEN").await?; + conn.send_command(CommandBody::search(None, SearchKey::Unseen, false)) + .await?; conn.read_response(&mut response, RequiredResponses::SEARCH) .await?; let unseen_count = protocol_parser::search_results(&response)?.1.len(); @@ -318,7 +321,8 @@ pub async fn examine_updates( if select_response.recent > 0 { /* UID SEARCH RECENT */ - conn.send_command(b"UID SEARCH RECENT").await?; + conn.send_command(CommandBody::search(None, SearchKey::Recent, true)) + .await?; conn.read_response(&mut response, RequiredResponses::SEARCH) .await?; let v = protocol_parser::search_results(response.as_slice()).map(|(_, v)| v)?; @@ -329,30 +333,15 @@ pub async fn examine_updates( ); return Ok(()); } - let mut cmd = "UID FETCH ".to_string(); - cmd.push_str(&v[0].to_string()); - if v.len() != 1 { - for n in v.into_iter().skip(1) { - cmd.push(','); - cmd.push_str(&n.to_string()); - } - } - cmd.push_str( - " (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", - ); - conn.send_command(cmd.as_bytes()).await?; + conn.send_command(CommandBody::fetch(v.as_slice(), common_attributes(), true)?) + .await?; conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED) .await?; } else if select_response.exists > mailbox.exists.lock().unwrap().len() { - conn.send_command( - format!( - "FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \ - BODYSTRUCTURE)", - std::cmp::max(mailbox.exists.lock().unwrap().len(), 1) - ) - .as_bytes(), - ) - .await?; + let min = std::cmp::max(mailbox.exists.lock().unwrap().len(), 1); + + conn.send_command(CommandBody::fetch(min.., common_attributes(), false)?) + .await?; conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED) .await?; } else { diff --git a/melib/src/email.rs b/melib/src/email.rs index 91842ed9..d2d6a413 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -112,6 +112,12 @@ pub use address::{Address, MessageID, References, StrBuild, StrBuilder}; pub use attachments::{Attachment, AttachmentBuilder}; pub use compose::{attachment_from_file, Draft}; pub use headers::*; +use imap_codec::{ + core::{AString, Atom, NonEmptyVec}, + fetch::{FetchAttribute, MacroOrFetchAttributes}, + flag::Flag as ImapCodecFlag, + section::Section, +}; use smallvec::SmallVec; use crate::{ @@ -122,6 +128,24 @@ use crate::{ TagHash, }; +// TODO(#222): Make this `const` as soon as it is possible. +pub(crate) fn common_attributes() -> MacroOrFetchAttributes<'static> { + MacroOrFetchAttributes::FetchAttributes(vec![ + FetchAttribute::Uid, + FetchAttribute::Flags, + FetchAttribute::Envelope, + FetchAttribute::BodyExt { + section: Some(Section::HeaderFields( + None, + NonEmptyVec::from(AString::from(Atom::unchecked("REFERENCES"))), + )), + partial: None, + peek: true, + }, + FetchAttribute::BodyStructure, + ]) +} + bitflags! { #[derive(Default, Serialize, Deserialize)] pub struct Flag: u8 { @@ -165,6 +189,39 @@ impl Flag { flag_impl!(fn is_flagged, Flag::FLAGGED); } +#[cfg(feature = "imap_backend")] +impl Flag { + pub(crate) fn derive_imap_codec_flags(&self) -> Vec { + let mut flags = Vec::new(); + + if self.is_passed() { + // This is from http://cr.yp.to/proto/maildir.html and not meaningful in IMAP. + } + + if self.is_replied() { + flags.push(ImapCodecFlag::Answered); + } + + if self.is_seen() { + flags.push(ImapCodecFlag::Seen); + } + + if self.is_trashed() { + flags.push(ImapCodecFlag::Deleted); + } + + if self.is_draft() { + flags.push(ImapCodecFlag::Draft); + } + + if self.is_flagged() { + flags.push(ImapCodecFlag::Flagged); + } + + flags + } +} + ///`Mail` holds both the envelope info of an email in its `envelope` field and /// the raw bytes that describe the email in `bytes`. Its body as an /// `melib::email::Attachment` can be parsed on demand diff --git a/melib/src/error.rs b/melib/src/error.rs index 357ed726..813e01cf 100644 --- a/melib/src/error.rs +++ b/melib/src/error.rs @@ -716,3 +716,96 @@ impl<'a> From<&'a Error> for Error { kind.clone() } } + +// ----- imap-codec ----- + +use imap_codec::{ + command::{AppendError, CopyError, ListError}, + core::LiteralError, + extensions::r#move::MoveError, + sequence::SequenceSetError, +}; + +impl From for Error { + #[inline] + fn from(error: LiteralError) -> Error { + Error { + summary: error.to_string().into(), + details: None, + source: Some(Arc::new(error)), + kind: ErrorKind::Configuration, + } + } +} + +impl From for Error { + #[inline] + fn from(error: SequenceSetError) -> Error { + Error { + summary: error.to_string().into(), + details: None, + source: Some(Arc::new(error)), + kind: ErrorKind::Bug, + } + } +} + +impl From> for Error +where + AppendError: fmt::Debug + fmt::Display + Sync + Send + 'static, +{ + #[inline] + fn from(error: AppendError) -> Error { + Error { + summary: error.to_string().into(), + details: None, + source: Some(Arc::new(error)), + kind: ErrorKind::Bug, + } + } +} + +impl From> for Error +where + CopyError: fmt::Debug + fmt::Display + Sync + Send + 'static, +{ + #[inline] + fn from(error: CopyError) -> Error { + Error { + summary: error.to_string().into(), + details: None, + source: Some(Arc::new(error)), + kind: ErrorKind::Bug, + } + } +} + +impl From> for Error +where + MoveError: fmt::Debug + fmt::Display + Sync + Send + 'static, +{ + #[inline] + fn from(error: MoveError) -> Error { + Error { + summary: error.to_string().into(), + details: None, + source: Some(Arc::new(error)), + kind: ErrorKind::Bug, + } + } +} + +impl From> for Error +where + ListError: fmt::Debug + fmt::Display + Sync + Send + 'static, +{ + #[inline] + fn from(error: ListError) -> Error { + Error { + summary: error.to_string().into(), + details: None, + source: Some(Arc::new(error)), + kind: ErrorKind::Bug, + } + } +}