Change the client builder so that it abstracts away connecting to TLS or non-TLS connections and what TLS provider is used.
- this allows a more transparent and versatile usage of the library as one can simply compile it as-is and then use the builder to configure where we connect and how we connect without having to be concerned about what type is used for the imap::Client / imap::Session
This commit is contained in:
parent
2be8210f3c
commit
bb39460491
17 changed files with 599 additions and 116 deletions
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ Below is a basic client example. See the `examples/` directory for more.
|
|||
```rust
|
||||
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.
|
||||
// to do anything useful with the e-mails, we need to log in
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ fn main() {
|
|||
}
|
||||
|
||||
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.
|
||||
// to do anything useful with the e-mails, we need to log in
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ fn fetch_inbox_top(
|
|||
password: String,
|
||||
port: u16,
|
||||
) -> 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.
|
||||
// to do anything useful with the e-mails, we need to log in
|
||||
|
|
|
|||
|
|
@ -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<Option<String>, Box<dyn Error>> {
|
||||
let client = imap::ClientBuilder::new(&host, port)
|
||||
.starttls()
|
||||
.native_tls()
|
||||
.connect()
|
||||
.expect("Could not connect to server");
|
||||
|
||||
// the client we have here is unauthenticated.
|
||||
|
|
|
|||
|
|
@ -340,11 +340,23 @@ impl<T: Read + Write> Client<T> {
|
|||
///
|
||||
/// This consumes `self` since the Client is not much use without
|
||||
/// 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()?;
|
||||
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
|
||||
/// 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<T: Read + Write> Client<T> {
|
|||
/// # {} #[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<T: Read + Write> Client<T> {
|
|||
/// user: String::from("me@example.com"),
|
||||
/// 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");
|
||||
///
|
||||
/// 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();
|
||||
|
|
|
|||
|
|
@ -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::{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.
|
||||
///
|
||||
/// 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).native_tls()?;
|
||||
/// let client = ClientBuilder::new("imap.example.com", 993).connect()?;
|
||||
/// # 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
|
||||
/// # use imap::ClientBuilder;
|
||||
/// # {} #[cfg(feature = "rustls-tls")]
|
||||
/// # {} #[cfg(feature = "native-tls")]
|
||||
/// # fn main() -> Result<(), imap::Error> {
|
||||
/// let client = ClientBuilder::new("imap.example.com", 993).rustls()?;
|
||||
/// let client = ClientBuilder::new("imap.example.com", 143).connect()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// To use `STARTTLS`, just call `starttls()` before one of the [`Client`]-yielding
|
||||
/// functions:
|
||||
/// 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)
|
||||
/// .starttls()
|
||||
/// .rustls()?;
|
||||
/// .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<D>
|
||||
where
|
||||
D: AsRef<str>,
|
||||
{
|
||||
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<D> ClientBuilder<D>
|
||||
|
|
@ -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<Client<TlsStream<TcpStream>>> {
|
||||
self.connect(|domain, tcp| {
|
||||
let ssl_conn = TlsConnector::builder().build()?;
|
||||
Ok(TlsConnector::connect(&ssl_conn, 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
|
||||
}
|
||||
|
||||
/// 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<Client<RustlsStream<TcpStream>>> {
|
||||
self.connect(|domain, tcp| {
|
||||
let ssl_conn = RustlsConnector::new_with_native_certs()?;
|
||||
Ok(ssl_conn.connect(domain, tcp)?)
|
||||
})
|
||||
/// 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 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<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.
|
||||
/// 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<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
|
||||
F: FnOnce(&str, TcpStream) -> Result<C>,
|
||||
C: Read + Write,
|
||||
C: Read + Write + Send + SetReadTimeout + 'static,
|
||||
{
|
||||
let tcp = if self.starttls {
|
||||
#[allow(unused_mut)]
|
||||
let mut greeting_read = false;
|
||||
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()?
|
||||
|
||||
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 {
|
||||
TcpStream::connect((self.domain.as_ref(), self.port))?
|
||||
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(tls);
|
||||
if !self.starttls {
|
||||
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<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
26
src/conn.rs
Normal 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 {}
|
||||
}
|
||||
11
src/error.rs
11
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<IoError> 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",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Duration>) -> Result<()> {
|
||||
self.deref_mut().set_read_timeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SetReadTimeout for TcpStream {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
|
||||
TcpStream::set_read_timeout(self, timeout).map_err(Error::Io)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
//! # #[cfg(feature = "native-tls")]
|
||||
//! 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.
|
||||
//! // 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};
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
136
tests/builder_integration.rs
Normal file
136
tests/builder_integration.rs
Normal 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());
|
||||
}
|
||||
|
|
@ -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<native_tls::TlsStream<TcpStream>>) {
|
||||
fn clean_mailbox(session: &mut imap::Session<Connection>) {
|
||||
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<native_tls::TlsStream<TcpStream>> {
|
||||
fn session_with_options(user: &str, clean: bool) -> imap::Session<Connection> {
|
||||
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<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)
|
||||
s.set_acl(
|
||||
mailbox,
|
||||
|
|
@ -111,7 +100,7 @@ fn delete_mailbox(s: &mut imap::Session<native_tls::TlsStream<TcpStream>>, mailb
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue