From 7204697dd919248f086d6276f37e5ba87fd37c3b Mon Sep 17 00:00:00 2001 From: mordak Date: Mon, 10 May 2021 21:39:46 -0500 Subject: [PATCH] Add ClientBuilder helper to make setting up TLS connections easy. (#197) Also replaces connect() and connect_starttls() with ClientBuilder. --- .cirrus.yml | 10 +-- Cargo.toml | 14 ++++ README.md | 14 +--- azure-pipelines.yml | 20 ++++- codecov.yml | 17 ++++ examples/README.md | 4 +- examples/basic.rs | 8 +- examples/gmail_oauth2.rs | 12 +-- examples/idle.rs | 6 +- examples/rustls.rs | 14 +--- examples/starttls.rs | 20 ++--- examples/timeout.rs | 3 +- src/client.rs | 159 +++++++------------------------------- src/client_builder.rs | 144 ++++++++++++++++++++++++++++++++++ src/error.rs | 19 ++++- src/extensions/idle.rs | 16 +++- src/lib.rs | 17 ++-- src/types/deleted.rs | 8 +- tests/imap_integration.rs | 46 ++++++----- 19 files changed, 314 insertions(+), 237 deletions(-) create mode 100644 codecov.yml create mode 100644 src/client_builder.rs diff --git a/.cirrus.yml b/.cirrus.yml index e5b9afd..eab595f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -11,12 +11,12 @@ task: - . $HOME/.cargo/env check_script: - . $HOME/.cargo/env - - cargo check --all-targets + - cargo check --all-targets --all-features build_script: - . $HOME/.cargo/env - - cargo build --all-targets --verbose + - cargo build --all-targets --verbose --all-features test_script: - . $HOME/.cargo/env - - cargo test --examples - - cargo test --doc - - cargo test --lib + - cargo test --examples --all-features + - cargo test --doc --all-features + - cargo test --lib --all-features diff --git a/Cargo.toml b/Cargo.toml index 929eca9..37b678c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,12 @@ categories = ["email", "network-programming"] [features] tls = ["native-tls"] +rustls-tls = ["rustls-connector"] default = ["tls"] [dependencies] native-tls = { version = "0.2.2", optional = true } +rustls-connector = { version = "0.13.1", optional = true } regex = "1.0" bufstream = "0.1" imap-proto = "0.14.1" @@ -45,6 +47,18 @@ required-features = ["default"] name = "idle" required-features = ["default"] +[[example]] +name = "rustls" +required-features = ["rustls-tls"] + +[[example]] +name = "starttls" +required-features = ["default"] + +[[example]] +name = "timeout" +required-features = ["default"] + [[test]] name = "imap_integration" required-features = ["default"] diff --git a/README.md b/README.md index 2e74966..af2f8aa 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ results](https://dev.azure.com/jonhoo/jonhoo/_build/latest?definitionId=11&branc [@jonhoo]: https://thesquareplanet.com/ -To connect, use the [`connect`] function. This gives you an unauthenticated [`Client`]. You can +To connect, use the [`ClientBuilder`]. This gives you an unauthenticated [`Client`]. You can then use [`Client::login`] or [`Client::authenticate`] to perform username/password or challenge/response authentication respectively. This in turn gives you an authenticated [`Session`], which lets you access the mailboxes at the server. @@ -34,16 +34,9 @@ in the documentation for the various types and methods and read the raw text the Below is a basic client example. See the `examples/` directory for more. ```rust -extern crate imap; -extern crate native_tls; - fn fetch_inbox_top() -> imap::error::Result> { - let domain = "imap.example.com"; - let tls = native_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((domain, 993), domain, &tls).unwrap(); + let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?; // the client we have here is unauthenticated. // to do anything useful with the e-mails, we need to log in @@ -90,7 +83,8 @@ default-features = false ``` Even without `native_tls`, you can still use TLS by leveraging the pure Rust `rustls` -crate. See the example/rustls.rs file for a working example. +crate, which is enabled with the `rustls-tls` feature. See the example/rustls.rs file +for a working example. ## Running the test suite diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8a1ae9a..5f2d167 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,13 +25,13 @@ jobs: - template: install-rust.yml@templates parameters: rust: $(rust) - - script: cargo check --all-targets + - script: cargo check --all-targets --all-features displayName: cargo check - - script: cargo test --examples + - script: cargo test --examples --all-features displayName: cargo test --examples - - script: cargo test --doc + - script: cargo test --doc --all-features displayName: cargo test --doc - - script: cargo test --lib + - script: cargo test --lib --all-features displayName: cargo test --lib - script: | set -e @@ -75,6 +75,18 @@ jobs: greenmail: greenmail env: TEST_HOST: greenmail + - job: features + displayName: "Check feature combinations" + pool: + vmImage: ubuntu-latest + steps: + - template: install-rust.yml@templates + parameters: + rust: stable + - script: cargo install cargo-hack + displayName: install cargo-hack + - script: cargo hack --feature-powerset check --all-targets + displayName: cargo hack resources: repositories: diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..7b19db2 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,17 @@ +coverage: + range: 70..100 + round: down + precision: 2 + status: + project: + default: + threshold: 2% + +# Tests aren't important for coverage +ignore: + - "tests" + +# Make less noisy comments +comment: + layout: "files" + require_changes: yes diff --git a/examples/README.md b/examples/README.md index 1ba24a0..b4a5964 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,5 +6,7 @@ This directory contains examples of working with the IMAP client. 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. + * idle - This is an example showing how to use IDLE to monitor a mailbox. * 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. + * starttls - This is an example showing how to use STARTTLS after connecting over plaintext. + * timeout - This demonstrates how to use timeouts while connecting to an IMAP server by using a custom TCP/TLS stream initialization and creating a `Client` directly instead of using the `ClientBuilder`. diff --git a/examples/basic.rs b/examples/basic.rs index b79ff44..3c0420a 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,5 +1,4 @@ extern crate imap; -extern crate native_tls; fn main() { // To connect to the gmail IMAP server with this you will need to allow unsecure apps access. @@ -9,12 +8,7 @@ fn main() { } fn fetch_inbox_top() -> imap::error::Result> { - let domain = "imap.example.com"; - let tls = native_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((domain, 993), domain, &tls).unwrap(); + let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?; // the client we have here is unauthenticated. // to do anything useful with the e-mails, we need to log in diff --git a/examples/gmail_oauth2.rs b/examples/gmail_oauth2.rs index 523d45f..a323f13 100644 --- a/examples/gmail_oauth2.rs +++ b/examples/gmail_oauth2.rs @@ -1,8 +1,5 @@ extern crate base64; extern crate imap; -extern crate native_tls; - -use native_tls::TlsConnector; struct GmailOAuth2 { user: String, @@ -25,11 +22,10 @@ fn main() { user: String::from("sombody@gmail.com"), access_token: String::from(""), }; - let domain = "imap.gmail.com"; - let port = 993; - let socket_addr = (domain, port); - let ssl_connector = TlsConnector::builder().build().unwrap(); - let client = imap::connect(socket_addr, domain, &ssl_connector).unwrap(); + + let client = imap::ClientBuilder::new("imap.gmail.com", 993) + .native_tls() + .expect("Could not connect to imap.gmail.com"); let mut imap_session = match client.authenticate("XOAUTH2", &gmail_auth) { Ok(c) => c, diff --git a/examples/idle.rs b/examples/idle.rs index b008b7c..a329a4c 100644 --- a/examples/idle.rs +++ b/examples/idle.rs @@ -1,4 +1,3 @@ -use native_tls::TlsConnector; use structopt::StructOpt; #[derive(StructOpt, Debug)] @@ -39,9 +38,10 @@ struct Opt { fn main() { let opt = Opt::from_args(); - let ssl_conn = TlsConnector::builder().build().unwrap(); - let client = imap::connect((opt.server.clone(), opt.port), opt.server, &ssl_conn) + let client = imap::ClientBuilder::new(opt.server.clone(), opt.port) + .native_tls() .expect("Could not connect to imap server"); + let mut imap = client .login(opt.username, opt.password) .expect("Could not authenticate"); diff --git a/examples/rustls.rs b/examples/rustls.rs index 2e67e37..5240826 100644 --- a/examples/rustls.rs +++ b/examples/rustls.rs @@ -1,9 +1,6 @@ extern crate imap; -extern crate rustls_connector; -use std::{env, error::Error, net::TcpStream}; - -use rustls_connector::RustlsConnector; +use std::{env, error::Error}; fn main() -> Result<(), Box> { // Read config from environment or .env file @@ -25,14 +22,7 @@ fn fetch_inbox_top( password: String, port: u16, ) -> Result, Box> { - // Setup Rustls TcpStream - let stream = TcpStream::connect((host.as_ref(), port))?; - let tls = RustlsConnector::default(); - let tlsstream = tls.connect(&host, stream)?; - - // 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::Client::new(tlsstream); + let client = imap::ClientBuilder::new(&host, port).rustls()?; // the client we have here is unauthenticated. // to do anything useful with the e-mails, we need to log in diff --git a/examples/starttls.rs b/examples/starttls.rs index 071021f..8b7e712 100644 --- a/examples/starttls.rs +++ b/examples/starttls.rs @@ -1,8 +1,9 @@ /** * 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), and so you can connect on port 143 instead of 993 - * as you have to when using TLS the entire way. + * + * The only difference is calling `starttls()` on the `ClientBuilder` before + * initiating the secure connection with `native_tls()` or `rustls()`, so you + * can connect on port 143 instead of 993. * * The following env vars are expected to be set: * - IMAP_HOST @@ -11,9 +12,7 @@ * - IMAP_PORT (supposed to be 143) */ extern crate imap; -extern crate native_tls; -use native_tls::TlsConnector; use std::env; use std::error::Error; @@ -42,13 +41,10 @@ fn fetch_inbox_top( 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(); + let client = imap::ClientBuilder::new(&host, port) + .starttls() + .native_tls() + .expect("Could not connect to server"); // the client we have here is unauthenticated. // to do anything useful with the e-mails, we need to log in diff --git a/examples/timeout.rs b/examples/timeout.rs index d2d717e..de502ab 100644 --- a/examples/timeout.rs +++ b/examples/timeout.rs @@ -58,8 +58,7 @@ fn connect_timeout>( 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` +// resolve address and try to connect to all in order fn connect_all_timeout>( addr: A, domain: S, diff --git a/src/client.rs b/src/client.rs index 0811f88..d9d705c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,11 +1,8 @@ use bufstream::BufStream; use chrono::{DateTime, FixedOffset}; use imap_proto::Response; -#[cfg(feature = "tls")] -use native_tls::{TlsConnector, TlsStream}; use std::collections::HashSet; use std::io::{Read, Write}; -use std::net::{TcpStream, ToSocketAddrs}; use std::ops::{Deref, DerefMut}; use std::str; use std::sync::mpsc; @@ -253,109 +250,6 @@ impl DerefMut for Session { } } -/// Connect to a server using a TLS-encrypted connection. -/// -/// The returned [`Client`] is unauthenticated; to access session-related methods (through -/// [`Session`]), use [`Client::login`] or [`Client::authenticate`]. -/// -/// The domain must be passed in separately from the `TlsConnector` so that the certificate of the -/// IMAP server can be validated. -/// -/// # Examples -/// -/// ```no_run -/// # extern crate native_tls; -/// # extern crate imap; -/// # use std::io; -/// # use native_tls::TlsConnector; -/// # fn main() { -/// let tls = TlsConnector::builder().build().unwrap(); -/// let client = imap::connect(("imap.example.org", 993), "imap.example.org", &tls).unwrap(); -/// # } -/// ``` -#[cfg(feature = "tls")] -pub fn connect>( - addr: A, - domain: S, - ssl_connector: &TlsConnector, -) -> Result>> { - match TcpStream::connect(addr) { - Ok(stream) => { - let ssl_stream = match TlsConnector::connect(ssl_connector, domain.as_ref(), stream) { - Ok(s) => s, - Err(e) => return Err(Error::TlsHandshake(e)), - }; - let mut socket = Client::new(ssl_stream); - - socket.read_greeting()?; - Ok(socket) - } - Err(e) => Err(Error::Io(e)), - } -} - -/// Connect to a server and upgrade to a TLS-encrypted connection. -/// -/// This is the [STARTTLS](https://tools.ietf.org/html/rfc2595) equivalent to [`connect`]. All -/// notes there also apply here. -/// -/// # Examples -/// -/// ```no_run -/// # extern crate native_tls; -/// # extern crate imap; -/// # use std::io; -/// # use native_tls::TlsConnector; -/// # fn main() { -/// let tls = TlsConnector::builder().build().unwrap(); -/// let client = imap::connect_starttls(("imap.example.org", 143), "imap.example.org", &tls).unwrap(); -/// # } -/// ``` -#[cfg(feature = "tls")] -pub fn connect_starttls>( - addr: A, - domain: S, - ssl_connector: &TlsConnector, -) -> Result>> { - match TcpStream::connect(addr) { - Ok(stream) => { - let mut socket = Client::new(stream); - socket.read_greeting()?; - socket.run_command_and_check_ok("STARTTLS")?; - TlsConnector::connect( - ssl_connector, - domain.as_ref(), - socket.conn.stream.into_inner()?, - ) - .map(Client::new) - .map_err(Error::TlsHandshake) - } - Err(e) => Err(Error::Io(e)), - } -} - -impl Client { - /// This will upgrade an IMAP client from using a regular TCP connection to use TLS. - /// - /// The domain parameter is required to perform hostname verification. - #[cfg(feature = "tls")] - pub fn secure>( - mut self, - domain: S, - ssl_connector: &TlsConnector, - ) -> Result>> { - // TODO This needs to be tested - self.run_command_and_check_ok("STARTTLS")?; - TlsConnector::connect( - ssl_connector, - domain.as_ref(), - self.conn.stream.into_inner()?, - ) - .map(Client::new) - .map_err(Error::TlsHandshake) - } -} - // As the pattern of returning the unauthenticated `Client` (a.k.a. `self`) back with a login error // is relatively common, it's abstacted away into a macro here. // @@ -375,27 +269,28 @@ macro_rules! ok_or_unauth_client_err { impl Client { /// Creates a new client over the given stream. /// - /// For an example of how to use this method to provide a pure-Rust TLS integration, see the - /// rustls.rs in the examples/ directory. + /// This method primarily exists for writing tests that mock the underlying transport, + /// but can also be used to support IMAP over custom tunnels. If you do not need to do + /// that, then it is simpler to use the [`ClientBuilder`](crate::ClientBuilder) to get + /// a new client. /// - /// This method primarily exists for writing tests that mock the underlying transport, but can - /// also be used to support IMAP over custom tunnels. + /// For an example, see `examples/timeout.rs` which uses a custom timeout on the + /// tcp stream. /// - /// **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: + /// **Note:** In case you do need to use `Client::new` instead of the `ClientBuilder` + /// 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; + /// # {} #[cfg(feature = "tls")] /// # fn main() { /// # let server = "imap.example.com"; /// # let username = ""; /// # let password = ""; /// # let tcp = TcpStream::connect((server, 993)).unwrap(); + /// # use native_tls::TlsConnector; /// # let ssl_connector = TlsConnector::builder().build().unwrap(); /// # let tls = TlsConnector::connect(&ssl_connector, server.as_ref(), tcp).unwrap(); /// let mut client = Client::new(tls); @@ -414,6 +309,15 @@ impl Client { } } + /// Yield the underlying connection for this Client. + /// + /// This consumes `self` since the Client is not much use without + /// an underlying transport. + pub(crate) fn into_inner(self) -> Result { + let res = self.conn.stream.into_inner()?; + Ok(res) + } + /// Log in to the IMAP server. Upon success a [`Session`](struct.Session.html) instance is /// returned; on error the original `Client` instance is returned in addition to the error. /// This is because `login` takes ownership of `self`, so in order to try again (e.g. after @@ -421,16 +325,10 @@ impl Client { /// transferred back to the caller. /// /// ```rust,no_run - /// # extern crate imap; - /// # extern crate native_tls; - /// # use std::io; - /// # use native_tls::TlsConnector; + /// # {} #[cfg(feature = "tls")] /// # fn main() { - /// # let tls_connector = TlsConnector::builder().build().unwrap(); - /// let client = imap::connect( - /// ("imap.example.org", 993), - /// "imap.example.org", - /// &tls_connector).unwrap(); + /// let client = imap::ClientBuilder::new("imap.example.org", 993) + /// .native_tls().unwrap(); /// /// match client.login("user", "pass") { /// Ok(s) => { @@ -463,10 +361,6 @@ impl Client { /// challenge. /// /// ```no_run - /// extern crate imap; - /// extern crate native_tls; - /// use native_tls::TlsConnector; - /// /// struct OAuth2 { /// user: String, /// access_token: String, @@ -482,14 +376,15 @@ impl Client { /// } /// } /// + /// # {} #[cfg(feature = "tls")] /// fn main() { /// let auth = OAuth2 { /// user: String::from("me@example.com"), /// access_token: String::from(""), /// }; - /// let domain = "imap.example.com"; - /// let tls = TlsConnector::builder().build().unwrap(); - /// let client = imap::connect((domain, 993), domain, &tls).unwrap(); + /// let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls() + /// .expect("Could not connect to server"); + /// /// match client.authenticate("XOAUTH2", &auth) { /// Ok(session) => { /// // you are successfully authenticated! @@ -1379,7 +1274,7 @@ impl Connection { Ok(v) } - fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { + pub(crate) fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { self.run_command_and_read_response(command).map(|_| ()) } diff --git a/src/client_builder.rs b/src/client_builder.rs new file mode 100644 index 0000000..01cac2d --- /dev/null +++ b/src/client_builder.rs @@ -0,0 +1,144 @@ +use crate::{Client, Result}; +use std::io::{Read, Write}; +use std::net::TcpStream; + +#[cfg(feature = "tls")] +use native_tls::{TlsConnector, TlsStream}; +#[cfg(feature = "rustls-tls")] +use rustls_connector::{RustlsConnector, TlsStream as RustlsStream}; + +/// A convenience builder for [`Client`] structs over various encrypted transports. +/// +/// Creating a [`Client`] using `native-tls` transport is straightforward: +/// ```no_run +/// # use imap::ClientBuilder; +/// # {} #[cfg(feature = "tls")] +/// # fn main() -> Result<(), imap::Error> { +/// let client = ClientBuilder::new("imap.example.com", 993).native_tls()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Similarly, if using the `rustls-tls` feature you can create a [`Client`] using rustls: +/// ```no_run +/// # use imap::ClientBuilder; +/// # {} #[cfg(feature = "rustls-tls")] +/// # fn main() -> Result<(), imap::Error> { +/// let client = ClientBuilder::new("imap.example.com", 993).rustls()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// To use `STARTTLS`, just call `starttls()` before one of the [`Client`]-yielding +/// functions: +/// ```no_run +/// # use imap::ClientBuilder; +/// # {} #[cfg(feature = "rustls-tls")] +/// # fn main() -> Result<(), imap::Error> { +/// let client = ClientBuilder::new("imap.example.com", 993) +/// .starttls() +/// .rustls()?; +/// # Ok(()) +/// # } +/// ``` +/// The returned [`Client`] is unauthenticated; to access session-related methods (through +/// [`Session`](crate::Session)), use [`Client::login`] or [`Client::authenticate`]. +pub struct ClientBuilder +where + D: AsRef, +{ + domain: D, + port: u16, + starttls: bool, +} + +impl ClientBuilder +where + D: AsRef, +{ + /// Make a new `ClientBuilder` using the given domain and port. + pub fn new(domain: D, port: u16) -> Self { + ClientBuilder { + domain, + port, + starttls: false, + } + } + + /// Use [`STARTTLS`](https://tools.ietf.org/html/rfc2595) for this connection. + #[cfg(any(feature = "tls", feature = "rustls-tls"))] + pub fn starttls(&mut self) -> &mut Self { + self.starttls = true; + self + } + + /// Return a new [`Client`] using a `native-tls` transport. + #[cfg(feature = "tls")] + #[cfg_attr(docsrs, doc(cfg(feature = "tls")))] + pub fn native_tls(&mut self) -> Result>> { + self.connect(|domain, tcp| { + let ssl_conn = TlsConnector::builder().build()?; + Ok(TlsConnector::connect(&ssl_conn, domain, tcp)?) + }) + } + + /// Return a new [`Client`] using `rustls` transport. + #[cfg(feature = "rustls-tls")] + #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] + pub fn rustls(&mut self) -> Result>> { + self.connect(|domain, tcp| { + let ssl_conn = RustlsConnector::new_with_native_certs()?; + Ok(ssl_conn.connect(domain, tcp)?) + }) + } + + /// Make a [`Client`] using a custom TLS initialization. This function is intended + /// to be used if your TLS setup requires custom work such as adding private CAs + /// or other specific TLS parameters. + /// + /// The `handshake` argument should accept two parameters: + /// + /// - domain: [`&str`] + /// - tcp: [`TcpStream`] + /// + /// and yield a `Result` where `C` is `Read + Write`. It should only perform + /// TLS initialization over the given `tcp` socket and return the encrypted stream + /// object, such as a [`native_tls::TlsStream`] or a [`rustls_connector::TlsStream`]. + /// + /// If the caller is using `STARTTLS` and previously called [`starttls`](Self::starttls) + /// then the `tcp` socket given to the `handshake` function will be connected and will + /// have initiated the `STARTTLS` handshake. + /// + /// ```no_run + /// # use imap::ClientBuilder; + /// # use rustls_connector::RustlsConnector; + /// # {} #[cfg(feature = "rustls-tls")] + /// # fn main() -> Result<(), imap::Error> { + /// let client = ClientBuilder::new("imap.example.com", 993) + /// .starttls() + /// .connect(|domain, tcp| { + /// let ssl_conn = RustlsConnector::new_with_native_certs()?; + /// Ok(ssl_conn.connect(domain, tcp)?) + /// })?; + /// # Ok(()) + /// # } + /// ``` + pub fn connect(&mut self, handshake: F) -> Result> + where + F: FnOnce(&str, TcpStream) -> Result, + C: Read + Write, + { + let tcp = if self.starttls { + let tcp = TcpStream::connect((self.domain.as_ref(), self.port))?; + let mut client = Client::new(tcp); + client.read_greeting()?; + client.run_command_and_check_ok("STARTTLS")?; + client.into_inner()? + } else { + TcpStream::connect((self.domain.as_ref(), self.port))? + }; + + let tls = handshake(self.domain.as_ref(), tcp)?; + Ok(Client::new(tls)) + } +} diff --git a/src/error.rs b/src/error.rs index 9c16566..4ed0ea1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,6 @@ use std::error::Error as StdError; use std::fmt; use std::io::Error as IoError; -#[cfg(feature = "tls")] use std::net::TcpStream; use std::result; use std::str::Utf8Error; @@ -15,6 +14,8 @@ use imap_proto::{types::ResponseCode, Response}; use native_tls::Error as TlsError; #[cfg(feature = "tls")] use native_tls::HandshakeError as TlsHandshakeError; +#[cfg(feature = "rustls-tls")] +use rustls_connector::HandshakeError as RustlsHandshakeError; /// A convenience wrapper around `Result` for `imap::Error`. pub type Result = result::Result; @@ -57,6 +58,9 @@ impl fmt::Display for No { pub enum Error { /// An `io::Error` that occurred while trying to read or write to a network stream. Io(IoError), + /// An error from the `rustls` library during the TLS handshake. + #[cfg(feature = "rustls-tls")] + RustlsHandshake(RustlsHandshakeError), /// An error from the `native_tls` library during the TLS handshake. #[cfg(feature = "tls")] TlsHandshake(TlsHandshakeError), @@ -100,6 +104,13 @@ impl From> for Error { } } +#[cfg(feature = "rustls-tls")] +impl From> for Error { + fn from(err: RustlsHandshakeError) -> Error { + Error::RustlsHandshake(err) + } +} + #[cfg(feature = "tls")] impl From> for Error { fn from(err: TlsHandshakeError) -> Error { @@ -124,6 +135,8 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { Error::Io(ref e) => fmt::Display::fmt(e, f), + #[cfg(feature = "rustls-tls")] + Error::RustlsHandshake(ref e) => fmt::Display::fmt(e, f), #[cfg(feature = "tls")] Error::Tls(ref e) => fmt::Display::fmt(e, f), #[cfg(feature = "tls")] @@ -144,6 +157,8 @@ impl StdError for Error { fn description(&self) -> &str { match *self { Error::Io(ref e) => e.description(), + #[cfg(feature = "rustls-tls")] + Error::RustlsHandshake(ref e) => e.description(), #[cfg(feature = "tls")] Error::Tls(ref e) => e.description(), #[cfg(feature = "tls")] @@ -161,6 +176,8 @@ impl StdError for Error { fn cause(&self) -> Option<&dyn StdError> { match *self { Error::Io(ref e) => Some(e), + #[cfg(feature = "rustls-tls")] + Error::RustlsHandshake(ref e) => Some(e), #[cfg(feature = "tls")] Error::Tls(ref e) => Some(e), #[cfg(feature = "tls")] diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index fa08142..b55d4f0 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -7,6 +7,8 @@ use crate::parse::parse_idle; use crate::types::UnsolicitedResponse; #[cfg(feature = "tls")] use native_tls::TlsStream; +#[cfg(feature = "rustls-tls")] +use rustls_connector::TlsStream as RustlsStream; use std::io::{self, Read, Write}; use std::net::TcpStream; use std::time::Duration; @@ -25,10 +27,10 @@ use std::time::Duration; /// a convenience callback function [`stop_on_any`] is provided. /// /// ```no_run -/// # use native_tls::TlsConnector; /// use imap::extensions::idle; -/// let ssl_conn = TlsConnector::builder().build().unwrap(); -/// let client = imap::connect(("example.com", 993), "example.com", &ssl_conn) +/// # #[cfg(feature = "tls")] +/// # { +/// let client = imap::ClientBuilder::new("example.com", 993).native_tls() /// .expect("Could not connect to imap server"); /// let mut imap = client.login("user@example.com", "password") /// .expect("Could not authenticate"); @@ -39,6 +41,7 @@ use std::time::Duration; /// /// // Exit on any mailbox change /// let result = idle.wait_keepalive_while(idle::stop_on_any); +/// # } /// ``` /// /// Note that the server MAY consider a client inactive if it has an IDLE command running, and if @@ -284,3 +287,10 @@ impl<'a> SetReadTimeout for TlsStream { self.get_ref().set_read_timeout(timeout).map_err(Error::Io) } } + +#[cfg(feature = "rustls-tls")] +impl<'a> SetReadTimeout for RustlsStream { + fn set_read_timeout(&mut self, timeout: Option) -> Result<()> { + self.get_ref().set_read_timeout(timeout).map_err(Error::Io) + } +} diff --git a/src/lib.rs b/src/lib.rs index 1e04d21..ae249b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ //! //! [@jonhoo]: https://thesquareplanet.com/ //! -//! To connect, use the [`connect`] function. This gives you an unauthenticated [`Client`]. You can +//! To connect, use the [`ClientBuilder`]. This gives you an unauthenticated [`Client`]. You can //! then use [`Client::login`] or [`Client::authenticate`] to perform username/password or //! challenge/response authentication respectively. This in turn gives you an authenticated //! [`Session`], which lets you access the mailboxes at the server. @@ -21,16 +21,10 @@ //! Below is a basic client example. See the `examples/` directory for more. //! //! ```no_run -//! extern crate imap; -//! extern crate native_tls; -//! +//! # #[cfg(feature = "tls")] //! fn fetch_inbox_top() -> imap::error::Result> { -//! let domain = "imap.example.com"; -//! let tls = native_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((domain, 993), domain, &tls).unwrap(); +//! let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?; //! //! // the client we have here is unauthenticated. //! // to do anything useful with the e-mails, we need to log in @@ -77,7 +71,8 @@ //! ``` //! //! Even without `native_tls`, you can still use TLS by leveraging the pure Rust `rustls` -//! crate. See the example/rustls.rs file for a working example. +//! crate, which is enabled with the `rustls-tls` feature. See the example/rustls.rs file +//! for a working example. #![deny(missing_docs)] #![warn(rust_2018_idioms)] @@ -90,6 +85,8 @@ pub use crate::authenticator::Authenticator; mod client; pub use crate::client::*; +mod client_builder; +pub use crate::client_builder::ClientBuilder; pub mod error; pub use error::{Error, Result}; diff --git a/src/types/deleted.rs b/src/types/deleted.rs index 90945c7..d4b91e6 100644 --- a/src/types/deleted.rs +++ b/src/types/deleted.rs @@ -18,9 +18,10 @@ use std::ops::RangeInclusive; /// /// # 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(); +/// # {} #[cfg(feature = "tls")] +/// # fn main() { +/// # let client = imap::ClientBuilder::new("imap.example.com", 993) +/// .native_tls().unwrap(); /// # let mut session = client.login("name", "pw").unwrap(); /// // Iterate over whatever is returned /// if let Ok(deleted) = session.expunge() { @@ -35,6 +36,7 @@ use std::ops::RangeInclusive; /// // Do something with uid /// } /// } +/// # } /// ``` #[derive(Debug, Clone)] pub enum Deleted { diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index a611f2f..bd17a21 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -19,17 +19,15 @@ fn tls() -> native_tls::TlsConnector { } fn session(user: &str) -> imap::Session> { - let mut s = imap::connect( - &format!( - "{}:3993", - std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()) - ), - "imap.example.com", - &tls(), - ) - .unwrap() - .login(user, user) - .unwrap(); + let host = std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()); + let mut s = imap::ClientBuilder::new(&host, 3993) + .connect(|domain, tcp| { + let ssl_conn = tls(); + Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap()) + }) + .unwrap() + .login(user, user) + .unwrap(); s.debug = true; s } @@ -55,25 +53,25 @@ fn smtp(user: &str) -> lettre::SmtpTransport { #[ignore] fn connect_insecure_then_secure() { let host = std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()); - let stream = TcpStream::connect((host.as_ref(), 3143)).unwrap(); - // ignored because of https://github.com/greenmail-mail-test/greenmail/issues/135 - imap::Client::new(stream) - .secure("imap.example.com", &tls()) + imap::ClientBuilder::new(&host, 3143) + .starttls() + .connect(|domain, tcp| { + let ssl_conn = tls(); + Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap()) + }) .unwrap(); } #[test] fn connect_secure() { - imap::connect( - &format!( - "{}:3993", - std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()) - ), - "imap.example.com", - &tls(), - ) - .unwrap(); + let host = std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()); + imap::ClientBuilder::new(&host, 3993) + .connect(|domain, tcp| { + let ssl_conn = tls(); + Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap()) + }) + .unwrap(); } #[test]