diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d44634a..3531d7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -118,8 +118,10 @@ jobs: - name: cargo generate-lockfile if: hashFiles('Cargo.lock') == '' run: cargo generate-lockfile + - name: cargo install cargo-hack + uses: taiki-e/install-action@cargo-hack - name: cargo check - run: cargo check --locked --all-features --all-targets + run: cargo hack --feature-powerset check --locked --all-targets coverage: runs-on: ubuntu-latest name: ubuntu / stable / coverage diff --git a/Cargo.toml b/Cargo.toml index 73079b7..65a0a72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ test-full-imap = [] [dependencies] native-tls = { version = "0.2.2", optional = true } -rustls-connector = { version = "0.18.0", optional = true } +rustls-connector = { version = "0.18.0", optional = true, features = ["dangerous-configuration"] } regex = "1.0" bufstream = "0.1.3" imap-proto = "0.16.1" @@ -73,5 +73,9 @@ required-features = ["default"] name = "imap_integration" required-features = ["default"] +[[test]] +name = "builder_integration" +required-features = [] + [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md index 17db72c..10145ee 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Below is a basic client example. See the `examples/` directory for more. ```rust fn fetch_inbox_top() -> imap::error::Result> { - let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?; + let client = imap::ClientBuilder::new("imap.example.com", 993).connect()?; // the client we have here is unauthenticated. // to do anything useful with the e-mails, we need to log in diff --git a/examples/basic.rs b/examples/basic.rs index 3c0420a..44bd6ee 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -8,7 +8,7 @@ fn main() { } fn fetch_inbox_top() -> imap::error::Result> { - let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?; + let client = imap::ClientBuilder::new("imap.example.com", 993).connect()?; // 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 62df292..2452bf1 100644 --- a/examples/gmail_oauth2.rs +++ b/examples/gmail_oauth2.rs @@ -24,7 +24,7 @@ fn main() { }; let client = imap::ClientBuilder::new("imap.gmail.com", 993) - .native_tls() + .connect() .expect("Could not connect to imap.gmail.com"); let mut imap_session = match client.authenticate("XOAUTH2", &gmail_auth) { diff --git a/examples/idle.rs b/examples/idle.rs index 2cc9685..dc17083 100644 --- a/examples/idle.rs +++ b/examples/idle.rs @@ -39,7 +39,7 @@ fn main() { let opt = Opt::from_args(); let client = imap::ClientBuilder::new(opt.server.clone(), opt.port) - .native_tls() + .connect() .expect("Could not connect to imap server"); let mut imap = client diff --git a/examples/rustls.rs b/examples/rustls.rs index 5240826..24e68ca 100644 --- a/examples/rustls.rs +++ b/examples/rustls.rs @@ -22,7 +22,7 @@ fn fetch_inbox_top( password: String, port: u16, ) -> Result, Box> { - let client = imap::ClientBuilder::new(&host, port).rustls()?; + let client = imap::ClientBuilder::new(&host, port).connect()?; // 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 8b7e712..122ba07 100644 --- a/examples/starttls.rs +++ b/examples/starttls.rs @@ -2,7 +2,7 @@ * Here's an example showing how to connect to the IMAP server with STARTTLS. * * The only difference is calling `starttls()` on the `ClientBuilder` before - * initiating the secure connection with `native_tls()` or `rustls()`, so you + * initiating the secure connection with `connect()`, so you * can connect on port 143 instead of 993. * * The following env vars are expected to be set: @@ -42,8 +42,7 @@ fn fetch_inbox_top( port: u16, ) -> Result, Box> { let client = imap::ClientBuilder::new(&host, port) - .starttls() - .native_tls() + .connect() .expect("Could not connect to server"); // the client we have here is unauthenticated. diff --git a/src/client.rs b/src/client.rs index 7969497..39d2b34 100644 --- a/src/client.rs +++ b/src/client.rs @@ -340,11 +340,23 @@ impl Client { /// /// This consumes `self` since the Client is not much use without /// an underlying transport. - pub(crate) fn into_inner(self) -> Result { + pub fn into_inner(self) -> Result { let res = self.conn.stream.into_inner()?; Ok(res) } + /// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a + /// listing of capabilities that the server supports. The server will include "IMAP4rev1" as + /// one of the listed capabilities. See [`Capabilities`] for further details. + /// + /// This allows reading capabilities before authentication. + pub fn capabilities(&mut self) -> Result { + // Create a temporary channel as we do not care about out of band responses before login + let (mut tx, _rx) = mpsc::channel(); + self.run_command_and_read_response("CAPABILITY") + .and_then(|lines| Capabilities::parse(lines, &mut tx)) + } + /// 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 @@ -355,7 +367,7 @@ impl Client { /// # {} #[cfg(feature = "native-tls")] /// # fn main() { /// let client = imap::ClientBuilder::new("imap.example.org", 993) - /// .native_tls().unwrap(); + /// .connect().unwrap(); /// /// match client.login("user", "pass") { /// Ok(s) => { @@ -412,7 +424,7 @@ impl Client { /// user: String::from("me@example.com"), /// access_token: String::from(""), /// }; - /// let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls() + /// let client = imap::ClientBuilder::new("imap.example.com", 993).connect() /// .expect("Could not connect to server"); /// /// match client.authenticate("XOAUTH2", &auth) { @@ -1821,6 +1833,31 @@ mod tests { ); } + #[test] + fn pre_login_capability() { + let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\ + a1 OK CAPABILITY completed\r\n" + .to_vec(); + let expected_capabilities = vec![ + Capability::Imap4rev1, + Capability::Atom(Cow::Borrowed("STARTTLS")), + Capability::Auth(Cow::Borrowed("GSSAPI")), + Capability::Atom(Cow::Borrowed("LOGINDISABLED")), + ]; + let mock_stream = MockStream::new(response); + let mut client = Client::new(mock_stream); + let capabilities = client.capabilities().unwrap(); + assert_eq!( + client.stream.get_ref().written_buf, + b"a1 CAPABILITY\r\n".to_vec(), + "Invalid capability command" + ); + assert_eq!(capabilities.len(), 4); + for e in expected_capabilities { + assert!(capabilities.has(&e)); + } + } + #[test] fn login() { let response = b"a1 OK Logged in\r\n".to_vec(); diff --git a/src/client_builder.rs b/src/client_builder.rs index 0e8e3ce..080d0c8 100644 --- a/src/client_builder.rs +++ b/src/client_builder.rs @@ -1,55 +1,159 @@ -use crate::{Client, Result}; +use crate::{Client, Connection, Error, Result}; + +use lazy_static::lazy_static; use std::io::{Read, Write}; use std::net::TcpStream; #[cfg(feature = "native-tls")] -use native_tls::{TlsConnector, TlsStream}; +use native_tls::TlsConnector as NativeTlsConnector; + +use crate::extensions::idle::SetReadTimeout; +#[cfg(feature = "rustls-tls")] +use rustls_connector::{ + rustls, + rustls::{Certificate, ClientConfig, RootCertStore, ServerName}, + rustls_native_certs::load_native_certs, + RustlsConnector, +}; +#[cfg(feature = "rustls-tls")] +use std::sync::Arc; + +#[cfg(feature = "rustls-tls")] +struct NoCertVerification; + #[cfg(feature = "rustls-tls")] -use rustls_connector::{RustlsConnector, TlsStream as RustlsStream}; +impl rustls::client::ServerCertVerifier for NoCertVerification { + fn verify_server_cert( + &self, + _: &Certificate, + _: &[Certificate], + _: &ServerName, + _: &mut dyn Iterator, + _: &[u8], + _: std::time::SystemTime, + ) -> std::result::Result { + Ok(rustls::client::ServerCertVerified::assertion()) + } +} + +#[cfg(feature = "rustls-tls")] +lazy_static! { + static ref CACERTS: RootCertStore = { + let mut store = RootCertStore::empty(); + for cert in load_native_certs().unwrap_or_else(|_| vec![]) { + if let Ok(_) = store.add(&Certificate(cert.0)) {} + } + store + }; +} + +lazy_static! { + static ref STARTLS_CHECK_REGEX: regex::bytes::Regex = + regex::bytes::Regex::new(r"\bSTARTTLS\b").unwrap(); +} + +/// The connection mode we are going to use +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConnectionMode { + /// Automatically detect what connection mode should be used. + /// + /// This will use TLS if the port is 993, and otherwise STARTTLS if available. + /// If no TLS communication mechanism is available, the connection will fail. + AutoTls, + /// Automatically detect what connection mode should be used. + /// + /// This will use TLS if the port is 993, and otherwise STARTTLS if available. + /// It will fallback to a plaintext connection if no TLS option can be used. + Auto, + /// A plain unencrypted TCP connection + Plaintext, + /// An encrypted TLS connection + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + Tls, + /// An eventually-encrypted (i.e., STARTTLS) connection + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + StartTls, +} + +/// A selection for TLS implementation +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum TlsKind { + /// Use the NativeTLS backend + #[cfg(feature = "native-tls")] + Native, + /// Use the Rustls backend + #[cfg(feature = "rustls-tls")] + Rust, + /// Use whatever backend is available (uses rustls if both are available) + Any, +} /// A convenience builder for [`Client`] structs over various encrypted transports. /// -/// Creating a [`Client`] using `native-tls` transport is straightforward: +/// Creating a [`Client`] using TLS is straightforward. +/// +/// This will make a TLS connection directly since the port is 993. +/// ```no_run +/// # use imap::ClientBuilder; +/// # {} #[cfg(feature = "native-tls")] +/// # fn main() -> Result<(), imap::Error> { +/// let client = ClientBuilder::new("imap.example.com", 993).connect()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// By default it will detect and use `STARTTLS` if available. /// ```no_run /// # use imap::ClientBuilder; /// # {} #[cfg(feature = "native-tls")] /// # fn main() -> Result<(), imap::Error> { -/// let client = ClientBuilder::new("imap.example.com", 993).native_tls()?; +/// let client = ClientBuilder::new("imap.example.com", 143).connect()?; /// # Ok(()) /// # } /// ``` /// -/// Similarly, if using the `rustls-tls` feature you can create a [`Client`] using rustls: +/// To force a certain implementation you can call tls_kind(): /// ```no_run /// # use imap::ClientBuilder; /// # {} #[cfg(feature = "rustls-tls")] /// # fn main() -> Result<(), imap::Error> { -/// let client = ClientBuilder::new("imap.example.com", 993).rustls()?; +/// let client = ClientBuilder::new("imap.example.com", 993) +/// .tls_kind(imap::TlsKind::Rust).connect()?; /// # Ok(()) /// # } /// ``` /// -/// To use `STARTTLS`, just call `starttls()` before one of the [`Client`]-yielding -/// functions: +/// To force the use `STARTTLS`, just call `mode()` before connect(): +/// +/// If the server does not provide STARTTLS this will error out. /// ```no_run /// # use imap::ClientBuilder; /// # {} #[cfg(feature = "rustls-tls")] /// # fn main() -> Result<(), imap::Error> { +/// use imap::ConnectionMode; /// let client = ClientBuilder::new("imap.example.com", 993) -/// .starttls() -/// .rustls()?; +/// .mode(ConnectionMode::StartTls) +/// .connect()?; /// # Ok(()) /// # } /// ``` /// The returned [`Client`] is unauthenticated; to access session-related methods (through /// [`Session`](crate::Session)), use [`Client::login`] or [`Client::authenticate`]. +#[derive(Clone)] pub struct ClientBuilder where D: AsRef, { domain: D, port: u16, - starttls: bool, + mode: ConnectionMode, + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + tls_kind: TlsKind, + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + skip_tls_verify: bool, } impl ClientBuilder @@ -61,90 +165,255 @@ where ClientBuilder { domain, port, - starttls: false, + mode: ConnectionMode::AutoTls, + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + tls_kind: TlsKind::Any, + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + skip_tls_verify: false, } } - /// Use [`STARTTLS`](https://tools.ietf.org/html/rfc2595) for this connection. - #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] - pub fn starttls(&mut self) -> &mut Self { - self.starttls = true; + /// Sets the Connection mode to use for this connection + pub fn mode(mut self, mode: ConnectionMode) -> Self { + self.mode = mode; self } - /// Return a new [`Client`] using a `native-tls` transport. - #[cfg(feature = "native-tls")] - #[cfg_attr(docsrs, doc(cfg(feature = "native-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)?) - }) + /// Sets the TLS backend to use for this connection. + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + pub fn tls_kind(mut self, kind: TlsKind) -> Self { + self.tls_kind = kind; + self } - /// 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. + /// Controls the use of certificate validation. /// - /// The `handshake` argument should accept two parameters: + /// Defaults to `false`. /// - /// - domain: [`&str`] - /// - tcp: [`TcpStream`] + /// # Warning /// - /// 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`]. + /// You should only use this as a last resort as it allows another server to impersonate the + /// server you think you're talking to, which would include being able to receive your + /// credentials. /// - /// 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. + /// See [`native_tls::TlsConnectorBuilder::danger_accept_invalid_certs`], + /// [`native_tls::TlsConnectorBuilder::danger_accept_invalid_hostnames`], + /// [`rustls::ClientConfig::dangerous`] + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + pub fn danger_skip_tls_verify(mut self, skip_tls_verify: bool) -> Self { + self.skip_tls_verify = skip_tls_verify; + self + } + + /// Make a [`Client`] using the configuration. /// /// ```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)?) - /// })?; + /// let client = ClientBuilder::new("imap.example.com", 143).connect()?; /// # Ok(()) /// # } /// ``` - pub fn connect(&mut self, handshake: F) -> Result> + pub fn connect(&self) -> Result> { + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + return self.connect_with(|_domain, tcp| self.build_tls_connection(tcp)); + #[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))] + return self.connect_with(|_domain, _tcp| -> Result { + return Err(Error::TlsNotConfigured); + }); + } + + #[allow(unused_variables)] + fn connect_with(&self, handshake: F) -> Result> where F: FnOnce(&str, TcpStream) -> Result, - C: Read + Write, + C: Read + Write + Send + SetReadTimeout + 'static, { - let tcp = if self.starttls { - let tcp = TcpStream::connect((self.domain.as_ref(), self.port))?; - let mut client = Client::new(tcp); + #[allow(unused_mut)] + let mut greeting_read = false; + let tcp = TcpStream::connect((self.domain.as_ref(), self.port))?; + + let stream: Connection = match self.mode { + ConnectionMode::AutoTls => { + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + if self.port == 993 { + Box::new(handshake(self.domain.as_ref(), tcp)?) + } else { + let (stream, upgraded) = self.upgrade_tls(Client::new(tcp), handshake)?; + greeting_read = true; + + if !upgraded { + Err(Error::StartTlsNotAvailable)? + } + stream + } + #[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))] + Err(Error::TlsNotConfigured)? + } + ConnectionMode::Auto => { + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + if self.port == 993 { + Box::new(handshake(self.domain.as_ref(), tcp)?) + } else { + let (stream, _upgraded) = self.upgrade_tls(Client::new(tcp), handshake)?; + greeting_read = true; + + stream + } + #[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))] + Box::new(tcp) + } + ConnectionMode::Plaintext => Box::new(tcp), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + ConnectionMode::StartTls => { + let (stream, upgraded) = self.upgrade_tls(Client::new(tcp), handshake)?; + greeting_read = true; + + if !upgraded { + Err(Error::StartTlsNotAvailable)? + } + stream + } + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + ConnectionMode::Tls => Box::new(handshake(self.domain.as_ref(), tcp)?), + }; + + let mut client = Client::new(stream); + if !greeting_read { client.read_greeting()?; + } else { + client.greeting_read = true; + } + + Ok(client) + } + + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + fn upgrade_tls( + &self, + mut client: Client, + handshake: F, + ) -> Result<(Connection, bool)> + where + F: FnOnce(&str, TcpStream) -> Result, + C: Read + Write + Send + SetReadTimeout + 'static, + { + client.read_greeting()?; + + let capabilities = client.capabilities()?; + if capabilities.has(&imap_proto::Capability::Atom("STARTTLS".into())) { client.run_command_and_check_ok("STARTTLS")?; - client.into_inner()? + let tcp = client.into_inner()?; + Ok((Box::new(handshake(self.domain.as_ref(), tcp)?), true)) } else { - TcpStream::connect((self.domain.as_ref(), self.port))? - }; + Ok((Box::new(client.into_inner()?), false)) + } + } - let tls = handshake(self.domain.as_ref(), tcp)?; + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + fn build_tls_connection(&self, tcp: TcpStream) -> Result { + match self.tls_kind { + #[cfg(feature = "native-tls")] + TlsKind::Native => self.build_tls_native(tcp), + #[cfg(feature = "rustls-tls")] + TlsKind::Rust => self.build_tls_rustls(tcp), + TlsKind::Any => self.build_tls_any(tcp), + } + } - let mut client = Client::new(tls); - if !self.starttls { - client.read_greeting()?; + #[cfg(feature = "rustls-tls")] + fn build_tls_any(&self, tcp: TcpStream) -> Result { + self.build_tls_rustls(tcp) + } + + #[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))] + fn build_tls_any(&self, tcp: TcpStream) -> Result { + self.build_tls_native(tcp) + } + + #[cfg(feature = "rustls-tls")] + fn build_tls_rustls(&self, tcp: TcpStream) -> Result { + let mut config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(CACERTS.clone()) + .with_no_client_auth(); + if self.skip_tls_verify { + let no_cert_verifier = NoCertVerification; + config + .dangerous() + .set_certificate_verifier(Arc::new(no_cert_verifier)); } + let ssl_conn: RustlsConnector = config.into(); + Ok(Box::new(ssl_conn.connect(self.domain.as_ref(), tcp)?)) + } - Ok(client) + #[cfg(feature = "native-tls")] + fn build_tls_native(&self, tcp: TcpStream) -> Result { + let mut builder = NativeTlsConnector::builder(); + if self.skip_tls_verify { + builder.danger_accept_invalid_certs(true); + builder.danger_accept_invalid_hostnames(true); + } + let ssl_conn = builder.build()?; + Ok(Box::new(NativeTlsConnector::connect( + &ssl_conn, + self.domain.as_ref(), + tcp, + )?)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod connection_mode { + use super::*; + + #[test] + fn connection_mode_eq() { + assert_eq!(ConnectionMode::Auto, ConnectionMode::Auto); + } + + #[test] + fn connection_mode_ne() { + assert_ne!(ConnectionMode::Auto, ConnectionMode::AutoTls); + } + } + + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + mod tls_kind { + use super::*; + + #[test] + fn connection_mode_eq() { + assert_eq!(TlsKind::Any, TlsKind::Any); + } + + #[cfg(feature = "native-tls")] + #[test] + fn connection_mode_ne() { + assert_ne!(TlsKind::Any, TlsKind::Native); + } + + #[cfg(feature = "rustls-tls")] + #[test] + fn connection_mode_ne() { + assert_ne!(TlsKind::Any, TlsKind::Rust); + } + } + + mod client_builder { + use super::*; + + #[test] + fn can_clone() { + let builder = ClientBuilder::new("imap.example.com", 143); + + let clone = builder.clone(); + assert_eq!(clone.domain, builder.domain); + assert_eq!(clone.port, builder.port); + } } } diff --git a/src/conn.rs b/src/conn.rs new file mode 100644 index 0000000..9ee02ca --- /dev/null +++ b/src/conn.rs @@ -0,0 +1,26 @@ +use crate::extensions::idle::SetReadTimeout; + +use std::fmt::{Debug, Formatter}; +use std::io::{Read, Write}; + +/// Imap connection trait of a read/write stream +pub trait ImapConnection: Read + Write + Send + SetReadTimeout + private::Sealed {} + +impl ImapConnection for T where T: Read + Write + Send + SetReadTimeout {} + +impl Debug for dyn ImapConnection { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Imap connection") + } +} + +/// A boxed connection type +pub type Connection = Box; + +mod private { + use super::{Read, SetReadTimeout, Write}; + + pub trait Sealed {} + + impl Sealed for T where T: Read + Write + SetReadTimeout {} +} diff --git a/src/error.rs b/src/error.rs index 477b926..d11601a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,7 @@ use std::error::Error as StdError; use std::fmt; use std::io::Error as IoError; +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use std::net::TcpStream; use std::result; use std::str::Utf8Error; @@ -104,6 +105,10 @@ pub enum Error { /// In response to a STATUS command, the server sent OK without actually sending any STATUS /// responses first. MissingStatusResponse, + /// StartTls is not available on the server + StartTlsNotAvailable, + /// Returns when Tls is not configured + TlsNotConfigured, } impl From for Error { @@ -170,6 +175,10 @@ impl fmt::Display for Error { Error::Append => f.write_str("Could not append mail to mailbox"), Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r), Error::MissingStatusResponse => write!(f, "Missing STATUS Response"), + Error::StartTlsNotAvailable => write!(f, "StartTls is not available on the server"), + Error::TlsNotConfigured => { + write!(f, "TLS was requested, but no TLS features are enabled") + } } } } @@ -194,6 +203,8 @@ impl StdError for Error { Error::Append => "Could not append mail to mailbox", Error::Unexpected(_) => "Unexpected Response", Error::MissingStatusResponse => "Missing STATUS Response", + Error::StartTlsNotAvailable => "StartTls is not available on the server", + Error::TlsNotConfigured => "TLS was requested, but no TLS features are enabled", } } diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 559fc77..4068fa7 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -5,12 +5,14 @@ use crate::client::Session; use crate::error::{Error, Result}; use crate::parse::parse_idle; use crate::types::UnsolicitedResponse; +use crate::Connection; #[cfg(feature = "native-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::ops::DerefMut; use std::time::Duration; /// `Handle` allows a client to block waiting for changes to the remote mailbox. @@ -31,7 +33,7 @@ use std::time::Duration; /// use imap::extensions::idle; /// # #[cfg(feature = "native-tls")] /// # { -/// let client = imap::ClientBuilder::new("example.com", 993).native_tls() +/// let client = imap::ClientBuilder::new("example.com", 993).connect() /// .expect("Could not connect to imap server"); /// let mut imap = client.login("user@example.com", "password") /// .expect("Could not authenticate"); @@ -245,6 +247,12 @@ impl<'a, T: Read + Write + 'a> Drop for Handle<'a, T> { } } +impl<'a> SetReadTimeout for Connection { + fn set_read_timeout(&mut self, timeout: Option) -> Result<()> { + self.deref_mut().set_read_timeout(timeout) + } +} + impl<'a> SetReadTimeout for TcpStream { fn set_read_timeout(&mut self, timeout: Option) -> Result<()> { TcpStream::set_read_timeout(self, timeout).map_err(Error::Io) diff --git a/src/lib.rs b/src/lib.rs index d80862f..9131ad7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ //! # #[cfg(feature = "native-tls")] //! fn fetch_inbox_top() -> imap::error::Result> { //! -//! let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?; +//! let client = imap::ClientBuilder::new("imap.example.com", 993).connect()?; //! //! // the client we have here is unauthenticated. //! // to do anything useful with the e-mails, we need to log in @@ -85,10 +85,15 @@ pub mod types; mod authenticator; pub use crate::authenticator::Authenticator; +mod conn; +pub use conn::{Connection, ImapConnection}; + mod client; pub use crate::client::*; mod client_builder; -pub use crate::client_builder::ClientBuilder; +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] +pub use crate::client_builder::TlsKind; +pub use crate::client_builder::{ClientBuilder, ConnectionMode}; pub mod error; pub use error::{Error, Result}; diff --git a/src/types/deleted.rs b/src/types/deleted.rs index 8e08be0..cfc7aea 100644 --- a/src/types/deleted.rs +++ b/src/types/deleted.rs @@ -23,7 +23,7 @@ use std::ops::RangeInclusive; /// # {} #[cfg(feature = "native-tls")] /// # fn main() { /// # let client = imap::ClientBuilder::new("imap.example.com", 993) -/// .native_tls().unwrap(); +/// .connect().unwrap(); /// # let mut session = client.login("name", "pw").unwrap(); /// // Iterate over whatever is returned /// if let Ok(deleted) = session.expunge() { diff --git a/tests/builder_integration.rs b/tests/builder_integration.rs new file mode 100644 index 0000000..62eb523 --- /dev/null +++ b/tests/builder_integration.rs @@ -0,0 +1,136 @@ +extern crate imap; + +use imap::ConnectionMode; + +fn test_host() -> String { + std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()) +} + +fn test_imap_port() -> u16 { + std::env::var("TEST_IMAP_PORT") + .unwrap_or("3143".to_string()) + .parse() + .unwrap_or(3143) +} + +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] +fn test_imaps_port() -> u16 { + std::env::var("TEST_IMAPS_PORT") + .unwrap_or("3993".to_string()) + .parse() + .unwrap_or(3993) +} + +fn list_mailbox(session: &mut imap::Session) -> Result<(), imap::Error> { + session.select("INBOX")?; + session.search("ALL")?; + Ok(()) +} + +#[cfg(all( + any(feature = "native-tls", feature = "rustls-tls"), + feature = "test-full-imap" +))] +#[test] +fn starttls_force() { + let user = "starttls@localhost"; + let host = test_host(); + let c = imap::ClientBuilder::new(&host, test_imap_port()) + .danger_skip_tls_verify(true) + .mode(ConnectionMode::StartTls) + .connect() + .unwrap(); + let mut s = c.login(user, user).unwrap(); + s.debug = true; + assert!(list_mailbox(&mut s).is_ok()); +} + +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] +#[test] +fn tls_force() { + let user = "tls@localhost"; + let host = test_host(); + let c = imap::ClientBuilder::new(&host, test_imaps_port()) + .danger_skip_tls_verify(true) + .mode(ConnectionMode::Tls) + .connect() + .unwrap(); + let mut s = c.login(user, user).unwrap(); + s.debug = true; + assert!(list_mailbox(&mut s).is_ok()); +} + +#[cfg(feature = "rustls-tls")] +#[test] +fn tls_force_rustls() { + let user = "tls@localhost"; + let host = test_host(); + let c = imap::ClientBuilder::new(&host, test_imaps_port()) + .danger_skip_tls_verify(true) + .tls_kind(imap::TlsKind::Rust) + .mode(ConnectionMode::Tls) + .connect() + .unwrap(); + let mut s = c.login(user, user).unwrap(); + s.debug = true; + assert!(list_mailbox(&mut s).is_ok()); +} + +#[cfg(feature = "native-tls")] +#[test] +fn tls_force_native() { + let user = "tls@localhost"; + let host = test_host(); + let c = imap::ClientBuilder::new(&host, test_imaps_port()) + .danger_skip_tls_verify(true) + .tls_kind(imap::TlsKind::Native) + .mode(ConnectionMode::Tls) + .connect() + .unwrap(); + let mut s = c.login(user, user).unwrap(); + s.debug = true; + assert!(list_mailbox(&mut s).is_ok()); +} + +#[test] +#[cfg(all( + feature = "test-full-imap", + any(feature = "native-tls", feature = "rustls-tls") +))] +fn auto_tls() { + let user = "auto@localhost"; + let host = test_host(); + let builder = imap::ClientBuilder::new(&host, test_imap_port()).danger_skip_tls_verify(true); + + let c = builder.connect().unwrap(); + let mut s = c.login(user, user).unwrap(); + s.debug = true; + assert!(list_mailbox(&mut s).is_ok()); +} + +#[test] +fn auto() { + let user = "auto@localhost"; + let host = test_host(); + let builder = imap::ClientBuilder::new(&host, test_imap_port()).mode(ConnectionMode::Auto); + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + let builder = builder.danger_skip_tls_verify(true); + + let c = builder.connect().unwrap(); + let mut s = c.login(user, user).unwrap(); + s.debug = true; + assert!(list_mailbox(&mut s).is_ok()); +} + +#[test] +fn raw_force() { + let user = "raw@localhost"; + let host = test_host(); + let c = imap::ClientBuilder::new(&host, test_imap_port()) + .mode(ConnectionMode::Plaintext) + .connect() + .unwrap(); + let mut s = c.login(user, user).unwrap(); + s.debug = true; + assert!(list_mailbox(&mut s).is_ok()); +} diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index 0983fbe..713e6c8 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -9,14 +9,7 @@ use std::net::TcpStream; use crate::imap::extensions::sort::{SortCharset, SortCriterion}; use crate::imap::types::Mailbox; - -fn tls() -> native_tls::TlsConnector { - native_tls::TlsConnector::builder() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - .build() - .unwrap() -} +use crate::imap::{Connection, ConnectionMode}; fn test_host() -> String { std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()) @@ -48,7 +41,7 @@ fn test_smtps_port() -> u16 { .unwrap_or(3465) } -fn clean_mailbox(session: &mut imap::Session>) { +fn clean_mailbox(session: &mut imap::Session) { session.select("INBOX").unwrap(); let inbox = session.search("ALL").unwrap(); if !inbox.is_empty() { @@ -70,16 +63,12 @@ fn wait_for_delivery() { std::thread::sleep(std::time::Duration::from_millis(500)); } -fn session_with_options( - user: &str, - clean: bool, -) -> imap::Session> { +fn session_with_options(user: &str, clean: bool) -> imap::Session { let host = test_host(); let mut s = imap::ClientBuilder::new(&host, test_imaps_port()) - .connect(|domain, tcp| { - let ssl_conn = tls(); - Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap()) - }) + .mode(ConnectionMode::Tls) + .danger_skip_tls_verify(true) + .connect() .unwrap() .login(user, user) .unwrap(); @@ -98,7 +87,7 @@ fn get_greeting() -> String { String::from_utf8(greeting).unwrap() } -fn delete_mailbox(s: &mut imap::Session>, mailbox: &str) { +fn delete_mailbox(s: &mut imap::Session, mailbox: &str) { // we are silently eating any error (e.g. mailbox does not exist) s.set_acl( mailbox, @@ -111,7 +100,7 @@ fn delete_mailbox(s: &mut imap::Session>, mailb s.delete(mailbox).unwrap_or(()); } -fn session(user: &str) -> imap::Session> { +fn session(user: &str) -> imap::Session { session_with_options(user, true) } @@ -144,11 +133,9 @@ fn connect_insecure_then_secure() { let host = test_host(); // Not supported on greenmail because of https://github.com/greenmail-mail-test/greenmail/issues/135 imap::ClientBuilder::new(&host, test_imap_port()) - .starttls() - .connect(|domain, tcp| { - let ssl_conn = tls(); - Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap()) - }) + .mode(ConnectionMode::StartTls) + .danger_skip_tls_verify(true) + .connect() .unwrap(); } @@ -156,10 +143,9 @@ fn connect_insecure_then_secure() { fn connect_secure() { let host = test_host(); imap::ClientBuilder::new(&host, test_imaps_port()) - .connect(|domain, tcp| { - let ssl_conn = tls(); - Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap()) - }) + .mode(ConnectionMode::Tls) + .danger_skip_tls_verify(true) + .connect() .unwrap(); }