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 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")] 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 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", 143).connect()?; /// # Ok(()) /// # } /// ``` /// /// 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) /// .tls_kind(imap::TlsKind::Rust).connect()?; /// # Ok(()) /// # } /// ``` /// /// 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) /// .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, 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 where D: AsRef, { /// Make a new `ClientBuilder` using the given domain and port. pub fn new(domain: D, port: u16) -> Self { ClientBuilder { domain, port, 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, } } /// Sets the Connection mode to use for this connection pub fn mode(mut self, mode: ConnectionMode) -> Self { self.mode = mode; self } /// 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 } /// Controls the use of certificate validation. /// /// Defaults to `false`. /// /// # Warning /// /// 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. /// /// 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; /// # {} #[cfg(feature = "rustls-tls")] /// # fn main() -> Result<(), imap::Error> { /// let client = ClientBuilder::new("imap.example.com", 143).connect()?; /// # Ok(()) /// # } /// ``` 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 + Send + SetReadTimeout + 'static, { #[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")?; let tcp = client.into_inner()?; Ok((Box::new(handshake(self.domain.as_ref(), tcp)?), true)) } else { Ok((Box::new(client.into_inner()?), false)) } } #[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), } } #[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)?)) } #[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_native() { assert_ne!(TlsKind::Any, TlsKind::Native); } #[cfg(feature = "rustls-tls")] #[test] fn connection_mode_ne_rust() { 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); } } }