Add ClientBuilder helper to make setting up TLS connections easy. (#197)

Also replaces connect() and connect_starttls() with ClientBuilder.
This commit is contained in:
mordak 2021-05-10 21:39:46 -05:00 committed by GitHub
parent c443a3ab5d
commit 7204697dd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 314 additions and 237 deletions

View file

@ -11,12 +11,12 @@ task:
- . $HOME/.cargo/env - . $HOME/.cargo/env
check_script: check_script:
- . $HOME/.cargo/env - . $HOME/.cargo/env
- cargo check --all-targets - cargo check --all-targets --all-features
build_script: build_script:
- . $HOME/.cargo/env - . $HOME/.cargo/env
- cargo build --all-targets --verbose - cargo build --all-targets --verbose --all-features
test_script: test_script:
- . $HOME/.cargo/env - . $HOME/.cargo/env
- cargo test --examples - cargo test --examples --all-features
- cargo test --doc - cargo test --doc --all-features
- cargo test --lib - cargo test --lib --all-features

View file

@ -15,10 +15,12 @@ categories = ["email", "network-programming"]
[features] [features]
tls = ["native-tls"] tls = ["native-tls"]
rustls-tls = ["rustls-connector"]
default = ["tls"] default = ["tls"]
[dependencies] [dependencies]
native-tls = { version = "0.2.2", optional = true } native-tls = { version = "0.2.2", optional = true }
rustls-connector = { version = "0.13.1", optional = true }
regex = "1.0" regex = "1.0"
bufstream = "0.1" bufstream = "0.1"
imap-proto = "0.14.1" imap-proto = "0.14.1"
@ -45,6 +47,18 @@ required-features = ["default"]
name = "idle" name = "idle"
required-features = ["default"] required-features = ["default"]
[[example]]
name = "rustls"
required-features = ["rustls-tls"]
[[example]]
name = "starttls"
required-features = ["default"]
[[example]]
name = "timeout"
required-features = ["default"]
[[test]] [[test]]
name = "imap_integration" name = "imap_integration"
required-features = ["default"] required-features = ["default"]

View file

@ -22,7 +22,7 @@ results](https://dev.azure.com/jonhoo/jonhoo/_build/latest?definitionId=11&branc
[@jonhoo]: https://thesquareplanet.com/ [@jonhoo]: https://thesquareplanet.com/
To connect, use the [`connect`] function. This gives you an unauthenticated [`Client`]. You can To connect, use the [`ClientBuilder`]. This gives you an unauthenticated [`Client`]. You can
then use [`Client::login`] or [`Client::authenticate`] to perform username/password or then use [`Client::login`] or [`Client::authenticate`] to perform username/password or
challenge/response authentication respectively. This in turn gives you an authenticated challenge/response authentication respectively. This in turn gives you an authenticated
[`Session`], which lets you access the mailboxes at the server. [`Session`], which lets you access the mailboxes at the server.
@ -34,16 +34,9 @@ in the documentation for the various types and methods and read the raw text the
Below is a basic client example. See the `examples/` directory for more. Below is a basic client example. See the `examples/` directory for more.
```rust ```rust
extern crate imap;
extern crate native_tls;
fn fetch_inbox_top() -> imap::error::Result<Option<String>> { fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
let domain = "imap.example.com";
let tls = native_tls::TlsConnector::builder().build().unwrap();
// we pass in the domain twice to check that the server's TLS let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?;
// certificate is valid for the domain we're connecting to.
let client = imap::connect((domain, 993), domain, &tls).unwrap();
// 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
@ -90,7 +83,8 @@ default-features = false
``` ```
Even without `native_tls`, you can still use TLS by leveraging the pure Rust `rustls` Even without `native_tls`, you can still use TLS by leveraging the pure Rust `rustls`
crate. See the example/rustls.rs file for a working example. crate, which is enabled with the `rustls-tls` feature. See the example/rustls.rs file
for a working example.
## Running the test suite ## Running the test suite

View file

@ -25,13 +25,13 @@ jobs:
- template: install-rust.yml@templates - template: install-rust.yml@templates
parameters: parameters:
rust: $(rust) rust: $(rust)
- script: cargo check --all-targets - script: cargo check --all-targets --all-features
displayName: cargo check displayName: cargo check
- script: cargo test --examples - script: cargo test --examples --all-features
displayName: cargo test --examples displayName: cargo test --examples
- script: cargo test --doc - script: cargo test --doc --all-features
displayName: cargo test --doc displayName: cargo test --doc
- script: cargo test --lib - script: cargo test --lib --all-features
displayName: cargo test --lib displayName: cargo test --lib
- script: | - script: |
set -e set -e
@ -75,6 +75,18 @@ jobs:
greenmail: greenmail greenmail: greenmail
env: env:
TEST_HOST: greenmail TEST_HOST: greenmail
- job: features
displayName: "Check feature combinations"
pool:
vmImage: ubuntu-latest
steps:
- template: install-rust.yml@templates
parameters:
rust: stable
- script: cargo install cargo-hack
displayName: install cargo-hack
- script: cargo hack --feature-powerset check --all-targets
displayName: cargo hack
resources: resources:
repositories: repositories:

17
codecov.yml Normal file
View file

@ -0,0 +1,17 @@
coverage:
range: 70..100
round: down
precision: 2
status:
project:
default:
threshold: 2%
# Tests aren't important for coverage
ignore:
- "tests"
# Make less noisy comments
comment:
layout: "files"
require_changes: yes

View file

@ -6,5 +6,7 @@ This directory contains examples of working with the IMAP client.
Examples: Examples:
* basic - This is a very basic example of using the client. * basic - This is a very basic example of using the client.
* gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure appplication. * gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure appplication.
* idle - This is an example showing how to use IDLE to monitor a mailbox.
* rustls - This demonstrates how to use Rustls instead of Openssl for secure connections (helpful for cross compilation). * rustls - This demonstrates how to use Rustls instead of Openssl for secure connections (helpful for cross compilation).
* timeout - This demonstrates how to use timeouts while connecting to an IMAP server. * starttls - This is an example showing how to use STARTTLS after connecting over plaintext.
* timeout - This demonstrates how to use timeouts while connecting to an IMAP server by using a custom TCP/TLS stream initialization and creating a `Client` directly instead of using the `ClientBuilder`.

View file

@ -1,5 +1,4 @@
extern crate imap; extern crate imap;
extern crate native_tls;
fn main() { fn main() {
// To connect to the gmail IMAP server with this you will need to allow unsecure apps access. // To connect to the gmail IMAP server with this you will need to allow unsecure apps access.
@ -9,12 +8,7 @@ fn main() {
} }
fn fetch_inbox_top() -> imap::error::Result<Option<String>> { fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
let domain = "imap.example.com"; let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?;
let tls = native_tls::TlsConnector::builder().build().unwrap();
// we pass in the domain twice to check that the server's TLS
// certificate is valid for the domain we're connecting to.
let client = imap::connect((domain, 993), domain, &tls).unwrap();
// 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

@ -1,8 +1,5 @@
extern crate base64; extern crate base64;
extern crate imap; extern crate imap;
extern crate native_tls;
use native_tls::TlsConnector;
struct GmailOAuth2 { struct GmailOAuth2 {
user: String, user: String,
@ -25,11 +22,10 @@ fn main() {
user: String::from("sombody@gmail.com"), user: String::from("sombody@gmail.com"),
access_token: String::from("<access_token>"), access_token: String::from("<access_token>"),
}; };
let domain = "imap.gmail.com";
let port = 993; let client = imap::ClientBuilder::new("imap.gmail.com", 993)
let socket_addr = (domain, port); .native_tls()
let ssl_connector = TlsConnector::builder().build().unwrap(); .expect("Could not connect to imap.gmail.com");
let client = imap::connect(socket_addr, domain, &ssl_connector).unwrap();
let mut imap_session = match client.authenticate("XOAUTH2", &gmail_auth) { let mut imap_session = match client.authenticate("XOAUTH2", &gmail_auth) {
Ok(c) => c, Ok(c) => c,

View file

@ -1,4 +1,3 @@
use native_tls::TlsConnector;
use structopt::StructOpt; use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
@ -39,9 +38,10 @@ struct Opt {
fn main() { fn main() {
let opt = Opt::from_args(); let opt = Opt::from_args();
let ssl_conn = TlsConnector::builder().build().unwrap(); let client = imap::ClientBuilder::new(opt.server.clone(), opt.port)
let client = imap::connect((opt.server.clone(), opt.port), opt.server, &ssl_conn) .native_tls()
.expect("Could not connect to imap server"); .expect("Could not connect to imap server");
let mut imap = client let mut imap = client
.login(opt.username, opt.password) .login(opt.username, opt.password)
.expect("Could not authenticate"); .expect("Could not authenticate");

View file

@ -1,9 +1,6 @@
extern crate imap; extern crate imap;
extern crate rustls_connector;
use std::{env, error::Error, net::TcpStream}; use std::{env, error::Error};
use rustls_connector::RustlsConnector;
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
// Read config from environment or .env file // Read config from environment or .env file
@ -25,14 +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>> {
// Setup Rustls TcpStream let client = imap::ClientBuilder::new(&host, port).rustls()?;
let stream = TcpStream::connect((host.as_ref(), port))?;
let tls = RustlsConnector::default();
let tlsstream = tls.connect(&host, stream)?;
// we pass in the domain twice to check that the server's TLS
// certificate is valid for the domain we're connecting to.
let client = imap::Client::new(tlsstream);
// 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

@ -1,8 +1,9 @@
/** /**
* 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 with the `basic.rs` example is when using `imap::connect_starttls()` method *
* instead of `imap::connect()` (l. 52), and so you can connect on port 143 instead of 993 * The only difference is calling `starttls()` on the `ClientBuilder` before
* as you have to when using TLS the entire way. * initiating the secure connection with `native_tls()` or `rustls()`, so you
* can connect on port 143 instead of 993.
* *
* The following env vars are expected to be set: * The following env vars are expected to be set:
* - IMAP_HOST * - IMAP_HOST
@ -11,9 +12,7 @@
* - IMAP_PORT (supposed to be 143) * - IMAP_PORT (supposed to be 143)
*/ */
extern crate imap; extern crate imap;
extern crate native_tls;
use native_tls::TlsConnector;
use std::env; use std::env;
use std::error::Error; use std::error::Error;
@ -42,13 +41,10 @@ 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 domain: &str = host.as_str(); let client = imap::ClientBuilder::new(&host, port)
.starttls()
let tls = TlsConnector::builder().build().unwrap(); .native_tls()
.expect("Could not connect to server");
// we pass in the domain twice to check that the server's TLS
// certificate is valid for the domain we're connecting to.
let client = imap::connect_starttls((domain, port), domain, &tls).unwrap();
// 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

@ -58,8 +58,7 @@ fn connect_timeout<S: AsRef<str>>(
Ok(client) Ok(client)
} }
// resolve address and try to connect to all in order; note that this function is required to fully // resolve address and try to connect to all in order
// mimic `imap::connect` with the usage of `ToSocketAddrs`
fn connect_all_timeout<A: ToSocketAddrs, S: AsRef<str>>( fn connect_all_timeout<A: ToSocketAddrs, S: AsRef<str>>(
addr: A, addr: A,
domain: S, domain: S,

View file

@ -1,11 +1,8 @@
use bufstream::BufStream; use bufstream::BufStream;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use imap_proto::Response; use imap_proto::Response;
#[cfg(feature = "tls")]
use native_tls::{TlsConnector, TlsStream};
use std::collections::HashSet; use std::collections::HashSet;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::str; use std::str;
use std::sync::mpsc; use std::sync::mpsc;
@ -253,109 +250,6 @@ impl<T: Read + Write> DerefMut for Session<T> {
} }
} }
/// Connect to a server using a TLS-encrypted connection.
///
/// The returned [`Client`] is unauthenticated; to access session-related methods (through
/// [`Session`]), use [`Client::login`] or [`Client::authenticate`].
///
/// The domain must be passed in separately from the `TlsConnector` so that the certificate of the
/// IMAP server can be validated.
///
/// # Examples
///
/// ```no_run
/// # extern crate native_tls;
/// # extern crate imap;
/// # use std::io;
/// # use native_tls::TlsConnector;
/// # fn main() {
/// let tls = TlsConnector::builder().build().unwrap();
/// let client = imap::connect(("imap.example.org", 993), "imap.example.org", &tls).unwrap();
/// # }
/// ```
#[cfg(feature = "tls")]
pub fn connect<A: ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
ssl_connector: &TlsConnector,
) -> Result<Client<TlsStream<TcpStream>>> {
match TcpStream::connect(addr) {
Ok(stream) => {
let ssl_stream = match TlsConnector::connect(ssl_connector, domain.as_ref(), stream) {
Ok(s) => s,
Err(e) => return Err(Error::TlsHandshake(e)),
};
let mut socket = Client::new(ssl_stream);
socket.read_greeting()?;
Ok(socket)
}
Err(e) => Err(Error::Io(e)),
}
}
/// Connect to a server and upgrade to a TLS-encrypted connection.
///
/// This is the [STARTTLS](https://tools.ietf.org/html/rfc2595) equivalent to [`connect`]. All
/// notes there also apply here.
///
/// # Examples
///
/// ```no_run
/// # extern crate native_tls;
/// # extern crate imap;
/// # use std::io;
/// # use native_tls::TlsConnector;
/// # fn main() {
/// let tls = TlsConnector::builder().build().unwrap();
/// let client = imap::connect_starttls(("imap.example.org", 143), "imap.example.org", &tls).unwrap();
/// # }
/// ```
#[cfg(feature = "tls")]
pub fn connect_starttls<A: ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
ssl_connector: &TlsConnector,
) -> Result<Client<TlsStream<TcpStream>>> {
match TcpStream::connect(addr) {
Ok(stream) => {
let mut socket = Client::new(stream);
socket.read_greeting()?;
socket.run_command_and_check_ok("STARTTLS")?;
TlsConnector::connect(
ssl_connector,
domain.as_ref(),
socket.conn.stream.into_inner()?,
)
.map(Client::new)
.map_err(Error::TlsHandshake)
}
Err(e) => Err(Error::Io(e)),
}
}
impl Client<TcpStream> {
/// This will upgrade an IMAP client from using a regular TCP connection to use TLS.
///
/// The domain parameter is required to perform hostname verification.
#[cfg(feature = "tls")]
pub fn secure<S: AsRef<str>>(
mut self,
domain: S,
ssl_connector: &TlsConnector,
) -> Result<Client<TlsStream<TcpStream>>> {
// TODO This needs to be tested
self.run_command_and_check_ok("STARTTLS")?;
TlsConnector::connect(
ssl_connector,
domain.as_ref(),
self.conn.stream.into_inner()?,
)
.map(Client::new)
.map_err(Error::TlsHandshake)
}
}
// As the pattern of returning the unauthenticated `Client` (a.k.a. `self`) back with a login error // As the pattern of returning the unauthenticated `Client` (a.k.a. `self`) back with a login error
// is relatively common, it's abstacted away into a macro here. // is relatively common, it's abstacted away into a macro here.
// //
@ -375,27 +269,28 @@ macro_rules! ok_or_unauth_client_err {
impl<T: Read + Write> Client<T> { impl<T: Read + Write> Client<T> {
/// Creates a new client over the given stream. /// Creates a new client over the given stream.
/// ///
/// For an example of how to use this method to provide a pure-Rust TLS integration, see the /// This method primarily exists for writing tests that mock the underlying transport,
/// rustls.rs in the examples/ directory. /// but can also be used to support IMAP over custom tunnels. If you do not need to do
/// that, then it is simpler to use the [`ClientBuilder`](crate::ClientBuilder) to get
/// a new client.
/// ///
/// This method primarily exists for writing tests that mock the underlying transport, but can /// For an example, see `examples/timeout.rs` which uses a custom timeout on the
/// also be used to support IMAP over custom tunnels. /// tcp stream.
/// ///
/// **Note:** In case you do need to use `Client::new` over `imap::connect`, you will need to /// **Note:** In case you do need to use `Client::new` instead of the `ClientBuilder`
/// listen for the IMAP protocol server greeting before authenticating: /// you will need to listen for the IMAP protocol server greeting before authenticating:
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # extern crate imap;
/// # extern crate native_tls;
/// # use imap::Client; /// # use imap::Client;
/// # use native_tls::TlsConnector;
/// # use std::io; /// # use std::io;
/// # use std::net::TcpStream; /// # use std::net::TcpStream;
/// # {} #[cfg(feature = "tls")]
/// # fn main() { /// # fn main() {
/// # let server = "imap.example.com"; /// # let server = "imap.example.com";
/// # let username = ""; /// # let username = "";
/// # let password = ""; /// # let password = "";
/// # let tcp = TcpStream::connect((server, 993)).unwrap(); /// # let tcp = TcpStream::connect((server, 993)).unwrap();
/// # use native_tls::TlsConnector;
/// # let ssl_connector = TlsConnector::builder().build().unwrap(); /// # let ssl_connector = TlsConnector::builder().build().unwrap();
/// # let tls = TlsConnector::connect(&ssl_connector, server.as_ref(), tcp).unwrap(); /// # let tls = TlsConnector::connect(&ssl_connector, server.as_ref(), tcp).unwrap();
/// let mut client = Client::new(tls); /// let mut client = Client::new(tls);
@ -414,6 +309,15 @@ impl<T: Read + Write> Client<T> {
} }
} }
/// Yield the underlying connection for this Client.
///
/// This consumes `self` since the Client is not much use without
/// an underlying transport.
pub(crate) fn into_inner(self) -> Result<T> {
let res = self.conn.stream.into_inner()?;
Ok(res)
}
/// 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
@ -421,16 +325,10 @@ impl<T: Read + Write> Client<T> {
/// transferred back to the caller. /// transferred back to the caller.
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # extern crate imap; /// # {} #[cfg(feature = "tls")]
/// # extern crate native_tls;
/// # use std::io;
/// # use native_tls::TlsConnector;
/// # fn main() { /// # fn main() {
/// # let tls_connector = TlsConnector::builder().build().unwrap(); /// let client = imap::ClientBuilder::new("imap.example.org", 993)
/// let client = imap::connect( /// .native_tls().unwrap();
/// ("imap.example.org", 993),
/// "imap.example.org",
/// &tls_connector).unwrap();
/// ///
/// match client.login("user", "pass") { /// match client.login("user", "pass") {
/// Ok(s) => { /// Ok(s) => {
@ -463,10 +361,6 @@ impl<T: Read + Write> Client<T> {
/// challenge. /// challenge.
/// ///
/// ```no_run /// ```no_run
/// extern crate imap;
/// extern crate native_tls;
/// use native_tls::TlsConnector;
///
/// struct OAuth2 { /// struct OAuth2 {
/// user: String, /// user: String,
/// access_token: String, /// access_token: String,
@ -482,14 +376,15 @@ impl<T: Read + Write> Client<T> {
/// } /// }
/// } /// }
/// ///
/// # {} #[cfg(feature = "tls")]
/// fn main() { /// fn main() {
/// let auth = OAuth2 { /// let auth = OAuth2 {
/// 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 domain = "imap.example.com"; /// let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()
/// let tls = TlsConnector::builder().build().unwrap(); /// .expect("Could not connect to server");
/// let client = imap::connect((domain, 993), domain, &tls).unwrap(); ///
/// match client.authenticate("XOAUTH2", &auth) { /// match client.authenticate("XOAUTH2", &auth) {
/// Ok(session) => { /// Ok(session) => {
/// // you are successfully authenticated! /// // you are successfully authenticated!
@ -1379,7 +1274,7 @@ impl<T: Read + Write> Connection<T> {
Ok(v) Ok(v)
} }
fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { pub(crate) fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> {
self.run_command_and_read_response(command).map(|_| ()) self.run_command_and_read_response(command).map(|_| ())
} }

144
src/client_builder.rs Normal file
View file

@ -0,0 +1,144 @@
use crate::{Client, Result};
use std::io::{Read, Write};
use std::net::TcpStream;
#[cfg(feature = "tls")]
use native_tls::{TlsConnector, TlsStream};
#[cfg(feature = "rustls-tls")]
use rustls_connector::{RustlsConnector, TlsStream as RustlsStream};
/// A convenience builder for [`Client`] structs over various encrypted transports.
///
/// Creating a [`Client`] using `native-tls` transport is straightforward:
/// ```no_run
/// # use imap::ClientBuilder;
/// # {} #[cfg(feature = "tls")]
/// # fn main() -> Result<(), imap::Error> {
/// let client = ClientBuilder::new("imap.example.com", 993).native_tls()?;
/// # Ok(())
/// # }
/// ```
///
/// Similarly, if using the `rustls-tls` feature you can create a [`Client`] using rustls:
/// ```no_run
/// # use imap::ClientBuilder;
/// # {} #[cfg(feature = "rustls-tls")]
/// # fn main() -> Result<(), imap::Error> {
/// let client = ClientBuilder::new("imap.example.com", 993).rustls()?;
/// # Ok(())
/// # }
/// ```
///
/// To use `STARTTLS`, just call `starttls()` before one of the [`Client`]-yielding
/// functions:
/// ```no_run
/// # use imap::ClientBuilder;
/// # {} #[cfg(feature = "rustls-tls")]
/// # fn main() -> Result<(), imap::Error> {
/// let client = ClientBuilder::new("imap.example.com", 993)
/// .starttls()
/// .rustls()?;
/// # Ok(())
/// # }
/// ```
/// The returned [`Client`] is unauthenticated; to access session-related methods (through
/// [`Session`](crate::Session)), use [`Client::login`] or [`Client::authenticate`].
pub struct ClientBuilder<D>
where
D: AsRef<str>,
{
domain: D,
port: u16,
starttls: bool,
}
impl<D> ClientBuilder<D>
where
D: AsRef<str>,
{
/// Make a new `ClientBuilder` using the given domain and port.
pub fn new(domain: D, port: u16) -> Self {
ClientBuilder {
domain,
port,
starttls: false,
}
}
/// Use [`STARTTLS`](https://tools.ietf.org/html/rfc2595) for this connection.
#[cfg(any(feature = "tls", feature = "rustls-tls"))]
pub fn starttls(&mut self) -> &mut Self {
self.starttls = true;
self
}
/// Return a new [`Client`] using a `native-tls` transport.
#[cfg(feature = "tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
pub fn native_tls(&mut self) -> Result<Client<TlsStream<TcpStream>>> {
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<Client<RustlsStream<TcpStream>>> {
self.connect(|domain, tcp| {
let ssl_conn = RustlsConnector::new_with_native_certs()?;
Ok(ssl_conn.connect(domain, tcp)?)
})
}
/// Make a [`Client`] using a custom TLS initialization. This function is intended
/// to be used if your TLS setup requires custom work such as adding private CAs
/// or other specific TLS parameters.
///
/// The `handshake` argument should accept two parameters:
///
/// - domain: [`&str`]
/// - tcp: [`TcpStream`]
///
/// and yield a `Result<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
/// # use imap::ClientBuilder;
/// # use rustls_connector::RustlsConnector;
/// # {} #[cfg(feature = "rustls-tls")]
/// # fn main() -> Result<(), imap::Error> {
/// let client = ClientBuilder::new("imap.example.com", 993)
/// .starttls()
/// .connect(|domain, tcp| {
/// let ssl_conn = RustlsConnector::new_with_native_certs()?;
/// Ok(ssl_conn.connect(domain, tcp)?)
/// })?;
/// # Ok(())
/// # }
/// ```
pub fn connect<F, C>(&mut self, handshake: F) -> Result<Client<C>>
where
F: FnOnce(&str, TcpStream) -> Result<C>,
C: Read + Write,
{
let tcp = if self.starttls {
let tcp = TcpStream::connect((self.domain.as_ref(), self.port))?;
let mut client = Client::new(tcp);
client.read_greeting()?;
client.run_command_and_check_ok("STARTTLS")?;
client.into_inner()?
} else {
TcpStream::connect((self.domain.as_ref(), self.port))?
};
let tls = handshake(self.domain.as_ref(), tcp)?;
Ok(Client::new(tls))
}
}

View file

@ -3,7 +3,6 @@
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(feature = "tls")]
use std::net::TcpStream; use std::net::TcpStream;
use std::result; use std::result;
use std::str::Utf8Error; use std::str::Utf8Error;
@ -15,6 +14,8 @@ use imap_proto::{types::ResponseCode, Response};
use native_tls::Error as TlsError; use native_tls::Error as TlsError;
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use native_tls::HandshakeError as TlsHandshakeError; use native_tls::HandshakeError as TlsHandshakeError;
#[cfg(feature = "rustls-tls")]
use rustls_connector::HandshakeError as RustlsHandshakeError;
/// A convenience wrapper around `Result` for `imap::Error`. /// A convenience wrapper around `Result` for `imap::Error`.
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
@ -57,6 +58,9 @@ impl fmt::Display for No {
pub enum Error { pub enum Error {
/// An `io::Error` that occurred while trying to read or write to a network stream. /// An `io::Error` that occurred while trying to read or write to a network stream.
Io(IoError), Io(IoError),
/// An error from the `rustls` library during the TLS handshake.
#[cfg(feature = "rustls-tls")]
RustlsHandshake(RustlsHandshakeError<TcpStream>),
/// An error from the `native_tls` library during the TLS handshake. /// An error from the `native_tls` library during the TLS handshake.
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
TlsHandshake(TlsHandshakeError<TcpStream>), TlsHandshake(TlsHandshakeError<TcpStream>),
@ -100,6 +104,13 @@ impl<T> From<BufError<T>> for Error {
} }
} }
#[cfg(feature = "rustls-tls")]
impl From<RustlsHandshakeError<TcpStream>> for Error {
fn from(err: RustlsHandshakeError<TcpStream>) -> Error {
Error::RustlsHandshake(err)
}
}
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
impl From<TlsHandshakeError<TcpStream>> for Error { impl From<TlsHandshakeError<TcpStream>> for Error {
fn from(err: TlsHandshakeError<TcpStream>) -> Error { fn from(err: TlsHandshakeError<TcpStream>) -> Error {
@ -124,6 +135,8 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { match *self {
Error::Io(ref e) => fmt::Display::fmt(e, f), Error::Io(ref e) => fmt::Display::fmt(e, f),
#[cfg(feature = "rustls-tls")]
Error::RustlsHandshake(ref e) => fmt::Display::fmt(e, f),
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
Error::Tls(ref e) => fmt::Display::fmt(e, f), Error::Tls(ref e) => fmt::Display::fmt(e, f),
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
@ -144,6 +157,8 @@ impl StdError for Error {
fn description(&self) -> &str { fn description(&self) -> &str {
match *self { match *self {
Error::Io(ref e) => e.description(), Error::Io(ref e) => e.description(),
#[cfg(feature = "rustls-tls")]
Error::RustlsHandshake(ref e) => e.description(),
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
Error::Tls(ref e) => e.description(), Error::Tls(ref e) => e.description(),
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
@ -161,6 +176,8 @@ impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> { fn cause(&self) -> Option<&dyn StdError> {
match *self { match *self {
Error::Io(ref e) => Some(e), Error::Io(ref e) => Some(e),
#[cfg(feature = "rustls-tls")]
Error::RustlsHandshake(ref e) => Some(e),
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
Error::Tls(ref e) => Some(e), Error::Tls(ref e) => Some(e),
#[cfg(feature = "tls")] #[cfg(feature = "tls")]

View file

@ -7,6 +7,8 @@ use crate::parse::parse_idle;
use crate::types::UnsolicitedResponse; use crate::types::UnsolicitedResponse;
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use native_tls::TlsStream; use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")]
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::time::Duration; use std::time::Duration;
@ -25,10 +27,10 @@ use std::time::Duration;
/// a convenience callback function [`stop_on_any`] is provided. /// a convenience callback function [`stop_on_any`] is provided.
/// ///
/// ```no_run /// ```no_run
/// # use native_tls::TlsConnector;
/// use imap::extensions::idle; /// use imap::extensions::idle;
/// let ssl_conn = TlsConnector::builder().build().unwrap(); /// # #[cfg(feature = "tls")]
/// let client = imap::connect(("example.com", 993), "example.com", &ssl_conn) /// # {
/// let client = imap::ClientBuilder::new("example.com", 993).native_tls()
/// .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");
@ -39,6 +41,7 @@ use std::time::Duration;
/// ///
/// // Exit on any mailbox change /// // Exit on any mailbox change
/// let result = idle.wait_keepalive_while(idle::stop_on_any); /// let result = idle.wait_keepalive_while(idle::stop_on_any);
/// # }
/// ``` /// ```
/// ///
/// Note that the server MAY consider a client inactive if it has an IDLE command running, and if /// Note that the server MAY consider a client inactive if it has an IDLE command running, and if
@ -284,3 +287,10 @@ impl<'a> SetReadTimeout for TlsStream<TcpStream> {
self.get_ref().set_read_timeout(timeout).map_err(Error::Io) self.get_ref().set_read_timeout(timeout).map_err(Error::Io)
} }
} }
#[cfg(feature = "rustls-tls")]
impl<'a> SetReadTimeout for RustlsStream<TcpStream> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
self.get_ref().set_read_timeout(timeout).map_err(Error::Io)
}
}

View file

@ -9,7 +9,7 @@
//! //!
//! [@jonhoo]: https://thesquareplanet.com/ //! [@jonhoo]: https://thesquareplanet.com/
//! //!
//! To connect, use the [`connect`] function. This gives you an unauthenticated [`Client`]. You can //! To connect, use the [`ClientBuilder`]. This gives you an unauthenticated [`Client`]. You can
//! then use [`Client::login`] or [`Client::authenticate`] to perform username/password or //! then use [`Client::login`] or [`Client::authenticate`] to perform username/password or
//! challenge/response authentication respectively. This in turn gives you an authenticated //! challenge/response authentication respectively. This in turn gives you an authenticated
//! [`Session`], which lets you access the mailboxes at the server. //! [`Session`], which lets you access the mailboxes at the server.
@ -21,16 +21,10 @@
//! Below is a basic client example. See the `examples/` directory for more. //! Below is a basic client example. See the `examples/` directory for more.
//! //!
//! ```no_run //! ```no_run
//! extern crate imap; //! # #[cfg(feature = "tls")]
//! extern crate native_tls;
//!
//! fn fetch_inbox_top() -> imap::error::Result<Option<String>> { //! fn fetch_inbox_top() -> imap::error::Result<Option<String>> {
//! let domain = "imap.example.com";
//! let tls = native_tls::TlsConnector::builder().build().unwrap();
//! //!
//! // we pass in the domain twice to check that the server's TLS //! let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?;
//! // certificate is valid for the domain we're connecting to.
//! let client = imap::connect((domain, 993), domain, &tls).unwrap();
//! //!
//! // 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
@ -77,7 +71,8 @@
//! ``` //! ```
//! //!
//! Even without `native_tls`, you can still use TLS by leveraging the pure Rust `rustls` //! Even without `native_tls`, you can still use TLS by leveraging the pure Rust `rustls`
//! crate. See the example/rustls.rs file for a working example. //! crate, which is enabled with the `rustls-tls` feature. See the example/rustls.rs file
//! for a working example.
#![deny(missing_docs)] #![deny(missing_docs)]
#![warn(rust_2018_idioms)] #![warn(rust_2018_idioms)]
@ -90,6 +85,8 @@ pub use crate::authenticator::Authenticator;
mod client; mod client;
pub use crate::client::*; pub use crate::client::*;
mod client_builder;
pub use crate::client_builder::ClientBuilder;
pub mod error; pub mod error;
pub use error::{Error, Result}; pub use error::{Error, Result};

View file

@ -18,9 +18,10 @@ use std::ops::RangeInclusive;
/// ///
/// # Examples /// # Examples
/// ```no_run /// ```no_run
/// # let domain = "imap.example.com"; /// # {} #[cfg(feature = "tls")]
/// # let tls = native_tls::TlsConnector::builder().build().unwrap(); /// # fn main() {
/// # let client = imap::connect((domain, 993), domain, &tls).unwrap(); /// # let client = imap::ClientBuilder::new("imap.example.com", 993)
/// .native_tls().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() {
@ -35,6 +36,7 @@ use std::ops::RangeInclusive;
/// // Do something with uid /// // Do something with uid
/// } /// }
/// } /// }
/// # }
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Deleted { pub enum Deleted {

View file

@ -19,17 +19,15 @@ fn tls() -> native_tls::TlsConnector {
} }
fn session(user: &str) -> imap::Session<native_tls::TlsStream<TcpStream>> { fn session(user: &str) -> imap::Session<native_tls::TlsStream<TcpStream>> {
let mut s = imap::connect( let host = std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string());
&format!( let mut s = imap::ClientBuilder::new(&host, 3993)
"{}:3993", .connect(|domain, tcp| {
std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()) let ssl_conn = tls();
), Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap())
"imap.example.com", })
&tls(), .unwrap()
) .login(user, user)
.unwrap() .unwrap();
.login(user, user)
.unwrap();
s.debug = true; s.debug = true;
s s
} }
@ -55,25 +53,25 @@ fn smtp(user: &str) -> lettre::SmtpTransport {
#[ignore] #[ignore]
fn connect_insecure_then_secure() { fn connect_insecure_then_secure() {
let host = std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()); let host = std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string());
let stream = TcpStream::connect((host.as_ref(), 3143)).unwrap();
// ignored because of https://github.com/greenmail-mail-test/greenmail/issues/135 // ignored because of https://github.com/greenmail-mail-test/greenmail/issues/135
imap::Client::new(stream) imap::ClientBuilder::new(&host, 3143)
.secure("imap.example.com", &tls()) .starttls()
.connect(|domain, tcp| {
let ssl_conn = tls();
Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap())
})
.unwrap(); .unwrap();
} }
#[test] #[test]
fn connect_secure() { fn connect_secure() {
imap::connect( let host = std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string());
&format!( imap::ClientBuilder::new(&host, 3993)
"{}:3993", .connect(|domain, tcp| {
std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()) let ssl_conn = tls();
), Ok(native_tls::TlsConnector::connect(&ssl_conn, domain, tcp).unwrap())
"imap.example.com", })
&tls(), .unwrap();
)
.unwrap();
} }
#[test] #[test]