Merge pull request #245 from urkle/feat-boxed-stream

add in a client builder that abstracts away connecting to TLS or non-TLS connections and what TLS provider is used.
This commit is contained in:
Jon Gjengset 2023-10-07 16:56:10 +02:00 committed by GitHub
commit 57ea453c52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 607 additions and 120 deletions

View file

@ -26,7 +26,7 @@ jobs:
if: hashFiles('Cargo.lock') == '' if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile run: cargo generate-lockfile
- name: cargo test --locked - name: cargo test --locked
run: caro test --locked --features test-full-imap --all-targets run: cargo test --locked --features test-full-imap --all-targets
services: services:
cyrus_imapd: cyrus_imapd:
image: outoforder/cyrus-imapd-tester:latest image: outoforder/cyrus-imapd-tester:latest

View file

@ -118,8 +118,10 @@ jobs:
- name: cargo generate-lockfile - name: cargo generate-lockfile
if: hashFiles('Cargo.lock') == '' if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile run: cargo generate-lockfile
- name: cargo install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: cargo check - name: cargo check
run: cargo check --locked --all-features --all-targets run: cargo hack --feature-powerset check --locked --all-targets
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: ubuntu / stable / coverage name: ubuntu / stable / coverage

View file

@ -23,7 +23,7 @@ test-full-imap = []
[dependencies] [dependencies]
native-tls = { version = "0.2.2", optional = true } 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" regex = "1.0"
bufstream = "0.1.3" bufstream = "0.1.3"
imap-proto = "0.16.1" imap-proto = "0.16.1"
@ -73,5 +73,9 @@ required-features = ["default"]
name = "imap_integration" name = "imap_integration"
required-features = ["default"] required-features = ["default"]
[[test]]
name = "builder_integration"
required-features = []
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View file

@ -34,7 +34,7 @@ Below is a basic client example. See the `examples/` directory for more.
```rust ```rust
fn fetch_inbox_top() -> imap::error::Result<Option<String>> { fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
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. // the client we have here is unauthenticated.
// to do anything useful with the e-mails, we need to log in // to do anything useful with the e-mails, we need to log in

View file

@ -8,7 +8,7 @@ fn main() {
} }
fn fetch_inbox_top() -> imap::error::Result<Option<String>> { fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
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. // the client we have here is unauthenticated.
// to do anything useful with the e-mails, we need to log in // to do anything useful with the e-mails, we need to log in

View file

@ -24,7 +24,7 @@ fn main() {
}; };
let client = imap::ClientBuilder::new("imap.gmail.com", 993) let client = imap::ClientBuilder::new("imap.gmail.com", 993)
.native_tls() .connect()
.expect("Could not connect to imap.gmail.com"); .expect("Could not connect to imap.gmail.com");
let mut imap_session = match client.authenticate("XOAUTH2", &gmail_auth) { let mut imap_session = match client.authenticate("XOAUTH2", &gmail_auth) {

View file

@ -39,7 +39,7 @@ fn main() {
let opt = Opt::from_args(); let opt = Opt::from_args();
let client = imap::ClientBuilder::new(opt.server.clone(), opt.port) let client = imap::ClientBuilder::new(opt.server.clone(), opt.port)
.native_tls() .connect()
.expect("Could not connect to imap server"); .expect("Could not connect to imap server");
let mut imap = client let mut imap = client

View file

@ -22,7 +22,7 @@ fn fetch_inbox_top(
password: String, password: String,
port: u16, port: u16,
) -> Result<Option<String>, Box<dyn Error>> { ) -> Result<Option<String>, Box<dyn Error>> {
let client = imap::ClientBuilder::new(&host, port).rustls()?; let client = imap::ClientBuilder::new(&host, port).connect()?;
// the client we have here is unauthenticated. // the client we have here is unauthenticated.
// to do anything useful with the e-mails, we need to log in // to do anything useful with the e-mails, we need to log in

View file

@ -2,7 +2,7 @@
* Here's an example showing how to connect to the IMAP server with STARTTLS. * Here's an example showing how to connect to the IMAP server with STARTTLS.
* *
* The only difference is calling `starttls()` on the `ClientBuilder` before * 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. * can connect on port 143 instead of 993.
* *
* The following env vars are expected to be set: * The following env vars are expected to be set:
@ -42,8 +42,7 @@ fn fetch_inbox_top(
port: u16, port: u16,
) -> Result<Option<String>, Box<dyn Error>> { ) -> Result<Option<String>, Box<dyn Error>> {
let client = imap::ClientBuilder::new(&host, port) let client = imap::ClientBuilder::new(&host, port)
.starttls() .connect()
.native_tls()
.expect("Could not connect to server"); .expect("Could not connect to server");
// the client we have here is unauthenticated. // the client we have here is unauthenticated.

View file

@ -340,11 +340,23 @@ impl<T: Read + Write> Client<T> {
/// ///
/// This consumes `self` since the Client is not much use without /// This consumes `self` since the Client is not much use without
/// an underlying transport. /// an underlying transport.
pub(crate) fn into_inner(self) -> Result<T> { pub fn into_inner(self) -> Result<T> {
let res = self.conn.stream.into_inner()?; let res = self.conn.stream.into_inner()?;
Ok(res) 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<Capabilities> {
// 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 /// 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. /// 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 /// This is because `login` takes ownership of `self`, so in order to try again (e.g. after
@ -355,7 +367,7 @@ impl<T: Read + Write> Client<T> {
/// # {} #[cfg(feature = "native-tls")] /// # {} #[cfg(feature = "native-tls")]
/// # fn main() { /// # fn main() {
/// let client = imap::ClientBuilder::new("imap.example.org", 993) /// let client = imap::ClientBuilder::new("imap.example.org", 993)
/// .native_tls().unwrap(); /// .connect().unwrap();
/// ///
/// match client.login("user", "pass") { /// match client.login("user", "pass") {
/// Ok(s) => { /// Ok(s) => {
@ -412,7 +424,7 @@ impl<T: Read + Write> Client<T> {
/// user: String::from("me@example.com"), /// user: String::from("me@example.com"),
/// access_token: String::from("<access_token>"), /// access_token: String::from("<access_token>"),
/// }; /// };
/// 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"); /// .expect("Could not connect to server");
/// ///
/// match client.authenticate("XOAUTH2", &auth) { /// 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] #[test]
fn login() { fn login() {
let response = b"a1 OK Logged in\r\n".to_vec(); let response = b"a1 OK Logged in\r\n".to_vec();

View file

@ -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::io::{Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::{TlsConnector, TlsStream}; use native_tls::TlsConnector as NativeTlsConnector;
use crate::extensions::idle::SetReadTimeout;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls_connector::{RustlsConnector, TlsStream as RustlsStream}; 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<Item = &[u8]>,
_: &[u8],
_: std::time::SystemTime,
) -> std::result::Result<rustls::client::ServerCertVerified, rustls::Error> {
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. /// 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 /// ```no_run
/// # use imap::ClientBuilder; /// # use imap::ClientBuilder;
/// # {} #[cfg(feature = "native-tls")] /// # {} #[cfg(feature = "native-tls")]
/// # fn main() -> Result<(), imap::Error> { /// # fn main() -> Result<(), imap::Error> {
/// let client = ClientBuilder::new("imap.example.com", 993).native_tls()?; /// let client = ClientBuilder::new("imap.example.com", 993).connect()?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
/// ///
/// Similarly, if using the `rustls-tls` feature you can create a [`Client`] using rustls: /// By default it will detect and use `STARTTLS` if available.
/// ```no_run /// ```no_run
/// # use imap::ClientBuilder; /// # use imap::ClientBuilder;
/// # {} #[cfg(feature = "rustls-tls")] /// # {} #[cfg(feature = "native-tls")]
/// # fn main() -> Result<(), imap::Error> { /// # fn main() -> Result<(), imap::Error> {
/// let client = ClientBuilder::new("imap.example.com", 993).rustls()?; /// let client = ClientBuilder::new("imap.example.com", 143).connect()?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
/// ///
/// To use `STARTTLS`, just call `starttls()` before one of the [`Client`]-yielding /// To force a certain implementation you can call tls_kind():
/// functions:
/// ```no_run /// ```no_run
/// # use imap::ClientBuilder; /// # use imap::ClientBuilder;
/// # {} #[cfg(feature = "rustls-tls")] /// # {} #[cfg(feature = "rustls-tls")]
/// # fn main() -> Result<(), imap::Error> { /// # fn main() -> Result<(), imap::Error> {
/// let client = ClientBuilder::new("imap.example.com", 993) /// let client = ClientBuilder::new("imap.example.com", 993)
/// .starttls() /// .tls_kind(imap::TlsKind::Rust).connect()?;
/// .rustls()?; /// # 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(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
/// The returned [`Client`] is unauthenticated; to access session-related methods (through /// The returned [`Client`] is unauthenticated; to access session-related methods (through
/// [`Session`](crate::Session)), use [`Client::login`] or [`Client::authenticate`]. /// [`Session`](crate::Session)), use [`Client::login`] or [`Client::authenticate`].
#[derive(Clone)]
pub struct ClientBuilder<D> pub struct ClientBuilder<D>
where where
D: AsRef<str>, D: AsRef<str>,
{ {
domain: D, domain: D,
port: u16, 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<D> ClientBuilder<D> impl<D> ClientBuilder<D>
@ -61,90 +165,255 @@ where
ClientBuilder { ClientBuilder {
domain, domain,
port, 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. /// Sets the Connection mode to use for this connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] pub fn mode(mut self, mode: ConnectionMode) -> Self {
pub fn starttls(&mut self) -> &mut Self { self.mode = mode;
self.starttls = true;
self self
} }
/// Return a new [`Client`] using a `native-tls` transport. /// Sets the TLS backend to use for this connection.
#[cfg(feature = "native-tls")] #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] pub fn tls_kind(mut self, kind: TlsKind) -> Self {
pub fn native_tls(&mut self) -> Result<Client<TlsStream<TcpStream>>> { self.tls_kind = kind;
self.connect(|domain, tcp| { self
let ssl_conn = TlsConnector::builder().build()?;
Ok(TlsConnector::connect(&ssl_conn, domain, tcp)?)
})
} }
/// Return a new [`Client`] using `rustls` transport. /// Controls the use of certificate validation.
#[cfg(feature = "rustls-tls")] ///
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] /// Defaults to `false`.
pub fn rustls(&mut self) -> Result<Client<RustlsStream<TcpStream>>> { ///
self.connect(|domain, tcp| { /// # Warning
let ssl_conn = RustlsConnector::new_with_native_certs()?; ///
Ok(ssl_conn.connect(domain, tcp)?) /// 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 a custom TLS initialization. This function is intended /// Make a [`Client`] using the configuration.
/// 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<C>` 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 /// ```no_run
/// # use imap::ClientBuilder; /// # use imap::ClientBuilder;
/// # use rustls_connector::RustlsConnector;
/// # {} #[cfg(feature = "rustls-tls")] /// # {} #[cfg(feature = "rustls-tls")]
/// # fn main() -> Result<(), imap::Error> { /// # fn main() -> Result<(), imap::Error> {
/// let client = ClientBuilder::new("imap.example.com", 993) /// let client = ClientBuilder::new("imap.example.com", 143).connect()?;
/// .starttls()
/// .connect(|domain, tcp| {
/// let ssl_conn = RustlsConnector::new_with_native_certs()?;
/// Ok(ssl_conn.connect(domain, tcp)?)
/// })?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn connect<F, C>(&mut self, handshake: F) -> Result<Client<C>> pub fn connect(&self) -> Result<Client<Connection>> {
#[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<Connection> {
return Err(Error::TlsNotConfigured);
});
}
#[allow(unused_variables)]
fn connect_with<F, C>(&self, handshake: F) -> Result<Client<Connection>>
where where
F: FnOnce(&str, TcpStream) -> Result<C>, F: FnOnce(&str, TcpStream) -> Result<C>,
C: Read + Write, C: Read + Write + Send + SetReadTimeout + 'static,
{ {
let tcp = if self.starttls { #[allow(unused_mut)]
let tcp = TcpStream::connect((self.domain.as_ref(), self.port))?; let mut greeting_read = false;
let mut client = Client::new(tcp); let tcp = TcpStream::connect((self.domain.as_ref(), self.port))?;
client.read_greeting()?;
client.run_command_and_check_ok("STARTTLS")?; let stream: Connection = match self.mode {
client.into_inner()? ConnectionMode::AutoTls => {
} else { #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
TcpStream::connect((self.domain.as_ref(), self.port))? 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 tls = handshake(self.domain.as_ref(), tcp)?; let mut client = Client::new(stream);
if !greeting_read {
let mut client = Client::new(tls);
if !self.starttls {
client.read_greeting()?; client.read_greeting()?;
} else {
client.greeting_read = true;
} }
Ok(client) Ok(client)
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
fn upgrade_tls<F, C>(
&self,
mut client: Client<TcpStream>,
handshake: F,
) -> Result<(Connection, bool)>
where
F: FnOnce(&str, TcpStream) -> Result<C>,
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<Connection> {
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<Connection> {
self.build_tls_rustls(tcp)
}
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
fn build_tls_any(&self, tcp: TcpStream) -> Result<Connection> {
self.build_tls_native(tcp)
}
#[cfg(feature = "rustls-tls")]
fn build_tls_rustls(&self, tcp: TcpStream) -> Result<Connection> {
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<Connection> {
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);
}
}
} }

26
src/conn.rs Normal file
View file

@ -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<T> 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<dyn ImapConnection>;
mod private {
use super::{Read, SetReadTimeout, Write};
pub trait Sealed {}
impl<T> Sealed for T where T: Read + Write + SetReadTimeout {}
}

View file

@ -3,6 +3,7 @@
use std::error::Error as StdError; use std::error::Error as StdError;
use std::fmt; use std::fmt;
use std::io::Error as IoError; use std::io::Error as IoError;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use std::net::TcpStream; use std::net::TcpStream;
use std::result; use std::result;
use std::str::Utf8Error; 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 /// In response to a STATUS command, the server sent OK without actually sending any STATUS
/// responses first. /// responses first.
MissingStatusResponse, MissingStatusResponse,
/// StartTls is not available on the server
StartTlsNotAvailable,
/// Returns when Tls is not configured
TlsNotConfigured,
} }
impl From<IoError> for Error { impl From<IoError> for Error {
@ -170,6 +175,10 @@ impl fmt::Display for Error {
Error::Append => f.write_str("Could not append mail to mailbox"), Error::Append => f.write_str("Could not append mail to mailbox"),
Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r), Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r),
Error::MissingStatusResponse => write!(f, "Missing STATUS Response"), 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::Append => "Could not append mail to mailbox",
Error::Unexpected(_) => "Unexpected Response", Error::Unexpected(_) => "Unexpected Response",
Error::MissingStatusResponse => "Missing STATUS 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",
} }
} }

View file

@ -5,12 +5,14 @@ use crate::client::Session;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::parse::parse_idle; use crate::parse::parse_idle;
use crate::types::UnsolicitedResponse; use crate::types::UnsolicitedResponse;
use crate::Connection;
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::TlsStream; use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls_connector::TlsStream as RustlsStream; use rustls_connector::TlsStream as RustlsStream;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
use std::ops::DerefMut;
use std::time::Duration; use std::time::Duration;
/// `Handle` allows a client to block waiting for changes to the remote mailbox. /// `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; /// use imap::extensions::idle;
/// # #[cfg(feature = "native-tls")] /// # #[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"); /// .expect("Could not connect to imap server");
/// let mut imap = client.login("user@example.com", "password") /// let mut imap = client.login("user@example.com", "password")
/// .expect("Could not authenticate"); /// .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<Duration>) -> Result<()> {
self.deref_mut().set_read_timeout(timeout)
}
}
impl<'a> SetReadTimeout for TcpStream { impl<'a> SetReadTimeout for TcpStream {
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> { fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
TcpStream::set_read_timeout(self, timeout).map_err(Error::Io) TcpStream::set_read_timeout(self, timeout).map_err(Error::Io)

View file

@ -24,7 +24,7 @@
//! # #[cfg(feature = "native-tls")] //! # #[cfg(feature = "native-tls")]
//! fn fetch_inbox_top() -> imap::error::Result<Option<String>> { //! fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
//! //!
//! 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. //! // the client we have here is unauthenticated.
//! // to do anything useful with the e-mails, we need to log in //! // to do anything useful with the e-mails, we need to log in
@ -85,10 +85,15 @@ pub mod types;
mod authenticator; mod authenticator;
pub use crate::authenticator::Authenticator; pub use crate::authenticator::Authenticator;
mod conn;
pub use conn::{Connection, ImapConnection};
mod client; mod client;
pub use crate::client::*; pub use crate::client::*;
mod client_builder; 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 mod error;
pub use error::{Error, Result}; pub use error::{Error, Result};

View file

@ -23,7 +23,7 @@ use std::ops::RangeInclusive;
/// # {} #[cfg(feature = "native-tls")] /// # {} #[cfg(feature = "native-tls")]
/// # fn main() { /// # fn main() {
/// # let client = imap::ClientBuilder::new("imap.example.com", 993) /// # let client = imap::ClientBuilder::new("imap.example.com", 993)
/// .native_tls().unwrap(); /// .connect().unwrap();
/// # let mut session = client.login("name", "pw").unwrap(); /// # let mut session = client.login("name", "pw").unwrap();
/// // Iterate over whatever is returned /// // Iterate over whatever is returned
/// if let Ok(deleted) = session.expunge() { /// if let Ok(deleted) = session.expunge() {

View file

@ -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<imap::Connection>) -> 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());
}

View file

@ -9,14 +9,7 @@ use std::net::TcpStream;
use crate::imap::extensions::sort::{SortCharset, SortCriterion}; use crate::imap::extensions::sort::{SortCharset, SortCriterion};
use crate::imap::types::Mailbox; use crate::imap::types::Mailbox;
use crate::imap::{Connection, ConnectionMode};
fn tls() -> native_tls::TlsConnector {
native_tls::TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
.unwrap()
}
fn test_host() -> String { fn test_host() -> String {
std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_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) .unwrap_or(3465)
} }
fn clean_mailbox(session: &mut imap::Session<native_tls::TlsStream<TcpStream>>) { fn clean_mailbox(session: &mut imap::Session<Connection>) {
session.select("INBOX").unwrap(); session.select("INBOX").unwrap();
let inbox = session.search("ALL").unwrap(); let inbox = session.search("ALL").unwrap();
if !inbox.is_empty() { if !inbox.is_empty() {
@ -70,16 +63,12 @@ fn wait_for_delivery() {
std::thread::sleep(std::time::Duration::from_millis(500)); std::thread::sleep(std::time::Duration::from_millis(500));
} }
fn session_with_options( fn session_with_options(user: &str, clean: bool) -> imap::Session<Connection> {
user: &str,
clean: bool,
) -> imap::Session<native_tls::TlsStream<TcpStream>> {
let host = test_host(); let host = test_host();
let mut s = imap::ClientBuilder::new(&host, test_imaps_port()) let mut s = imap::ClientBuilder::new(&host, test_imaps_port())
.connect(|domain, tcp| { .mode(ConnectionMode::Tls)
let ssl_conn = tls(); .danger_skip_tls_verify(true)
Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap()) .connect()
})
.unwrap() .unwrap()
.login(user, user) .login(user, user)
.unwrap(); .unwrap();
@ -98,7 +87,7 @@ fn get_greeting() -> String {
String::from_utf8(greeting).unwrap() String::from_utf8(greeting).unwrap()
} }
fn delete_mailbox(s: &mut imap::Session<native_tls::TlsStream<TcpStream>>, mailbox: &str) { fn delete_mailbox(s: &mut imap::Session<Connection>, mailbox: &str) {
// we are silently eating any error (e.g. mailbox does not exist) // we are silently eating any error (e.g. mailbox does not exist)
s.set_acl( s.set_acl(
mailbox, mailbox,
@ -111,7 +100,7 @@ fn delete_mailbox(s: &mut imap::Session<native_tls::TlsStream<TcpStream>>, mailb
s.delete(mailbox).unwrap_or(()); s.delete(mailbox).unwrap_or(());
} }
fn session(user: &str) -> imap::Session<native_tls::TlsStream<TcpStream>> { fn session(user: &str) -> imap::Session<Connection> {
session_with_options(user, true) session_with_options(user, true)
} }
@ -144,11 +133,9 @@ fn connect_insecure_then_secure() {
let host = test_host(); let host = test_host();
// Not supported on greenmail because of https://github.com/greenmail-mail-test/greenmail/issues/135 // Not supported on greenmail because of https://github.com/greenmail-mail-test/greenmail/issues/135
imap::ClientBuilder::new(&host, test_imap_port()) imap::ClientBuilder::new(&host, test_imap_port())
.starttls() .mode(ConnectionMode::StartTls)
.connect(|domain, tcp| { .danger_skip_tls_verify(true)
let ssl_conn = tls(); .connect()
Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap())
})
.unwrap(); .unwrap();
} }
@ -156,10 +143,9 @@ fn connect_insecure_then_secure() {
fn connect_secure() { fn connect_secure() {
let host = test_host(); let host = test_host();
imap::ClientBuilder::new(&host, test_imaps_port()) imap::ClientBuilder::new(&host, test_imaps_port())
.connect(|domain, tcp| { .mode(ConnectionMode::Tls)
let ssl_conn = tls(); .danger_skip_tls_verify(true)
Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap()) .connect()
})
.unwrap(); .unwrap();
} }
@ -516,9 +502,13 @@ fn append_with_flags_and_date() {
let mbox = "INBOX"; let mbox = "INBOX";
c.select(mbox).unwrap(); c.select(mbox).unwrap();
// append // append
let date = FixedOffset::east(8 * 3600) #[allow(deprecated)]
.ymd(2020, 12, 13) // ymd_opt is deprecated in chrono 0.4.23 and replace with new with_ymd_and_hms
.and_hms(13, 36, 36); let date = FixedOffset::east_opt(8 * 3600)
.unwrap()
.ymd_opt(2020, 12, 13)
.and_hms_opt(13, 36, 36)
.unwrap();
c.append(mbox, &e.formatted()) c.append(mbox, &e.formatted())
.flag(Flag::Seen) .flag(Flag::Seen)
.flag(Flag::Flagged) .flag(Flag::Flagged)