From d6a659242083efaa1520249c22adf4d0ab3f1e44 Mon Sep 17 00:00:00 2001 From: Remy Vuong Date: Tue, 19 May 2020 10:41:38 +0200 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 07/17] 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 08/17] 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 09/17] 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 10/17] 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 11/17] 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 8be583a9f71371d970cdbd03ce87c578dccda6b6 Mon Sep 17 00:00:00 2001 From: mordak Date: Mon, 7 Dec 2020 23:43:19 -0500 Subject: [PATCH 12/17] 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 13/17] 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 14/17] 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 9b6ff70e3b387432c4ba3ab2ca1a84259df8e40b Mon Sep 17 00:00:00 2001 From: Milo Mirate Date: Tue, 12 Jan 2021 23:30:38 -0500 Subject: [PATCH 15/17] 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 16/17] 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 17/17] 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"]