From 0c3ce7943dbaae422211ea12bd8c4a7d8e7d6fa9 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Mon, 18 May 2020 14:12:10 -0400 Subject: [PATCH 01/41] Expose unilateral mailbox flag changes This is a backwards incompatible change, since it adds a variant to a public enum. --- src/parse.rs | 11 +++++++++-- src/types/mod.rs | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index b2dc002..b7418a9 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -322,8 +322,15 @@ fn handle_unilateral<'a>( Response::MailboxData(MailboxDatum::Recent(n)) => { unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap(); } - Response::MailboxData(MailboxDatum::Flags(_)) => { - // TODO: next breaking change: + Response::MailboxData(MailboxDatum::Flags(flags)) => { + unsolicited + .send(UnsolicitedResponse::Flags( + flags + .into_iter() + .map(|s| Flag::from(s.to_string())) + .collect(), + )) + .unwrap(); } Response::MailboxData(MailboxDatum::Exists(n)) => { unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap(); diff --git a/src/types/mod.rs b/src/types/mod.rs index 4a789e5..9757680 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -262,6 +262,15 @@ pub enum UnsolicitedResponse { /// sequence numbers 9, 8, 7, 6, and 5. // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited? Expunge(Seq), + + /// An unsolicited [`FLAGS` response](https://tools.ietf.org/html/rfc3501#section-7.2.6) that + /// identifies the flags (at a minimum, the system-defined flags) that are applicable in the + /// mailbox. Flags other than the system flags can also exist, depending on server + /// implementation. + /// + /// See [`Flag`] for details. + // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited? + Flags(Vec>), } /// This type wraps an input stream and a type that was constructed by parsing that input stream, From cf4aed569b62dfcc9dea0b54778bba4886de06d3 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Mon, 18 May 2020 14:12:34 -0400 Subject: [PATCH 02/41] Make UnsolicitedResponse non_exhaustive That way, as we discover additional unilateral responses in the future, we won't have to make a breaking change to add them. --- src/types/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/mod.rs b/src/types/mod.rs index 9757680..8a13903 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -211,6 +211,7 @@ pub use imap_proto::StatusAttribute; /// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder, /// so the user must take care when interpreting these. #[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum UnsolicitedResponse { /// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4). Status { From 02ce8cb5118e77e0ff6287418074a954ba9acfc6 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Mon, 18 May 2020 14:28:08 -0400 Subject: [PATCH 03/41] Bump MSRV for non_exhaustive --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5cc11d8..f9e126c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -47,12 +47,12 @@ jobs: - job: msrv pool: vmImage: ubuntu-latest - displayName: "Minimum supported Rust version: 1.36.0" + displayName: "Minimum supported Rust version: 1.40.0" dependsOn: [] steps: - template: install-rust.yml@templates parameters: - rust: 1.36.0 + rust: 1.40.0 - script: cargo check displayName: cargo check - job: integration From d6a659242083efaa1520249c22adf4d0ab3f1e44 Mon Sep 17 00:00:00 2001 From: Remy Vuong Date: Tue, 19 May 2020 10:41:38 +0200 Subject: [PATCH 04/41] Changed: a typo was fixed --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index dd6365b..a640668 100644 --- a/src/client.rs +++ b/src/client.rs @@ -456,7 +456,7 @@ impl Session { .and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) } - /// Fetch retreives data associated with a set of messages in the mailbox. + /// Fetch retrieves data associated with a set of messages in the mailbox. /// /// Note that the server *is* allowed to unilaterally include `FETCH` responses for other /// messages in the selected mailbox whose status has changed. See the note on [unilateral From e46b9dcfa1c44a1b77b84e60d7de8d0993a49b67 Mon Sep 17 00:00:00 2001 From: Remy Vuong Date: Tue, 19 May 2020 18:24:08 +0200 Subject: [PATCH 05/41] Added: STARTTLS example --- examples/starttls.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 examples/starttls.rs diff --git a/examples/starttls.rs b/examples/starttls.rs new file mode 100644 index 0000000..7f1588d --- /dev/null +++ b/examples/starttls.rs @@ -0,0 +1,68 @@ +/** + * Here's an example showing how to connect to the IMAP server with STARTTLS. + * The only difference with the `basic.rs` example is when using `imap::connect_starttls()` method + * instead of `imap::connect()` (l. 52) + * + * The following env vars are expected to be set: + * - IMAP_HOST + * - IMAP_USERNAME + * - IMAP_PASSWORD + * - IMAP_PORT (supposed to be 143) + */ + +extern crate imap; +extern crate native_tls; + +use native_tls::TlsConnector; +use std::env; +use std::error::Error; + +fn main() -> Result<(), Box> { + let imap_host = env::var("IMAP_HOST") + .expect("Missing or invalid env var: IMAP_HOST"); + let imap_username = env::var("IMAP_USERNAME") + .expect("Missing or invalid env var: IMAP_USERNAME"); + let imap_password = env::var("IMAP_PASSWORD") + .expect("Missing or invalid env var: IMAP_PASSWORD"); + let imap_port: u16 = env::var("IMAP_PORT") + .expect("Missing or invalid env var: IMAP_PORT") + .to_string() + .parse() + .unwrap(); + + if let Some(_email) = fetch_inbox_top(imap_host, imap_username, imap_password, imap_port)? { + eprintln!("OK :)"); + } + + Ok(()) +} + +fn fetch_inbox_top( + host: String, + username: String, + password: String, + port: u16, +) -> Result, Box> { + let domain: &str = host.as_str(); + + let tls = TlsConnector::builder().build().unwrap(); + + // we pass in the domain twice to check that the server's TLS + // certificate is valid for the domain we're connecting to. + let client = imap::connect_starttls( + (domain, port), + domain, + &tls, + ).unwrap(); + + // the client we have here is unauthenticated. + // to do anything useful with the e-mails, we need to log in + let mut _imap_session = client + .login(username.as_str(), password.as_str()) + .map_err(|e| e.0)?; + + // TODO Here you can process as you want. eg. search/fetch messages according to your needs. + + // This returns `Ok(None)` for the need of the example + Ok(None) +} From 1b3840187826ce7e68b2b1feb1bced8532292cf1 Mon Sep 17 00:00:00 2001 From: Remy Vuong Date: Wed, 20 May 2020 08:34:13 +0200 Subject: [PATCH 06/41] Changed: description update, plus code formatting --- examples/starttls.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/examples/starttls.rs b/examples/starttls.rs index 7f1588d..071021f 100644 --- a/examples/starttls.rs +++ b/examples/starttls.rs @@ -1,7 +1,8 @@ /** * Here's an example showing how to connect to the IMAP server with STARTTLS. * The only difference with the `basic.rs` example is when using `imap::connect_starttls()` method - * instead of `imap::connect()` (l. 52) + * instead of `imap::connect()` (l. 52), and so you can connect on port 143 instead of 993 + * as you have to when using TLS the entire way. * * The following env vars are expected to be set: * - IMAP_HOST @@ -9,7 +10,6 @@ * - IMAP_PASSWORD * - IMAP_PORT (supposed to be 143) */ - extern crate imap; extern crate native_tls; @@ -18,12 +18,11 @@ use std::env; use std::error::Error; fn main() -> Result<(), Box> { - let imap_host = env::var("IMAP_HOST") - .expect("Missing or invalid env var: IMAP_HOST"); - let imap_username = env::var("IMAP_USERNAME") - .expect("Missing or invalid env var: IMAP_USERNAME"); - let imap_password = env::var("IMAP_PASSWORD") - .expect("Missing or invalid env var: IMAP_PASSWORD"); + let imap_host = env::var("IMAP_HOST").expect("Missing or invalid env var: IMAP_HOST"); + let imap_username = + env::var("IMAP_USERNAME").expect("Missing or invalid env var: IMAP_USERNAME"); + let imap_password = + env::var("IMAP_PASSWORD").expect("Missing or invalid env var: IMAP_PASSWORD"); let imap_port: u16 = env::var("IMAP_PORT") .expect("Missing or invalid env var: IMAP_PORT") .to_string() @@ -49,11 +48,7 @@ fn fetch_inbox_top( // we pass in the domain twice to check that the server's TLS // certificate is valid for the domain we're connecting to. - let client = imap::connect_starttls( - (domain, port), - domain, - &tls, - ).unwrap(); + let client = imap::connect_starttls((domain, port), domain, &tls).unwrap(); // the client we have here is unauthenticated. // to do anything useful with the e-mails, we need to log in From e6268cbc47143095f210d3a8497bf064a88c09b8 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Sun, 21 Jun 2020 13:21:16 -0400 Subject: [PATCH 07/41] Bump MSRV --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5cc11d8..11a52c1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -47,12 +47,12 @@ jobs: - job: msrv pool: vmImage: ubuntu-latest - displayName: "Minimum supported Rust version: 1.36.0" + displayName: "Minimum supported Rust version: 1.40.0" dependsOn: [] steps: - template: install-rust.yml@templates parameters: - rust: 1.36.0 + rust: 1.40.0 # static-assertions (1.37+) and base64 (1.40+) - script: cargo check displayName: cargo check - job: integration From f0ebe4b568e5beeaed224b61495307edad09f302 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Sun, 21 Jun 2020 13:23:57 -0400 Subject: [PATCH 08/41] Update remote CI deps --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 11a52c1..de1917f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -79,11 +79,11 @@ resources: - repository: templates type: github name: crate-ci/azure-pipelines - ref: refs/heads/v0.3 + ref: refs/heads/v0.4 endpoint: jonhoo containers: - container: greenmail - image: greenmail/standalone:1.5.11 + image: greenmail/standalone:1.5.13 ports: - 3025:3025 - 3110:3110 From 45a8fcf27e3a7d1b79338c53779c3a7dc17e3aca Mon Sep 17 00:00:00 2001 From: Christian Krause Date: Sun, 21 Jun 2020 11:39:57 +0200 Subject: [PATCH 09/41] Add note about server greeting to Client::new --- src/client.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/client.rs b/src/client.rs index a640668..33f5d1a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -240,6 +240,29 @@ impl Client { /// /// This method primarily exists for writing tests that mock the underlying transport, but can /// also be used to support IMAP over custom tunnels. + /// + /// **Note:** In case you do need to use `Client::new` over `imap::connect`, you will need to + /// listen for the IMAP protocol server greeting before authenticating: + /// + /// ```rust,no_run + /// # extern crate imap; + /// # extern crate native_tls; + /// # use imap::Client; + /// # use native_tls::TlsConnector; + /// # use std::io; + /// # use std::net::TcpStream; + /// # fn main() { + /// # let server = "imap.example.com"; + /// # let username = ""; + /// # let password = ""; + /// # let tcp = TcpStream::connect((server, 993)).unwrap(); + /// # let ssl_connector = TlsConnector::builder().build().unwrap(); + /// # let tls = TlsConnector::connect(&ssl_connector, server.as_ref(), tcp).unwrap(); + /// let mut client = Client::new(tls); + /// client.read_greeting().unwrap(); + /// let session = client.login(username, password).unwrap(); + /// # } + /// ``` pub fn new(stream: T) -> Client { Client { conn: Connection { From 0104330e9dbb667310bd9798af604c2aede68ad3 Mon Sep 17 00:00:00 2001 From: Christian Krause Date: Sun, 21 Jun 2020 11:40:58 +0200 Subject: [PATCH 10/41] Add timeout example --- examples/README.md | 1 + examples/timeout.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 examples/timeout.rs diff --git a/examples/README.md b/examples/README.md index b1bde9d..1ba24a0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,3 +7,4 @@ Examples: * basic - This is a very basic example of using the client. * gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure appplication. * rustls - This demonstrates how to use Rustls instead of Openssl for secure connections (helpful for cross compilation). + * timeout - This demonstrates how to use timeouts while connecting to an IMAP server. diff --git a/examples/timeout.rs b/examples/timeout.rs new file mode 100644 index 0000000..d2d717e --- /dev/null +++ b/examples/timeout.rs @@ -0,0 +1,92 @@ +extern crate imap; +extern crate native_tls; + +use imap::Client; +use native_tls::TlsConnector; +use native_tls::TlsStream; +use std::env; +use std::error::Error; +use std::fmt; +use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; +use std::time::Duration; + +fn main() -> Result<(), Box> { + let server = env::var("IMAP_SERVER")?; + let port = env::var("IMAP_PORT").unwrap_or_else(|_| String::from("993")); + let port = port.parse()?; + + let username = env::var("IMAP_USER")?; + let password = env::var("IMAP_PASSWORD")?; + + let timeout = env::var("IMAP_TIMEOUT").unwrap_or_else(|_| String::from("1")); + let timeout = timeout.parse()?; + let timeout = Duration::from_secs(timeout); + + let tls = TlsConnector::builder().build()?; + + let client = connect_all_timeout((server.as_str(), port), server.as_str(), &tls, timeout)?; + + let mut session = client.login(&username, &password).map_err(|e| e.0)?; + + // do something productive with session + + session.logout()?; + + Ok(()) +} + +// connect to an IMAP host with a `Duration` timeout; note that this accepts only a single +// `SocketAddr` while `connect_all_timeout` does resolve the DNS entry and try to connect to all; +// this is necessary due to the difference of the function signatures of `TcpStream::connect` and +// `TcpStream::connect_timeout` +fn connect_timeout>( + addr: &SocketAddr, + domain: S, + ssl_connector: &TlsConnector, + timeout: Duration, +) -> Result>, Box> { + // the timeout is actually used with the initial TcpStream + let tcp_stream = TcpStream::connect_timeout(addr, timeout)?; + + let tls_stream = TlsConnector::connect(ssl_connector, domain.as_ref(), tcp_stream)?; + + let mut client = Client::new(tls_stream); + + // don't forget to wait for the IMAP protocol server greeting ;) + client.read_greeting()?; + + Ok(client) +} + +// resolve address and try to connect to all in order; note that this function is required to fully +// mimic `imap::connect` with the usage of `ToSocketAddrs` +fn connect_all_timeout>( + addr: A, + domain: S, + ssl_connector: &TlsConnector, + timeout: Duration, +) -> Result>, Box> { + let addrs = addr.to_socket_addrs()?; + + for addr in addrs { + match connect_timeout(&addr, &domain, ssl_connector, timeout) { + Ok(client) => return Ok(client), + Err(error) => eprintln!("couldn't connect to {}: {}", addr, error), + } + } + + Err(Box::new(TimeoutError)) +} + +// very simple timeout error; instead of printing the errors immediately like in +// `connect_all_timeout`, you may want to collect and return them +#[derive(Debug)] +struct TimeoutError; + +impl fmt::Display for TimeoutError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "all addresses failed to connect") + } +} + +impl Error for TimeoutError {} From 5794b0f8713ca135ab4b108e75949e3a63efb671 Mon Sep 17 00:00:00 2001 From: timando Date: Mon, 27 Jul 2020 23:28:02 +1000 Subject: [PATCH 11/41] Re-export Result and Error types (#170) --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 48fc3d9..dcc6826 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,7 @@ mod client; pub use crate::client::*; pub mod error; +pub use error::{Error, Result}; pub mod extensions; From 6c43b19f60eae7490d4a4dcfbd6c882dd809bf51 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Mon, 27 Jul 2020 09:33:49 -0400 Subject: [PATCH 12/41] Release 2.2.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b48ebcb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added + +### Changed + +### Removed + +## [2.2.0] - 2020-07-27 +### Added + + - Changelog + - STARTTLS example (#165) + - Timeout example (#168) + - Export `Result` and `Error` types (#170) + +### Changed + + - MSRV increased + - Better documentation of server greeting handling (#168) + +[Unreleased]: https://github.com/jonhoo/rust-imap/compare/v2.2.0...HEAD +[2.2.0]: https://github.com/jonhoo/rust-imap/compare/v2.1.2...v2.2.0 diff --git a/Cargo.toml b/Cargo.toml index d7172e4..a4e8976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "2.1.2" +version = "2.2.0" authors = ["Matt McCoy ", "Jon Gjengset "] documentation = "https://docs.rs/imap/" From ae07c9f31c8c53fec1d0cc175a9353ade182df6b Mon Sep 17 00:00:00 2001 From: mordak Date: Sun, 23 Aug 2020 11:02:57 -0500 Subject: [PATCH 13/41] Add append_with_flags (#171) Allows a client to set flags on a new message as it is appended to a mailbox. --- src/client.rs | 29 +++++++++++- src/types/mod.rs | 15 ++++++ tests/imap_integration.rs | 99 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 33f5d1a..a47c401 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1078,10 +1078,37 @@ impl Session { /// `EXISTS` response. If the server does not do so, the client MAY issue a `NOOP` command (or /// failing that, a `CHECK` command) after one or more `APPEND` commands. pub fn append, B: AsRef<[u8]>>(&mut self, mailbox: S, content: B) -> Result<()> { + self.append_with_flags(mailbox, content, &[]) + } + + /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) can take + /// an optional FLAGS parameter to set the flags on the new message. + /// + /// > If a flag parenthesized list is specified, the flags SHOULD be set + /// > in the resulting message; otherwise, the flag list of the + /// > resulting message is set to empty by default. In either case, the + /// > Recent flag is also set. + /// + /// The [`\Recent` flag](https://tools.ietf.org/html/rfc3501#section-2.3.2) is not + /// allowed as an argument to `APPEND` and will be filtered out if present in `flags`. + pub fn append_with_flags, B: AsRef<[u8]>>( + &mut self, + mailbox: S, + content: B, + flags: &[Flag<'_>], + ) -> Result<()> { let content = content.as_ref(); + let flagstr = flags + .iter() + .filter(|f| **f != Flag::Recent) + .map(|f| f.to_string()) + .collect::>() + .join(" "); + self.run_command(&format!( - "APPEND \"{}\" {{{}}}", + "APPEND \"{}\" ({}) {{{}}}", mailbox.as_ref(), + flagstr, content.len() ))?; let mut v = Vec::new(); diff --git a/src/types/mod.rs b/src/types/mod.rs index 4a789e5..fb57e4e 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -169,6 +169,21 @@ impl Flag<'static> { } } +impl<'a> fmt::Display for Flag<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Flag::Seen => write!(f, "{}", "\\Seen"), + Flag::Answered => write!(f, "{}", "\\Answered"), + Flag::Flagged => write!(f, "{}", "\\Flagged"), + Flag::Deleted => write!(f, "{}", "\\Deleted"), + Flag::Draft => write!(f, "{}", "\\Draft"), + Flag::Recent => write!(f, "{}", "\\Recent"), + Flag::MayCreate => write!(f, "{}", "\\*"), + Flag::Custom(ref s) => write!(f, "{}", s), + } + } +} + impl<'a> From for Flag<'a> { fn from(s: String) -> Self { if let Some(f) = Flag::system(&s) { diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index f639684..0a8bf76 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -231,3 +231,102 @@ fn list() { // TODO: make a subdir } + +#[test] +fn append() { + let to = "inbox-append1@localhost"; + + // make a message to append + let e: lettre::SendableEmail = lettre_email::Email::builder() + .from("sender@localhost") + .to(to) + .subject("My second e-mail") + .text("Hello world") + .build() + .unwrap() + .into(); + + // connect + let mut c = session(to); + let mbox = "INBOX"; + c.select(mbox).unwrap(); + //append + c.append(mbox, e.message_to_string().unwrap()).unwrap(); + + // now we should see the e-mail! + let inbox = c.uid_search("ALL").unwrap(); + // and the one message should have the first message sequence number + assert_eq!(inbox.len(), 1); + let uid = inbox.into_iter().next().unwrap(); + + // fetch the e-mail + let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap(); + assert_eq!(fetch.len(), 1); + let fetch = &fetch[0]; + assert_eq!(fetch.uid, Some(uid)); + let e = fetch.envelope().unwrap(); + assert_eq!(e.subject, Some(&b"My second e-mail"[..])); + + // and let's delete it to clean up + c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)") + .unwrap(); + c.expunge().unwrap(); + + // the e-mail should be gone now + let inbox = c.search("ALL").unwrap(); + assert_eq!(inbox.len(), 0); +} + +#[test] +fn append_with_flags() { + use imap::types::Flag; + + let to = "inbox-append2@localhost"; + + // make a message to append + let e: lettre::SendableEmail = lettre_email::Email::builder() + .from("sender@localhost") + .to(to) + .subject("My third e-mail") + .text("Hello world") + .build() + .unwrap() + .into(); + + // connect + let mut c = session(to); + let mbox = "INBOX"; + c.select(mbox).unwrap(); + //append + let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; + c.append_with_flags(mbox, e.message_to_string().unwrap(), flags) + .unwrap(); + + // now we should see the e-mail! + let inbox = c.uid_search("ALL").unwrap(); + // and the one message should have the first message sequence number + assert_eq!(inbox.len(), 1); + let uid = inbox.into_iter().next().unwrap(); + + // fetch the e-mail + let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap(); + assert_eq!(fetch.len(), 1); + let fetch = &fetch[0]; + assert_eq!(fetch.uid, Some(uid)); + let e = fetch.envelope().unwrap(); + assert_eq!(e.subject, Some(&b"My third e-mail"[..])); + + // check the flags + let setflags = fetch.flags(); + assert!(setflags.contains(&Flag::Seen)); + assert!(setflags.contains(&Flag::Flagged)); + + // and let's delete it to clean up + c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)") + .unwrap(); + c.expunge().unwrap(); + + // the e-mail should be gone now + let inbox = c.search("ALL").unwrap(); + assert_eq!(inbox.len(), 0); +} From 7868e312ff1349a667541b235053fc5c95c94854 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Sun, 23 Aug 2020 12:11:57 -0400 Subject: [PATCH 14/41] Release 2.3.0 with append_with_flags --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a4e8976..e4f5388 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "2.2.0" +version = "2.3.0" authors = ["Matt McCoy ", "Jon Gjengset "] documentation = "https://docs.rs/imap/" @@ -38,7 +38,7 @@ lazy_static = "1.4" [dev-dependencies] lettre = "0.9" lettre_email = "0.9" -rustls-connector = "0.11.0" +rustls-connector = "0.12.0" [[example]] name = "basic" From 753e1b9db138ca4f56a9f7bf94c508d7a3abe613 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 11 Nov 2020 11:25:21 -0500 Subject: [PATCH 15/41] Fix trivial clippy warnings. --- src/client.rs | 2 -- src/types/mod.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index a47c401..6d54e58 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,8 +1,6 @@ -use base64; use bufstream::BufStream; #[cfg(feature = "tls")] use native_tls::{TlsConnector, TlsStream}; -use nom; use std::collections::HashSet; use std::io::{Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; diff --git a/src/types/mod.rs b/src/types/mod.rs index fb57e4e..a91fa25 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -172,13 +172,13 @@ impl Flag<'static> { impl<'a> fmt::Display for Flag<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - Flag::Seen => write!(f, "{}", "\\Seen"), - Flag::Answered => write!(f, "{}", "\\Answered"), - Flag::Flagged => write!(f, "{}", "\\Flagged"), - Flag::Deleted => write!(f, "{}", "\\Deleted"), - Flag::Draft => write!(f, "{}", "\\Draft"), - Flag::Recent => write!(f, "{}", "\\Recent"), - Flag::MayCreate => write!(f, "{}", "\\*"), + Flag::Seen => write!(f, "\\Seen"), + Flag::Answered => write!(f, "\\Answered"), + Flag::Flagged => write!(f, "\\Flagged"), + Flag::Deleted => write!(f, "\\Deleted"), + Flag::Draft => write!(f, "\\Draft"), + Flag::Recent => write!(f, "\\Recent"), + Flag::MayCreate => write!(f, "\\*"), Flag::Custom(ref s) => write!(f, "{}", s), } } From b11b08954c6cefd16c4356cd3ec094c4bf29a6ca Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 11 Nov 2020 11:34:42 -0500 Subject: [PATCH 16/41] Fix clippy manual_non_exhaustive lint. --- src/error.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index c062d56..dc57422 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,6 +21,7 @@ pub type Result = result::Result; /// A set of errors that can occur in the IMAP client #[derive(Debug)] +#[non_exhaustive] pub enum Error { /// An `io::Error` that occurred while trying to read or write to a network stream. Io(IoError), @@ -43,8 +44,6 @@ pub enum Error { Validate(ValidateError), /// Error appending an e-mail. Append, - #[doc(hidden)] - __Nonexhaustive, } impl From for Error { @@ -99,7 +98,6 @@ impl fmt::Display for Error { Error::Bad(ref data) => write!(f, "Bad Response: {}", data), Error::ConnectionLost => f.write_str("Connection Lost"), Error::Append => f.write_str("Could not append mail to mailbox"), - Error::__Nonexhaustive => f.write_str("Unknown"), } } } @@ -119,7 +117,6 @@ impl StdError for Error { Error::No(_) => "No Response", Error::ConnectionLost => "Connection lost", Error::Append => "Could not append mail to mailbox", - Error::__Nonexhaustive => "Unknown", } } From c49e78b4d06ae374548a22ff3dda0d152e98e7ea Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 11 Nov 2020 11:37:33 -0500 Subject: [PATCH 17/41] Update imap-proto and nom dependencies. Add support for HIGHESTMODSEQ (RFC 4551) and VANISHED (RFC 7162), which allows users to quickly synchronize to a mailbox by fetching only changes since the last known highest mod sequence. --- Cargo.toml | 4 +-- src/client.rs | 6 ++-- src/parse.rs | 78 ++++++++++++++++++++++++++++++++++++++++---- src/types/mailbox.rs | 10 ++++-- src/types/mod.rs | 26 +++++++++++++++ 5 files changed, 111 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e4f5388..4d4be82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ default = ["tls"] native-tls = { version = "0.2.2", optional = true } regex = "1.0" bufstream = "0.1" -imap-proto = "0.10.0" -nom = "5.0" +imap-proto = "0.12.0" +nom = "6.0" base64 = "0.12" chrono = "0.4" lazy_static = "1.4" diff --git a/src/client.rs b/src/client.rs index 6d54e58..a6f63d6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1252,10 +1252,10 @@ impl Connection { }; let break_with = { - use imap_proto::{parse_response, Response, Status}; + use imap_proto::{Response, Status}; let line = &data[line_start..]; - match parse_response(line) { + match imap_proto::parser::parse_response(line) { Ok(( _, Response::Done { @@ -1606,6 +1606,7 @@ mod tests { permanent_flags: vec![], uid_next: Some(2), uid_validity: Some(1257842737), + highest_mod_seq: None, }; let mailbox_name = "INBOX"; let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name)); @@ -1652,6 +1653,7 @@ mod tests { ], uid_next: Some(2), uid_validity: Some(1257842737), + highest_mod_seq: None, }; let mailbox_name = "INBOX"; let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name)); diff --git a/src/parse.rs b/src/parse.rs index b2dc002..862bb45 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -44,7 +44,7 @@ where break Ok(things); } - match imap_proto::parse_response(lines) { + match imap_proto::parser::parse_response(lines) { Ok((rest, resp)) => { lines = rest; @@ -143,7 +143,7 @@ pub fn parse_capabilities( let f = |mut lines| { let mut caps = HashSet::new(); loop { - match imap_proto::parse_response(lines) { + match imap_proto::parser::parse_response(lines) { Ok((rest, Response::Capabilities(c))) => { lines = rest; caps.extend(c); @@ -179,7 +179,7 @@ pub fn parse_noop( break Ok(()); } - match imap_proto::parse_response(lines) { + match imap_proto::parser::parse_response(lines) { Ok((rest, data)) => { lines = rest; if let Some(resp) = handle_unilateral(data, unsolicited) { @@ -200,7 +200,7 @@ pub fn parse_mailbox( let mut mailbox = Mailbox::default(); loop { - match imap_proto::parse_response(lines) { + match imap_proto::parser::parse_response(lines) { Ok((rest, Response::Data { status, code, .. })) => { lines = rest; @@ -212,6 +212,9 @@ pub fn parse_mailbox( use imap_proto::ResponseCode; match code { + Some(ResponseCode::HighestModSeq(seq)) => { + mailbox.highest_mod_seq = Some(seq); + } Some(ResponseCode::UidValidity(uid)) => { mailbox.uid_validity = Some(uid); } @@ -254,7 +257,8 @@ pub fn parse_mailbox( } MailboxDatum::List { .. } | MailboxDatum::MetadataSolicited { .. } - | MailboxDatum::MetadataUnsolicited { .. } => {} + | MailboxDatum::MetadataUnsolicited { .. } + | MailboxDatum::Search { .. } => {} } } Ok((rest, Response::Expunge(n))) => { @@ -286,8 +290,8 @@ pub fn parse_ids( break Ok(ids); } - match imap_proto::parse_response(lines) { - Ok((rest, Response::IDs(c))) => { + match imap_proto::parser::parse_response(lines) { + Ok((rest, Response::MailboxData(MailboxDatum::Search(c)))) => { lines = rest; ids.extend(c); } @@ -331,6 +335,11 @@ fn handle_unilateral<'a>( Response::Expunge(n) => { unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap(); } + Response::Vanished { earlier, uids } => { + unsolicited + .send(UnsolicitedResponse::Vanished { earlier, uids }) + .unwrap(); + } res => { return Some(res); } @@ -547,4 +556,59 @@ mod tests { let ids: HashSet = ids.iter().cloned().collect(); assert_eq!(ids, HashSet::::new()); } + + #[test] + fn parse_vanished_test() { + // VANISHED can appear if the user has enabled QRESYNC (RFC 7162), in response to + // SELECT/EXAMINE (QRESYNC); UID FETCH (VANISHED); or EXPUNGE commands. In the first + // two cases the VANISHED respone will be a different type than the expected response + // and so goes into the unsolicited respones channel. In the last case, VANISHED is + // explicitly a response to an EXPUNGE command, but the semantics of EXPUNGE (one ID + // per response, multiple responses) vs VANISHED (a sequence-set of UIDs in a single + // response) are different enough that is isn't obvious what parse_expunge() should do. + // If we do nothing special, then the VANISHED response ends up in the unsolicited + // responses channel, which is at least consistent with the other cases where VANISHED + // can show up. + let lines = b"* VANISHED 3\r\n"; + let (mut send, recv) = mpsc::channel(); + let resp = parse_expunge(lines.to_vec(), &mut send).unwrap(); + assert!(resp.is_empty()); + + match recv.try_recv().unwrap() { + UnsolicitedResponse::Vanished { earlier, uids } => { + assert!(!earlier); + assert_eq!(uids.len(), 1); + assert_eq!(*uids[0].start(), 3); + assert_eq!(*uids[0].end(), 3); + } + what => panic!("Unexpected response in unsolicited responses: {:?}", what), + } + assert!(recv.try_recv().is_err()); + + // test VANISHED mixed with FETCH + let lines = b"* VANISHED (EARLIER) 3:8,12,50:60\r\n\ + * 49 FETCH (UID 117 FLAGS (\\Seen \\Answered) MODSEQ (90060115194045001))\r\n"; + + let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + match recv.try_recv().unwrap() { + UnsolicitedResponse::Vanished { earlier, uids } => { + assert!(earlier); + assert_eq!(uids.len(), 3); + assert_eq!(*uids[0].start(), 3); + assert_eq!(*uids[0].end(), 8); + assert_eq!(*uids[1].start(), 12); + assert_eq!(*uids[1].end(), 12); + assert_eq!(*uids[2].start(), 50); + assert_eq!(*uids[2].end(), 60); + } + what => panic!("Unexpected response in unsolicited responses: {:?}", what), + } + assert!(recv.try_recv().is_err()); + assert_eq!(fetches.len(), 1); + assert_eq!(fetches[0].message, 49); + assert_eq!(fetches[0].flags(), &[Flag::Seen, Flag::Answered]); + assert_eq!(fetches[0].uid, Some(117)); + assert_eq!(fetches[0].body(), None); + assert_eq!(fetches[0].header(), None); + } } diff --git a/src/types/mailbox.rs b/src/types/mailbox.rs index 1b87720..cec4166 100644 --- a/src/types/mailbox.rs +++ b/src/types/mailbox.rs @@ -35,6 +35,10 @@ pub struct Mailbox { /// The unique identifier validity value. See [`Uid`] for more details. If this is missing, /// the server does not support unique identifiers. pub uid_validity: Option, + + /// The highest mod sequence for this mailboxr. Used with + /// [Conditional STORE](https://tools.ietf.org/html/rfc4551#section-3.1.1). + pub highest_mod_seq: Option, } impl Default for Mailbox { @@ -47,6 +51,7 @@ impl Default for Mailbox { permanent_flags: Vec::new(), uid_next: None, uid_validity: None, + highest_mod_seq: None, } } } @@ -56,14 +61,15 @@ impl fmt::Display for Mailbox { write!( f, "flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\ - uid_next: {:?}, uid_validity: {:?}", + uid_next: {:?}, uid_validity: {:?}, highest_mod_seq: {:?}", self.flags, self.exists, self.recent, self.unseen, self.permanent_flags, self.uid_next, - self.uid_validity + self.uid_validity, + self.highest_mod_seq, ) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index a91fa25..5f6f647 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -277,6 +277,32 @@ pub enum UnsolicitedResponse { /// sequence numbers 9, 8, 7, 6, and 5. // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited? Expunge(Seq), + + /// An unsolicited [`VANISHED` response](https://tools.ietf.org/html/rfc7162#section-3.2.10) + /// that reports a sequence-set of `UID`s that have been expunged from the mailbox. + /// + /// The `VANISHED` response is similar to the `EXPUNGE` response and can be sent wherever + /// an `EXPUNGE` response can be sent. It can only be sent by the server if the client + /// has enabled [`QRESYNC`](https://tools.ietf.org/html/rfc7162). + /// + /// The `VANISHED` response has two forms, one with the `EARLIER` tag which is used to + /// respond to a `UID FETCH` or `SELECT/EXAMINE` command, and one without an `EARLIER` + /// tag, which is used to announce removals within an already selected mailbox. + /// + /// If using `QRESYNC`, the client can fetch new, updated and deleted `UID`s in a + /// single round trip by including the `(CHANGEDSINCE VANISHED)` + /// modifier to the `UID SEARCH` command, as described in + /// [RFC7162](https://tools.ietf.org/html/rfc7162#section-3.1.4). For example + /// `UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE 1234 VANISHED)` would return `FETCH` + /// results for all `UID`s added or modified since `MODSEQ` `1234`. Deleted `UID`s + /// will be present as a `VANISHED` response in the `Session::unsolicited_responses` + /// channel. + Vanished { + /// Whether the `EARLIER` tag was set on the response + earlier: bool, + /// The list of `UID`s which have been removed + uids: Vec>, + }, } /// This type wraps an input stream and a type that was constructed by parsing that input stream, From 22dae40ab5857fd78798f454d3d927014182f832 Mon Sep 17 00:00:00 2001 From: mordak Date: Sat, 14 Nov 2020 16:45:50 -0500 Subject: [PATCH 18/41] Fix typo in doc comment Co-authored-by: Jon Gjengset --- src/types/mailbox.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/mailbox.rs b/src/types/mailbox.rs index cec4166..962e05d 100644 --- a/src/types/mailbox.rs +++ b/src/types/mailbox.rs @@ -36,7 +36,7 @@ pub struct Mailbox { /// the server does not support unique identifiers. pub uid_validity: Option, - /// The highest mod sequence for this mailboxr. Used with + /// The highest mod sequence for this mailbox. Used with /// [Conditional STORE](https://tools.ietf.org/html/rfc4551#section-3.1.1). pub highest_mod_seq: Option, } From d381723deba922a2f28456ecd2d73240c09235db Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 14 Nov 2020 16:52:03 -0500 Subject: [PATCH 19/41] Add non_exhaustive to Mailbox. --- src/types/mailbox.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/mailbox.rs b/src/types/mailbox.rs index 962e05d..1cadba6 100644 --- a/src/types/mailbox.rs +++ b/src/types/mailbox.rs @@ -4,6 +4,7 @@ use std::fmt; /// Meta-information about an IMAP mailbox, as returned by /// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends. #[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[non_exhaustive] pub struct Mailbox { /// Defined flags in the mailbox. See the description of the [FLAGS /// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail. From a9788ad1e07508a4e9f3d18db7b8038d51303cae Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 21 Nov 2020 14:35:47 -0500 Subject: [PATCH 20/41] Add a Deleted type to wrap EXPUNGE and VANISHED responses. EXPUNGE may return either a series of EXPUNGE responses each with a single message sequence number, or a VANISHED response with a sequence set of UIDs. This adds a wrapper enum and some associated iterators to make it easy to handle these in the client. --- src/client.rs | 4 +- src/parse.rs | 73 ++++++++++++------ src/types/deleted.rs | 180 +++++++++++++++++++++++++++++++++++++++++++ src/types/mod.rs | 4 + 4 files changed, 234 insertions(+), 27 deletions(-) create mode 100644 src/types/deleted.rs diff --git a/src/client.rs b/src/client.rs index a6f63d6..149bc24 100644 --- a/src/client.rs +++ b/src/client.rs @@ -687,7 +687,7 @@ impl Session { /// The [`EXPUNGE` command](https://tools.ietf.org/html/rfc3501#section-6.4.3) permanently /// removes all messages that have [`Flag::Deleted`] set from the currently selected mailbox. /// The message sequence number of each message that is removed is returned. - pub fn expunge(&mut self) -> Result> { + pub fn expunge(&mut self) -> Result { self.run_command_and_read_response("EXPUNGE") .and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) } @@ -714,7 +714,7 @@ impl Session { /// /// Alternatively, the client may fall back to using just [`Session::expunge`], risking the /// unintended removal of some messages. - pub fn uid_expunge>(&mut self, uid_set: S) -> Result> { + pub fn uid_expunge>(&mut self, uid_set: S) -> Result { self.run_command_and_read_response(&format!("UID EXPUNGE {}", uid_set.as_ref())) .and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) } diff --git a/src/parse.rs b/src/parse.rs index 862bb45..0b57f52 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -127,13 +127,42 @@ pub fn parse_fetches( pub fn parse_expunge( lines: Vec, unsolicited: &mut mpsc::Sender, -) -> Result> { - let f = |resp| match resp { - Response::Expunge(id) => Ok(MapOrNot::Map(id)), - resp => Ok(MapOrNot::Not(resp)), - }; +) -> Result { + let mut lines: &[u8] = &lines; + let mut expunged = Vec::new(); + let mut vanished = Vec::new(); - unsafe { parse_many(lines, f, unsolicited).map(|ids| ids.take()) } + loop { + if lines.is_empty() { + break; + } + + match imap_proto::parser::parse_response(lines) { + Ok((rest, Response::Expunge(seq))) => { + lines = rest; + expunged.push(seq); + } + Ok((rest, Response::Vanished { earlier: _, uids })) => { + lines = rest; + vanished.extend(uids); + } + Ok((rest, data)) => { + lines = rest; + if let Some(resp) = handle_unilateral(data, unsolicited) { + return Err(resp.into()); + } + } + _ => { + return Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); + } + } + } + + if !vanished.is_empty() { + Ok(Deleted::from_vanished(vanished)) + } else { + Ok(Deleted::from_expunged(expunged)) + } } pub fn parse_capabilities( @@ -561,31 +590,25 @@ mod tests { fn parse_vanished_test() { // VANISHED can appear if the user has enabled QRESYNC (RFC 7162), in response to // SELECT/EXAMINE (QRESYNC); UID FETCH (VANISHED); or EXPUNGE commands. In the first - // two cases the VANISHED respone will be a different type than the expected response - // and so goes into the unsolicited respones channel. In the last case, VANISHED is - // explicitly a response to an EXPUNGE command, but the semantics of EXPUNGE (one ID - // per response, multiple responses) vs VANISHED (a sequence-set of UIDs in a single - // response) are different enough that is isn't obvious what parse_expunge() should do. - // If we do nothing special, then the VANISHED response ends up in the unsolicited - // responses channel, which is at least consistent with the other cases where VANISHED - // can show up. + // two cases the VANISHED response will be a different type than expected + // and so goes into the unsolicited responses channel. let lines = b"* VANISHED 3\r\n"; let (mut send, recv) = mpsc::channel(); let resp = parse_expunge(lines.to_vec(), &mut send).unwrap(); - assert!(resp.is_empty()); - match recv.try_recv().unwrap() { - UnsolicitedResponse::Vanished { earlier, uids } => { - assert!(!earlier); - assert_eq!(uids.len(), 1); - assert_eq!(*uids[0].start(), 3); - assert_eq!(*uids[0].end(), 3); - } - what => panic!("Unexpected response in unsolicited responses: {:?}", what), - } + // Should be not empty, and have no seqs + assert!(!resp.is_empty()); + assert_eq!(None, resp.seqs().next()); + + // Should have one UID response + let mut uids = resp.uids(); + assert_eq!(Some(3), uids.next()); + assert_eq!(None, uids.next()); + + // Should be nothing in the unsolicited responses channel assert!(recv.try_recv().is_err()); - // test VANISHED mixed with FETCH + // Test VANISHED mixed with FETCH let lines = b"* VANISHED (EARLIER) 3:8,12,50:60\r\n\ * 49 FETCH (UID 117 FLAGS (\\Seen \\Answered) MODSEQ (90060115194045001))\r\n"; diff --git a/src/types/deleted.rs b/src/types/deleted.rs new file mode 100644 index 0000000..90945c7 --- /dev/null +++ b/src/types/deleted.rs @@ -0,0 +1,180 @@ +use super::{Seq, Uid}; +use std::ops::RangeInclusive; + +/// An enum representing message sequence numbers or UID sequence sets returned +/// in response to a `EXPUNGE` command. +/// +/// The `EXPUNGE` command may return several `EXPUNGE` responses referencing +/// message sequence numbers, or it may return a `VANISHED` response referencing +/// multiple UID values in a sequence set if the client has enabled +/// [QRESYNC](https://tools.ietf.org/html/rfc7162#section-3.2.7). +/// +/// `Deleted` implements some iterators to make it easy to use. If the caller +/// knows that they should be receiving an `EXPUNGE` or `VANISHED` response, +/// then they can use [`seqs()`](#method.seqs) to get an iterator over `EXPUNGE` +/// message sequence numbers, or [`uids()`](#method.uids) to get an iterator over +/// the `VANISHED` UIDs. As a convenience `Deleted` also implents `IntoIterator` +/// which just returns an iterator over whatever is contained within. +/// +/// # Examples +/// ```no_run +/// # let domain = "imap.example.com"; +/// # let tls = native_tls::TlsConnector::builder().build().unwrap(); +/// # let client = imap::connect((domain, 993), domain, &tls).unwrap(); +/// # let mut session = client.login("name", "pw").unwrap(); +/// // Iterate over whatever is returned +/// if let Ok(deleted) = session.expunge() { +/// for id in &deleted { +/// // Do something with id +/// } +/// } +/// +/// // Expect a VANISHED response with UIDs +/// if let Ok(deleted) = session.expunge() { +/// for uid in deleted.uids() { +/// // Do something with uid +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +pub enum Deleted { + /// Message sequence numbers given in an `EXPUNGE` response. + Expunged(Vec), + /// Message UIDs given in a `VANISHED` response. + Vanished(Vec>), +} + +impl Deleted { + /// Construct a new `Deleted` value from a vector of message sequence + /// numbers returned in one or more `EXPUNGE` responses. + pub fn from_expunged(v: Vec) -> Self { + Deleted::Expunged(v) + } + + /// Construct a new `Deleted` value from a sequence-set of UIDs + /// returned in a `VANISHED` response + pub fn from_vanished(v: Vec>) -> Self { + Deleted::Vanished(v) + } + + /// Return an iterator over message sequence numbers from an `EXPUNGE` + /// response. If the client is expecting sequence numbers this function + /// can be used to ensure only sequence numbers returned in an `EXPUNGE` + /// response are processed. + pub fn seqs(&self) -> impl Iterator + '_ { + match self { + Deleted::Expunged(s) => s.iter(), + Deleted::Vanished(_) => [].iter(), + } + .copied() + } + + /// Return an iterator over UIDs returned in a `VANISHED` response. + /// If the client is expecting UIDs this function can be used to ensure + /// only UIDs are processed. + pub fn uids(&self) -> impl Iterator + '_ { + match self { + Deleted::Expunged(_) => [].iter(), + Deleted::Vanished(s) => s.iter(), + } + .flat_map(|range| range.clone()) + } + + /// Return if the set is empty + pub fn is_empty(&self) -> bool { + match self { + Deleted::Expunged(v) => v.is_empty(), + Deleted::Vanished(v) => v.is_empty(), + } + } +} + +impl<'a> IntoIterator for &'a Deleted { + type Item = u32; + type IntoIter = Box + 'a>; + + fn into_iter(self) -> Self::IntoIter { + match self { + Deleted::Expunged(_) => Box::new(self.seqs()), + Deleted::Vanished(_) => Box::new(self.uids()), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn seq() { + let seqs = Deleted::from_expunged(vec![3, 6, 9, 12]); + let mut i = seqs.into_iter(); + assert_eq!(Some(3), i.next()); + assert_eq!(Some(6), i.next()); + assert_eq!(Some(9), i.next()); + assert_eq!(Some(12), i.next()); + assert_eq!(None, i.next()); + + let seqs = Deleted::from_expunged(vec![]); + let mut i = seqs.into_iter(); + assert_eq!(None, i.next()); + } + + #[test] + fn seq_set() { + let uids = Deleted::from_vanished(vec![1..=1, 3..=5, 8..=9, 12..=12]); + let mut i = uids.into_iter(); + assert_eq!(Some(1), i.next()); + assert_eq!(Some(3), i.next()); + assert_eq!(Some(4), i.next()); + assert_eq!(Some(5), i.next()); + assert_eq!(Some(8), i.next()); + assert_eq!(Some(9), i.next()); + assert_eq!(Some(12), i.next()); + assert_eq!(None, i.next()); + + let uids = Deleted::from_vanished(vec![]); + assert_eq!(None, uids.into_iter().next()); + } + + #[test] + fn seqs() { + let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12]); + let mut count: u32 = 0; + for seq in seqs.seqs() { + count += 3; + assert_eq!(seq, count); + } + assert_eq!(count, 12); + } + + #[test] + fn uids() { + let uids: Deleted = Deleted::from_vanished(vec![1..=6]); + let mut count: u32 = 0; + for uid in uids.uids() { + count += 1; + assert_eq!(uid, count); + } + assert_eq!(count, 6); + } + + #[test] + fn generic_iteration() { + let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12]); + let mut count: u32 = 0; + for seq in &seqs { + count += 3; + assert_eq!(seq, count); + } + assert_eq!(count, 12); + + let uids: Deleted = Deleted::from_vanished(vec![1..=6]); + let mut count: u32 = 0; + for uid in &uids { + count += 1; + assert_eq!(uid, count); + } + assert_eq!(count, 6); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 5f6f647..5dbaec9 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -216,6 +216,9 @@ pub use self::name::{Name, NameAttribute}; mod capabilities; pub use self::capabilities::Capabilities; +mod deleted; +pub use self::deleted::Deleted; + /// re-exported from imap_proto; pub use imap_proto::StatusAttribute; @@ -350,6 +353,7 @@ impl ZeroCopy { /// /// Only safe if `D` contains no references into the underlying input stream (i.e., the `owned` /// passed to `ZeroCopy::new`). + #[allow(dead_code)] pub(crate) unsafe fn take(self) -> D { self.derived } From b87083c5b9b50ef5ea8cfdaf5f616a08efa6dc3f Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 5 Dec 2020 21:12:53 -0500 Subject: [PATCH 21/41] Bump minimum version. Via nom6 dependency bitvec. --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index de1917f..c9ac219 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -47,12 +47,12 @@ jobs: - job: msrv pool: vmImage: ubuntu-latest - displayName: "Minimum supported Rust version: 1.40.0" + displayName: "Minimum supported Rust version: 1.43.0" dependsOn: [] steps: - template: install-rust.yml@templates parameters: - rust: 1.40.0 # static-assertions (1.37+) and base64 (1.40+) + rust: 1.43.0 # nom6 depends on bitvec (1.43+) - script: cargo check displayName: cargo check - job: integration From 5bb1500d1484f21ad42ebd8e46375af8b7b26c6a Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 5 Dec 2020 21:26:28 -0500 Subject: [PATCH 22/41] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b48ebcb..ade21d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - VANISHED support in EXPUNGE responses and unsolicited responses (#172). ### Changed + - MSRV increased to 1.43 for nom6 and bitvec + - `expunge` and `uid_expunge` return `Result` instead of `Result>`. ### Removed From 166a0cb6b3a9d630557e04e41344ed477d5c084c Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sun, 6 Dec 2020 15:49:11 -0500 Subject: [PATCH 23/41] Link to QRESYNC RFC section describing VANISHED vs EXPUNGE responses. --- src/parse.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/parse.rs b/src/parse.rs index 0b57f52..9bf61b9 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -158,6 +158,10 @@ pub fn parse_expunge( } } + // If the server sends a VANISHED response then they must only send VANISHED + // in lieu of EXPUNGE responses for the rest of this connection, so it is + // always one or the other. + // https://tools.ietf.org/html/rfc7162#section-3.2.10 if !vanished.is_empty() { Ok(Deleted::from_vanished(vanished)) } else { From 8be583a9f71371d970cdbd03ce87c578dccda6b6 Mon Sep 17 00:00:00 2001 From: mordak Date: Mon, 7 Dec 2020 23:43:19 -0500 Subject: [PATCH 24/41] CI: Only install components when needed. (#173) --- azure-pipelines.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index de1917f..c0b9b84 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,9 +25,6 @@ jobs: - template: install-rust.yml@templates parameters: rust: $(rust) - components: - - rustfmt - - clippy - script: cargo check --all-targets displayName: cargo check - script: cargo test --examples @@ -36,10 +33,16 @@ jobs: displayName: cargo test --doc - script: cargo test --lib displayName: cargo test --lib - - script: cargo fmt --all -- --check + - script: | + set -e + rustup component add rustfmt + cargo fmt --all -- --check displayName: cargo fmt --check condition: and(eq( variables['rust'], 'beta' ), eq( variables['Agent.OS'], 'Linux' )) - - script: cargo clippy -- -D warnings + - script: | + set -e + rustup component add clippy + cargo clippy -- -D warnings displayName: cargo clippy condition: and(eq( variables['rust'], 'beta' ), eq( variables['Agent.OS'], 'Linux' )) # This represents the minimum Rust version supported. From ee56c8e42ba2fafd0270d16c248eb2dce3899248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Wed, 16 Dec 2020 00:25:38 +0800 Subject: [PATCH 25/41] feat: allow setting sent date on APPEND (#174) Fixes #60 --- src/client.rs | 34 ++++++++++++++++++++++++- tests/imap_integration.rs | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index a47c401..26a1432 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,5 +1,6 @@ use base64; use bufstream::BufStream; +use chrono::{DateTime, FixedOffset}; #[cfg(feature = "tls")] use native_tls::{TlsConnector, TlsStream}; use nom; @@ -1096,6 +1097,32 @@ impl Session { mailbox: S, content: B, flags: &[Flag<'_>], + ) -> Result<()> { + self.append_with_flags_and_date(mailbox, content, flags, None) + } + + /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) can take + /// an optional FLAGS parameter to set the flags on the new message. + /// + /// > If a flag parenthesized list is specified, the flags SHOULD be set + /// > in the resulting message; otherwise, the flag list of the + /// > resulting message is set to empty by default. In either case, the + /// > Recent flag is also set. + /// + /// The [`\Recent` flag](https://tools.ietf.org/html/rfc3501#section-2.3.2) is not + /// allowed as an argument to `APPEND` and will be filtered out if present in `flags`. + /// + /// Pass a date in order to set the date that the message was originally sent. + /// + /// > If a date-time is specified, the internal date SHOULD be set in + /// > the resulting message; otherwise, the internal date of the + /// > resulting message is set to the current date and time by default. + pub fn append_with_flags_and_date, B: AsRef<[u8]>>( + &mut self, + mailbox: S, + content: B, + flags: &[Flag<'_>], + date: impl Into>>, ) -> Result<()> { let content = content.as_ref(); let flagstr = flags @@ -1104,11 +1131,16 @@ impl Session { .map(|f| f.to_string()) .collect::>() .join(" "); + let datestr = match date.into() { + Some(date) => format!(" \"{}\"", date.format("%d-%h-%Y %T %z")), + None => "".to_string(), + }; self.run_command(&format!( - "APPEND \"{}\" ({}) {{{}}}", + "APPEND \"{}\" ({}){} {{{}}}", mailbox.as_ref(), flagstr, + datestr, content.len() ))?; let mut v = Vec::new(); diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index 0a8bf76..b37f3f3 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -1,8 +1,10 @@ +extern crate chrono; extern crate imap; extern crate lettre; extern crate lettre_email; extern crate native_tls; +use chrono::{FixedOffset, TimeZone}; use lettre::Transport; use std::net::TcpStream; @@ -330,3 +332,54 @@ fn append_with_flags() { let inbox = c.search("ALL").unwrap(); assert_eq!(inbox.len(), 0); } + +#[test] +fn append_with_flags_and_date() { + use imap::types::Flag; + + let to = "inbox-append3@localhost"; + + // make a message to append + let e: lettre::SendableEmail = lettre_email::Email::builder() + .from("sender@localhost") + .to(to) + .subject("My third e-mail") + .text("Hello world") + .build() + .unwrap() + .into(); + + // connect + let mut c = session(to); + let mbox = "INBOX"; + c.select(mbox).unwrap(); + // append + let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; + let date = FixedOffset::east(8 * 3600) + .ymd(2020, 12, 13) + .and_hms(13, 36, 36); + c.append_with_flags_and_date(mbox, e.message_to_string().unwrap(), flags, Some(date)) + .unwrap(); + + // now we should see the e-mail! + let inbox = c.uid_search("ALL").unwrap(); + // and the one message should have the first message sequence number + assert_eq!(inbox.len(), 1); + let uid = inbox.into_iter().next().unwrap(); + + // fetch the e-mail + let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap(); + assert_eq!(fetch.len(), 1); + let fetch = &fetch[0]; + assert_eq!(fetch.uid, Some(uid)); + assert_eq!(fetch.internal_date(), Some(date)); + + // and let's delete it to clean up + c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)") + .unwrap(); + c.expunge().unwrap(); + + // the e-mail should be gone now + let inbox = c.search("ALL").unwrap(); + assert_eq!(inbox.len(), 0); +} From 3386c267116605c53a402e98e72948ecf277f9a1 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Tue, 15 Dec 2020 08:29:57 -0800 Subject: [PATCH 26/41] Bump version for append_with_date --- CHANGELOG.md | 14 +++++++++++++- Cargo.toml | 6 +++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b48ebcb..ebfdbe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [2.4.0] - 2020-12-15 +### Added + + - `append_with_flags_and_date` (#174) + +## [2.3.0] - 2020-08-23 +### Added + + - `append_with_flags` (#171) + ## [2.2.0] - 2020-07-27 ### Added @@ -24,5 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MSRV increased - Better documentation of server greeting handling (#168) -[Unreleased]: https://github.com/jonhoo/rust-imap/compare/v2.2.0...HEAD +[Unreleased]: https://github.com/jonhoo/rust-imap/compare/v2.4.0...HEAD +[2.4.0]: https://github.com/jonhoo/rust-imap/compare/v2.2.0...v2.4.0 +[2.3.0]: https://github.com/jonhoo/rust-imap/compare/v2.2.0...v2.3.0 [2.2.0]: https://github.com/jonhoo/rust-imap/compare/v2.1.2...v2.2.0 diff --git a/Cargo.toml b/Cargo.toml index e4f5388..1dd1d68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "2.3.0" +version = "2.4.0" authors = ["Matt McCoy ", "Jon Gjengset "] documentation = "https://docs.rs/imap/" @@ -31,14 +31,14 @@ regex = "1.0" bufstream = "0.1" imap-proto = "0.10.0" nom = "5.0" -base64 = "0.12" +base64 = "0.13" chrono = "0.4" lazy_static = "1.4" [dev-dependencies] lettre = "0.9" lettre_email = "0.9" -rustls-connector = "0.12.0" +rustls-connector = "0.13.0" [[example]] name = "basic" From 469d338d5da0cb8c3996614b42677428abbb9aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Thu, 17 Dec 2020 00:20:48 +0800 Subject: [PATCH 27/41] refactor: combine all append_* + introduce AppendOptions --- src/client.rs | 54 ++++++++++++++++++--------------------- tests/imap_integration.rs | 25 ++++++++++++++---- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/client.rs b/src/client.rs index 26a1432..167ea70 100644 --- a/src/client.rs +++ b/src/client.rs @@ -84,6 +84,15 @@ pub struct Connection { pub greeting_read: bool, } +/// A set of options for the append command +#[derive(Default)] +pub struct AppendOptions<'a> { + /// Optional list of flags + pub flags: Option<&'a [Flag<'a>]>, + /// Optional internal date + pub date: Option>, +} + // `Deref` instances are so we can make use of the same underlying primitives in `Client` and // `Session` impl Deref for Client { @@ -1078,10 +1087,9 @@ impl Session { /// Specifically, the server will generally notify the client immediately via an untagged /// `EXISTS` response. If the server does not do so, the client MAY issue a `NOOP` command (or /// failing that, a `CHECK` command) after one or more `APPEND` commands. - pub fn append, B: AsRef<[u8]>>(&mut self, mailbox: S, content: B) -> Result<()> { - self.append_with_flags(mailbox, content, &[]) - } - + /// + /// -- TODO merge docs possibly move to AppendOptions + /// /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) can take /// an optional FLAGS parameter to set the flags on the new message. /// @@ -1092,48 +1100,36 @@ impl Session { /// /// The [`\Recent` flag](https://tools.ietf.org/html/rfc3501#section-2.3.2) is not /// allowed as an argument to `APPEND` and will be filtered out if present in `flags`. - pub fn append_with_flags, B: AsRef<[u8]>>( - &mut self, - mailbox: S, - content: B, - flags: &[Flag<'_>], - ) -> Result<()> { - self.append_with_flags_and_date(mailbox, content, flags, None) - } - - /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) can take - /// an optional FLAGS parameter to set the flags on the new message. /// - /// > If a flag parenthesized list is specified, the flags SHOULD be set - /// > in the resulting message; otherwise, the flag list of the - /// > resulting message is set to empty by default. In either case, the - /// > Recent flag is also set. - /// - /// The [`\Recent` flag](https://tools.ietf.org/html/rfc3501#section-2.3.2) is not - /// allowed as an argument to `APPEND` and will be filtered out if present in `flags`. + /// -- TODO merge docs possibly move to AppendOptions /// /// Pass a date in order to set the date that the message was originally sent. /// /// > If a date-time is specified, the internal date SHOULD be set in /// > the resulting message; otherwise, the internal date of the /// > resulting message is set to the current date and time by default. - pub fn append_with_flags_and_date, B: AsRef<[u8]>>( + pub fn append<'a, S: AsRef, B: AsRef<[u8]>>( &mut self, mailbox: S, content: B, - flags: &[Flag<'_>], - date: impl Into>>, + options: impl Into>>, ) -> Result<()> { let content = content.as_ref(); - let flagstr = flags + let options_ = options.into().unwrap_or(AppendOptions::default()); + + let flagstr = options_ + .flags + .unwrap_or(&[]) .iter() .filter(|f| **f != Flag::Recent) .map(|f| f.to_string()) .collect::>() .join(" "); - let datestr = match date.into() { - Some(date) => format!(" \"{}\"", date.format("%d-%h-%Y %T %z")), - None => "".to_string(), + + let datestr = if let Some(date) = options_.date { + format!(" \"{}\"", date.format("%d-%h-%Y %T %z")) + } else { + "".to_string() }; self.run_command(&format!( diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index b37f3f3..18dd3e8 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -253,7 +253,8 @@ fn append() { let mbox = "INBOX"; c.select(mbox).unwrap(); //append - c.append(mbox, e.message_to_string().unwrap()).unwrap(); + c.append(mbox, e.message_to_string().unwrap(), None) + .unwrap(); // now we should see the e-mail! let inbox = c.uid_search("ALL").unwrap(); @@ -301,8 +302,15 @@ fn append_with_flags() { c.select(mbox).unwrap(); //append let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; - c.append_with_flags(mbox, e.message_to_string().unwrap(), flags) - .unwrap(); + c.append( + mbox, + e.message_to_string().unwrap(), + imap::AppendOptions { + flags: Some(flags), + date: None, + }, + ) + .unwrap(); // now we should see the e-mail! let inbox = c.uid_search("ALL").unwrap(); @@ -358,8 +366,15 @@ fn append_with_flags_and_date() { let date = FixedOffset::east(8 * 3600) .ymd(2020, 12, 13) .and_hms(13, 36, 36); - c.append_with_flags_and_date(mbox, e.message_to_string().unwrap(), flags, Some(date)) - .unwrap(); + c.append( + mbox, + e.message_to_string().unwrap(), + imap::AppendOptions { + flags: Some(flags), + date: Some(date), + }, + ) + .unwrap(); // now we should see the e-mail! let inbox = c.uid_search("ALL").unwrap(); From 24445c5c652b6098d8c859411da74264105506dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Thu, 17 Dec 2020 00:41:26 +0800 Subject: [PATCH 28/41] feat: add AppendCmd builder --- src/client.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/client.rs b/src/client.rs index 167ea70..1e33ab5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -93,6 +93,41 @@ pub struct AppendOptions<'a> { pub date: Option>, } +/// A builder for the append command +#[derive(Default)] +pub struct AppendCmd<'a> { + flags: Vec<&'a Flag<'a>>, + date: Option>, +} + +impl<'a> AppendCmd<'a> { + /// Create a new AppendCmd builder + pub fn create() -> Self { + Self::default() + } + + /// Append a flag + pub fn flag(&mut self, flag: &'a Flag<'a>) -> &mut Self { + self.flags.push(flag); + self + } + + /// Set the internal date + pub fn internal_date(&mut self, date: DateTime) -> &mut Self { + self.date = Some(date); + self + } +} + +impl<'a> Into> for AppendCmd<'a> { + fn into(self) -> AppendOptions<'a> { + AppendOptions { + flags: Some(&self.flags[..]), + date: self.date, + } + } +} + // `Deref` instances are so we can make use of the same underlying primitives in `Client` and // `Session` impl Deref for Client { From cdf320fb0c87009bd3856220d263b62180637b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Thu, 17 Dec 2020 11:02:48 +0800 Subject: [PATCH 29/41] refactor: use AppendCmd instead of AppendOptions --- src/client.rs | 102 ++++++++++++++++---------------------- tests/imap_integration.rs | 32 +++++------- 2 files changed, 57 insertions(+), 77 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1e33ab5..6bbe119 100644 --- a/src/client.rs +++ b/src/client.rs @@ -84,28 +84,16 @@ pub struct Connection { pub greeting_read: bool, } -/// A set of options for the append command -#[derive(Default)] -pub struct AppendOptions<'a> { - /// Optional list of flags - pub flags: Option<&'a [Flag<'a>]>, - /// Optional internal date - pub date: Option>, -} - /// A builder for the append command -#[derive(Default)] -pub struct AppendCmd<'a> { +pub struct AppendCmd<'a, T: Read + Write> { + session: &'a Session, + content: &'a [u8], + mailbox: &'a str, flags: Vec<&'a Flag<'a>>, date: Option>, } -impl<'a> AppendCmd<'a> { - /// Create a new AppendCmd builder - pub fn create() -> Self { - Self::default() - } - +impl<'a, T: Read + Write> AppendCmd<'a, T> { /// Append a flag pub fn flag(&mut self, flag: &'a Flag<'a>) -> &mut Self { self.flags.push(flag); @@ -117,14 +105,40 @@ impl<'a> AppendCmd<'a> { self.date = Some(date); self } -} -impl<'a> Into> for AppendCmd<'a> { - fn into(self) -> AppendOptions<'a> { - AppendOptions { - flags: Some(&self.flags[..]), - date: self.date, + /// Run command when set up + #[must_use] + pub fn run(&self) -> Result<()> { + let flagstr = self + .flags + .into_iter() + .filter(|f| **f != Flag::Recent) + .map(|f| f.to_string()) + .collect::>() + .join(" "); + + let datestr = if let Some(date) = self.date { + format!(" \"{}\"", date.format("%d-%h-%Y %T %z")) + } else { + "".to_string() + }; + + self.session.run_command(&format!( + "APPEND \"{}\" ({}){} {{{}}}", + self.mailbox, + flagstr, + datestr, + self.content.len() + ))?; + let mut v = Vec::new(); + self.session.readline(&mut v)?; + if !v.starts_with(b"+") { + return Err(Error::Append); } + self.session.stream.write_all(self.content)?; + self.session.stream.write_all(b"\r\n")?; + self.session.stream.flush()?; + self.session.read_response().map(|_| ()) } } @@ -1147,42 +1161,14 @@ impl Session { &mut self, mailbox: S, content: B, - options: impl Into>>, - ) -> Result<()> { - let content = content.as_ref(); - let options_ = options.into().unwrap_or(AppendOptions::default()); - - let flagstr = options_ - .flags - .unwrap_or(&[]) - .iter() - .filter(|f| **f != Flag::Recent) - .map(|f| f.to_string()) - .collect::>() - .join(" "); - - let datestr = if let Some(date) = options_.date { - format!(" \"{}\"", date.format("%d-%h-%Y %T %z")) - } else { - "".to_string() - }; - - self.run_command(&format!( - "APPEND \"{}\" ({}){} {{{}}}", - mailbox.as_ref(), - flagstr, - datestr, - content.len() - ))?; - let mut v = Vec::new(); - self.readline(&mut v)?; - if !v.starts_with(b"+") { - return Err(Error::Append); + ) -> AppendCmd<'a, T> { + AppendCmd { + session: &self, + content: content.as_ref(), + mailbox: mailbox.as_ref(), + flags: Vec::new(), + date: None, } - self.stream.write_all(content)?; - self.stream.write_all(b"\r\n")?; - self.stream.flush()?; - self.read_response().map(|_| ()) } /// The [`SEARCH` command](https://tools.ietf.org/html/rfc3501#section-6.4.4) searches the diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index 18dd3e8..abecb0f 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -253,7 +253,8 @@ fn append() { let mbox = "INBOX"; c.select(mbox).unwrap(); //append - c.append(mbox, e.message_to_string().unwrap(), None) + c.append(mbox, e.message_to_string().unwrap()) + .run() .unwrap(); // now we should see the e-mail! @@ -302,15 +303,11 @@ fn append_with_flags() { c.select(mbox).unwrap(); //append let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; - c.append( - mbox, - e.message_to_string().unwrap(), - imap::AppendOptions { - flags: Some(flags), - date: None, - }, - ) - .unwrap(); + c.append(mbox, e.message_to_string().unwrap()) + .flag(Flag::Seen) + .flag(Flag::Flagged) + .run() + .unwrap(); // now we should see the e-mail! let inbox = c.uid_search("ALL").unwrap(); @@ -366,15 +363,12 @@ fn append_with_flags_and_date() { let date = FixedOffset::east(8 * 3600) .ymd(2020, 12, 13) .and_hms(13, 36, 36); - c.append( - mbox, - e.message_to_string().unwrap(), - imap::AppendOptions { - flags: Some(flags), - date: Some(date), - }, - ) - .unwrap(); + c.append(mbox, e.message_to_string().unwrap()) + .flag(Flag::Seen) + .flag(Flag::Flagged) + .internal_date(date) + .run() + .unwrap(); // now we should see the e-mail! let inbox = c.uid_search("ALL").unwrap(); From e6341ccfc0af9e617f15b1f59545ad3312bbe599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Thu, 17 Dec 2020 11:37:01 +0800 Subject: [PATCH 30/41] fix: pass session as &mut --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 6bbe119..c5cfc82 100644 --- a/src/client.rs +++ b/src/client.rs @@ -86,7 +86,7 @@ pub struct Connection { /// A builder for the append command pub struct AppendCmd<'a, T: Read + Write> { - session: &'a Session, + session: &'a mut Session, content: &'a [u8], mailbox: &'a str, flags: Vec<&'a Flag<'a>>, @@ -1163,7 +1163,7 @@ impl Session { content: B, ) -> AppendCmd<'a, T> { AppendCmd { - session: &self, + session: self, content: content.as_ref(), mailbox: mailbox.as_ref(), flags: Vec::new(), From b7bc84297979b7ad3f48e33300a149b406e8fb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Sun, 20 Dec 2020 13:03:54 +0800 Subject: [PATCH 31/41] fix: correct lifetimes and types for append --- src/client.rs | 19 ++++++++----------- tests/imap_integration.rs | 7 +++---- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/client.rs b/src/client.rs index c5cfc82..4fc4c25 100644 --- a/src/client.rs +++ b/src/client.rs @@ -89,13 +89,13 @@ pub struct AppendCmd<'a, T: Read + Write> { session: &'a mut Session, content: &'a [u8], mailbox: &'a str, - flags: Vec<&'a Flag<'a>>, + flags: Vec>, date: Option>, } impl<'a, T: Read + Write> AppendCmd<'a, T> { /// Append a flag - pub fn flag(&mut self, flag: &'a Flag<'a>) -> &mut Self { + pub fn flag(&mut self, flag: Flag<'a>) -> &mut Self { self.flags.push(flag); self } @@ -108,11 +108,12 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> { /// Run command when set up #[must_use] - pub fn run(&self) -> Result<()> { + pub fn run(&mut self) -> Result<()> { let flagstr = self .flags + .clone() .into_iter() - .filter(|f| **f != Flag::Recent) + .filter(|f| *f != Flag::Recent) .map(|f| f.to_string()) .collect::>() .join(" "); @@ -1157,15 +1158,11 @@ impl Session { /// > If a date-time is specified, the internal date SHOULD be set in /// > the resulting message; otherwise, the internal date of the /// > resulting message is set to the current date and time by default. - pub fn append<'a, S: AsRef, B: AsRef<[u8]>>( - &mut self, - mailbox: S, - content: B, - ) -> AppendCmd<'a, T> { + pub fn append<'a>(&'a mut self, mailbox: &'a str, content: &'a [u8]) -> AppendCmd<'a, T> { AppendCmd { session: self, - content: content.as_ref(), - mailbox: mailbox.as_ref(), + content, + mailbox, flags: Vec::new(), date: None, } diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index abecb0f..b32a0c0 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -253,7 +253,7 @@ fn append() { let mbox = "INBOX"; c.select(mbox).unwrap(); //append - c.append(mbox, e.message_to_string().unwrap()) + c.append(mbox, e.message_to_string().unwrap().as_bytes()) .run() .unwrap(); @@ -303,7 +303,7 @@ fn append_with_flags() { c.select(mbox).unwrap(); //append let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; - c.append(mbox, e.message_to_string().unwrap()) + c.append(mbox, e.message_to_string().unwrap().as_bytes()) .flag(Flag::Seen) .flag(Flag::Flagged) .run() @@ -359,11 +359,10 @@ fn append_with_flags_and_date() { let mbox = "INBOX"; c.select(mbox).unwrap(); // append - let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; let date = FixedOffset::east(8 * 3600) .ymd(2020, 12, 13) .and_hms(13, 36, 36); - c.append(mbox, e.message_to_string().unwrap()) + c.append(mbox, e.message_to_string().unwrap().as_bytes()) .flag(Flag::Seen) .flag(Flag::Flagged) .internal_date(date) From 5053cfbb3ec2037e0cc59684e24d31499759f1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Sun, 20 Dec 2020 13:04:31 +0800 Subject: [PATCH 32/41] feat: add 'flags' method to add multiple flags at once --- src/client.rs | 6 ++++++ tests/imap_integration.rs | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4fc4c25..74127ef 100644 --- a/src/client.rs +++ b/src/client.rs @@ -100,6 +100,12 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> { self } + /// Append an array of flags + pub fn flags(&mut self, flags: &'a [Flag<'a>]) -> &mut Self { + self.flags.append(&mut flags.to_vec()); + self + } + /// Set the internal date pub fn internal_date(&mut self, date: DateTime) -> &mut Self { self.date = Some(date); diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index b32a0c0..536efd2 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -304,8 +304,7 @@ fn append_with_flags() { //append let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; c.append(mbox, e.message_to_string().unwrap().as_bytes()) - .flag(Flag::Seen) - .flag(Flag::Flagged) + .flags(flags) .run() .unwrap(); From 74ef623fc589bc20ce1773f1ab9ef4729fe6729d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Sun, 20 Dec 2020 13:09:57 +0800 Subject: [PATCH 33/41] docs: move flag + date documentation to AppendCmd --- src/client.rs | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/client.rs b/src/client.rs index 74127ef..aa5623f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -94,7 +94,16 @@ pub struct AppendCmd<'a, T: Read + Write> { } impl<'a, T: Read + Write> AppendCmd<'a, T> { - /// Append a flag + /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) can take + /// an optional FLAGS parameter to set the flags on the new message. + /// + /// > If a flag parenthesized list is specified, the flags SHOULD be set + /// > in the resulting message; otherwise, the flag list of the + /// > resulting message is set to empty by default. In either case, the + /// > Recent flag is also set. + /// + /// The [`\Recent` flag](https://tools.ietf.org/html/rfc3501#section-2.3.2) is not + /// allowed as an argument to `APPEND` and will be filtered out if present in `flags`. pub fn flag(&mut self, flag: Flag<'a>) -> &mut Self { self.flags.push(flag); self @@ -106,14 +115,18 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> { self } - /// Set the internal date + /// Pass a date in order to set the date that the message was originally sent. + /// + /// > If a date-time is specified, the internal date SHOULD be set in + /// > the resulting message; otherwise, the internal date of the + /// > resulting message is set to the current date and time by default. pub fn internal_date(&mut self, date: DateTime) -> &mut Self { self.date = Some(date); self } - /// Run command when set up - #[must_use] + /// Run command + #[must_use = "always run a command once options are configured"] pub fn run(&mut self) -> Result<()> { let flagstr = self .flags @@ -1144,26 +1157,6 @@ impl Session { /// `EXISTS` response. If the server does not do so, the client MAY issue a `NOOP` command (or /// failing that, a `CHECK` command) after one or more `APPEND` commands. /// - /// -- TODO merge docs possibly move to AppendOptions - /// - /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) can take - /// an optional FLAGS parameter to set the flags on the new message. - /// - /// > If a flag parenthesized list is specified, the flags SHOULD be set - /// > in the resulting message; otherwise, the flag list of the - /// > resulting message is set to empty by default. In either case, the - /// > Recent flag is also set. - /// - /// The [`\Recent` flag](https://tools.ietf.org/html/rfc3501#section-2.3.2) is not - /// allowed as an argument to `APPEND` and will be filtered out if present in `flags`. - /// - /// -- TODO merge docs possibly move to AppendOptions - /// - /// Pass a date in order to set the date that the message was originally sent. - /// - /// > If a date-time is specified, the internal date SHOULD be set in - /// > the resulting message; otherwise, the internal date of the - /// > resulting message is set to the current date and time by default. pub fn append<'a>(&'a mut self, mailbox: &'a str, content: &'a [u8]) -> AppendCmd<'a, T> { AppendCmd { session: self, From 19af971c9a435bcb4f9d389b0deb33ac1d62249c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Mon, 21 Dec 2020 10:37:18 +0800 Subject: [PATCH 34/41] refactor: use extend instead of append Co-authored-by: Jon Gjengset --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index aa5623f..e7833e4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -110,8 +110,8 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> { } /// Append an array of flags - pub fn flags(&mut self, flags: &'a [Flag<'a>]) -> &mut Self { - self.flags.append(&mut flags.to_vec()); + pub fn flags(&mut self, flags: impl IntoIterator>) -> &mut Self { + self.flags.extend(flags); self } From 029da6fd5226c1aa607881a66907a8cfc4878325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Mon, 21 Dec 2020 11:19:42 +0800 Subject: [PATCH 35/41] refactor: move must_use to AppendCmd --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index e7833e4..5d4ddd4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -85,6 +85,7 @@ pub struct Connection { } /// A builder for the append command +#[must_use] pub struct AppendCmd<'a, T: Read + Write> { session: &'a mut Session, content: &'a [u8], @@ -126,7 +127,6 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> { } /// Run command - #[must_use = "always run a command once options are configured"] pub fn run(&mut self) -> Result<()> { let flagstr = self .flags From 8cd8a210085e6d86ec3a3859b96dbcf149a9094f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Mon, 21 Dec 2020 11:21:22 +0800 Subject: [PATCH 36/41] refactor: rename run to finish --- src/client.rs | 2 +- tests/imap_integration.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5d4ddd4..8b94376 100644 --- a/src/client.rs +++ b/src/client.rs @@ -127,7 +127,7 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> { } /// Run command - pub fn run(&mut self) -> Result<()> { + pub fn finish(&mut self) -> Result<()> { let flagstr = self .flags .clone() diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index 536efd2..9d443cd 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -254,7 +254,7 @@ fn append() { c.select(mbox).unwrap(); //append c.append(mbox, e.message_to_string().unwrap().as_bytes()) - .run() + .finish() .unwrap(); // now we should see the e-mail! @@ -305,7 +305,7 @@ fn append_with_flags() { let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; c.append(mbox, e.message_to_string().unwrap().as_bytes()) .flags(flags) - .run() + .finish() .unwrap(); // now we should see the e-mail! @@ -365,7 +365,7 @@ fn append_with_flags_and_date() { .flag(Flag::Seen) .flag(Flag::Flagged) .internal_date(date) - .run() + .finish() .unwrap(); // now we should see the e-mail! From b2f2e297c2f7d9cf04005bf5c0a76f923b1d4140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Mon, 21 Dec 2020 11:21:40 +0800 Subject: [PATCH 37/41] docs: improve docs --- src/client.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8b94376..0637514 100644 --- a/src/client.rs +++ b/src/client.rs @@ -110,7 +110,7 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> { self } - /// Append an array of flags + /// Set multiple flags at once. pub fn flags(&mut self, flags: impl IntoIterator>) -> &mut Self { self.flags.extend(flags); self @@ -126,7 +126,10 @@ impl<'a, T: Read + Write> AppendCmd<'a, T> { self } - /// Run command + /// Finishes up the command and executes it. + /// + /// Note: be sure to set flags and optional date before you + /// finish the command. pub fn finish(&mut self) -> Result<()> { let flagstr = self .flags From 80f54b1e81e339e67bcebe1edc276402056b0679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Tue, 22 Dec 2020 20:31:55 +0800 Subject: [PATCH 38/41] test: correct test for append_with_flags --- tests/imap_integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index 9d443cd..997b2f5 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -302,7 +302,7 @@ fn append_with_flags() { let mbox = "INBOX"; c.select(mbox).unwrap(); //append - let flags: &[Flag] = &[Flag::Seen, Flag::Flagged]; + let flags = vec![Flag::Seen, Flag::Flagged]; c.append(mbox, e.message_to_string().unwrap().as_bytes()) .flags(flags) .finish() From 9b6ff70e3b387432c4ba3ab2ca1a84259df8e40b Mon Sep 17 00:00:00 2001 From: Milo Mirate Date: Tue, 12 Jan 2021 23:30:38 -0500 Subject: [PATCH 39/41] Avoid trying to FETCH an empty set of messages (#177) Also, apply correct validation to FETCH arguments. --- CHANGELOG.md | 2 + src/client.rs | 107 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebfdbe4..1df8120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed + - Handle empty-set inputs to `fetch` and `uid_fetch` (#177) + ### Removed ## [2.4.0] - 2020-12-15 diff --git a/src/client.rs b/src/client.rs index 26a1432..2438a86 100644 --- a/src/client.rs +++ b/src/client.rs @@ -28,15 +28,74 @@ macro_rules! quote { }; } +trait OptionExt { + fn err(self) -> std::result::Result<(), E>; +} + +impl OptionExt for Option { + fn err(self) -> std::result::Result<(), E> { + match self { + Some(e) => Err(e), + None => Ok(()), + } + } +} + +/// Convert the input into what [the IMAP +/// grammar](https://tools.ietf.org/html/rfc3501#section-9) +/// calls "quoted", which is reachable from "string" et al. +/// Also ensure it doesn't contain a colliding command-delimiter (newline). fn validate_str(value: &str) -> Result { - let quoted = quote!(value); - if quoted.find('\n').is_some() { - return Err(Error::Validate(ValidateError('\n'))); - } - if quoted.find('\r').is_some() { - return Err(Error::Validate(ValidateError('\r'))); - } - Ok(quoted) + validate_str_noquote(value)?; + Ok(quote!(value)) +} + +/// Ensure the input doesn't contain a command-terminator (newline), but don't quote it like +/// `validate_str`. +/// This is helpful for things like the FETCH attributes, which, +/// per [the IMAP grammar](https://tools.ietf.org/html/rfc3501#section-9) may not be quoted: +/// +/// > fetch = "FETCH" SP sequence-set SP ("ALL" / "FULL" / "FAST" / +/// > fetch-att / "(" fetch-att *(SP fetch-att) ")") +/// > +/// > fetch-att = "ENVELOPE" / "FLAGS" / "INTERNALDATE" / +/// > "RFC822" [".HEADER" / ".SIZE" / ".TEXT"] / +/// > "BODY" ["STRUCTURE"] / "UID" / +/// > "BODY" section ["<" number "." nz-number ">"] / +/// > "BODY.PEEK" section ["<" number "." nz-number ">"] +/// +/// Note the lack of reference to any of the string-like rules or the quote characters themselves. +fn validate_str_noquote(value: &str) -> Result<&str> { + value + .matches(|c| c == '\n' || c == '\r') + .next() + .and_then(|s| s.chars().next()) + .map(|offender| Error::Validate(ValidateError(offender))) + .err()?; + Ok(value) +} + +/// This ensures the input doesn't contain a command-terminator or any other whitespace +/// while leaving it not-quoted. +/// This is needed because, per [the formal grammer given in RFC +/// 3501](https://tools.ietf.org/html/rfc3501#section-9), a sequence set consists of the following: +/// +/// > sequence-set = (seq-number / seq-range) *("," sequence-set) +/// > seq-range = seq-number ":" seq-number +/// > seq-number = nz-number / "*" +/// > nz-number = digit-nz *DIGIT +/// > digit-nz = %x31-39 +/// +/// Note the lack of reference to SP or any other such whitespace terminals. +/// Per this grammar, in theory we ought to be even more restrictive than "no whitespace". +fn validate_sequence_set(value: &str) -> Result<&str> { + value + .matches(|c: char| c.is_ascii_whitespace()) + .next() + .and_then(|s| s.chars().next()) + .map(|offender| Error::Validate(ValidateError(offender))) + .err()?; + Ok(value) } /// An authenticated IMAP session providing the usual IMAP commands. This type is what you get from @@ -543,12 +602,16 @@ impl Session { S1: AsRef, S2: AsRef, { - self.run_command_and_read_response(&format!( - "FETCH {} {}", - sequence_set.as_ref(), - query.as_ref() - )) - .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) + if sequence_set.as_ref().is_empty() { + parse_fetches(vec![], &mut self.unsolicited_responses_tx) + } else { + self.run_command_and_read_response(&format!( + "FETCH {} {}", + validate_sequence_set(sequence_set.as_ref())?, + validate_str_noquote(query.as_ref())? + )) + .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) + } } /// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are @@ -558,12 +621,16 @@ impl Session { S1: AsRef, S2: AsRef, { - self.run_command_and_read_response(&format!( - "UID FETCH {} {}", - uid_set.as_ref(), - query.as_ref() - )) - .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) + if uid_set.as_ref().is_empty() { + parse_fetches(vec![], &mut self.unsolicited_responses_tx) + } else { + self.run_command_and_read_response(&format!( + "UID FETCH {} {}", + validate_sequence_set(uid_set.as_ref())?, + validate_str_noquote(query.as_ref())? + )) + .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) + } } /// Noop always succeeds, and it does nothing. From bf9191527f5d5fe97a1de181123d4526558ccd51 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Tue, 12 Jan 2021 20:32:23 -0800 Subject: [PATCH 40/41] Release 2.4.1 --- CHANGELOG.md | 12 ++++++++---- Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df8120..27fbd5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - - Handle empty-set inputs to `fetch` and `uid_fetch` (#177) - ### Removed +## [2.4.1] - 2021-01-12 +### Changed + + - Handle empty-set inputs to `fetch` and `uid_fetch` (#177) + ## [2.4.0] - 2020-12-15 ### Added @@ -36,7 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MSRV increased - Better documentation of server greeting handling (#168) -[Unreleased]: https://github.com/jonhoo/rust-imap/compare/v2.4.0...HEAD -[2.4.0]: https://github.com/jonhoo/rust-imap/compare/v2.2.0...v2.4.0 +[Unreleased]: https://github.com/jonhoo/rust-imap/compare/v2.4.1...HEAD +[2.4.1]: https://github.com/jonhoo/rust-imap/compare/v2.4.0...v2.4.1 +[2.4.0]: https://github.com/jonhoo/rust-imap/compare/v2.3.0...v2.4.0 [2.3.0]: https://github.com/jonhoo/rust-imap/compare/v2.2.0...v2.3.0 [2.2.0]: https://github.com/jonhoo/rust-imap/compare/v2.1.2...v2.2.0 diff --git a/Cargo.toml b/Cargo.toml index 1dd1d68..f56fb92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "2.4.0" +version = "2.4.1" authors = ["Matt McCoy ", "Jon Gjengset "] documentation = "https://docs.rs/imap/" From 7e2ab19409156411ee77b788ac76f0975f052a87 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Sat, 6 Mar 2021 09:36:43 -0800 Subject: [PATCH 41/41] Prepare for 3.0.0 alpha --- Cargo.toml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f56fb92..05ad84b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,18 @@ [package] name = "imap" -version = "2.4.1" -authors = ["Matt McCoy ", - "Jon Gjengset "] +version = "3.0.0-alpha.1" +authors = ["Jon Gjengset ", + "Matt McCoy "] documentation = "https://docs.rs/imap/" repository = "https://github.com/jonhoo/rust-imap" homepage = "https://github.com/jonhoo/rust-imap" description = "IMAP client for Rust" -readme = "README.md" -license = "Apache-2.0/MIT" +license = "Apache-2.0 OR MIT" edition = "2018" keywords = ["email", "imap"] categories = ["email", "network-programming"] -[badges] -azure-devops = { project = "jonhoo/jonhoo", pipeline = "imap", build = "11" } -codecov = { repository = "jonhoo/rust-imap", branch = "master", service = "github" } -maintenance = { status = "actively-developed" } -is-it-maintained-issue-resolution = { repository = "jonhoo/rust-imap" } -is-it-maintained-open-issues = { repository = "jonhoo/rust-imap" } - [features] tls = ["native-tls"] default = ["tls"]