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
check_script:
- . $HOME/.cargo/env
- cargo check --all-targets
- cargo check --all-targets --all-features
build_script:
- . $HOME/.cargo/env
- cargo build --all-targets --verbose
- cargo build --all-targets --verbose --all-features
test_script:
- . $HOME/.cargo/env
- cargo test --examples
- cargo test --doc
- cargo test --lib
- cargo test --examples --all-features
- cargo test --doc --all-features
- cargo test --lib --all-features

View file

@ -15,10 +15,12 @@ categories = ["email", "network-programming"]
[features]
tls = ["native-tls"]
rustls-tls = ["rustls-connector"]
default = ["tls"]
[dependencies]
native-tls = { version = "0.2.2", optional = true }
rustls-connector = { version = "0.13.1", optional = true }
regex = "1.0"
bufstream = "0.1"
imap-proto = "0.14.1"
@ -45,6 +47,18 @@ required-features = ["default"]
name = "idle"
required-features = ["default"]
[[example]]
name = "rustls"
required-features = ["rustls-tls"]
[[example]]
name = "starttls"
required-features = ["default"]
[[example]]
name = "timeout"
required-features = ["default"]
[[test]]
name = "imap_integration"
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/
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
challenge/response authentication respectively. This in turn gives you an authenticated
[`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.
```rust
extern crate imap;
extern crate native_tls;
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
// certificate is valid for the domain we're connecting to.
let client = imap::connect((domain, 993), domain, &tls).unwrap();
let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?;
// the client we have here is unauthenticated.
// 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`
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

View file

@ -25,13 +25,13 @@ jobs:
- template: install-rust.yml@templates
parameters:
rust: $(rust)
- script: cargo check --all-targets
- script: cargo check --all-targets --all-features
displayName: cargo check
- script: cargo test --examples
- script: cargo test --examples --all-features
displayName: cargo test --examples
- script: cargo test --doc
- script: cargo test --doc --all-features
displayName: cargo test --doc
- script: cargo test --lib
- script: cargo test --lib --all-features
displayName: cargo test --lib
- script: |
set -e
@ -75,6 +75,18 @@ jobs:
greenmail: greenmail
env:
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:
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:
* 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.
* 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).
* 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 native_tls;
fn main() {
// 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>> {
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
// certificate is valid for the domain we're connecting to.
let client = imap::connect((domain, 993), domain, &tls).unwrap();
let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?;
// the client we have here is unauthenticated.
// to do anything useful with the e-mails, we need to log in

View file

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

View file

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

View file

@ -1,9 +1,6 @@
extern crate imap;
extern crate rustls_connector;
use std::{env, error::Error, net::TcpStream};
use rustls_connector::RustlsConnector;
use std::{env, error::Error};
fn main() -> Result<(), Box<dyn Error>> {
// Read config from environment or .env file
@ -25,14 +22,7 @@ fn fetch_inbox_top(
password: String,
port: u16,
) -> Result<Option<String>, Box<dyn Error>> {
// Setup Rustls TcpStream
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);
let client = imap::ClientBuilder::new(&host, port).rustls()?;
// the client we have here is unauthenticated.
// 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.
* 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
* as you have to when using TLS the entire way.
*
* The only difference is calling `starttls()` on the `ClientBuilder` before
* 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:
* - IMAP_HOST
@ -11,9 +12,7 @@
* - IMAP_PORT (supposed to be 143)
*/
extern crate imap;
extern crate native_tls;
use native_tls::TlsConnector;
use std::env;
use std::error::Error;
@ -42,13 +41,10 @@ fn fetch_inbox_top(
password: String,
port: u16,
) -> Result<Option<String>, Box<dyn Error>> {
let domain: &str = host.as_str();
let 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_starttls((domain, port), domain, &tls).unwrap();
let client = imap::ClientBuilder::new(&host, port)
.starttls()
.native_tls()
.expect("Could not connect to server");
// the client we have here is unauthenticated.
// 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)
}
// resolve address and try to connect to all in order; note that this function is required to fully
// mimic `imap::connect` with the usage of `ToSocketAddrs`
// resolve address and try to connect to all in order
fn connect_all_timeout<A: ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,

View file

@ -1,11 +1,8 @@
use bufstream::BufStream;
use chrono::{DateTime, FixedOffset};
use imap_proto::Response;
#[cfg(feature = "tls")]
use native_tls::{TlsConnector, TlsStream};
use std::collections::HashSet;
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::ops::{Deref, DerefMut};
use std::str;
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
// 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> {
/// 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
/// rustls.rs in the examples/ directory.
/// This method primarily exists for writing tests that mock the underlying transport,
/// 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
/// also be used to support IMAP over custom tunnels.
/// For an example, see `examples/timeout.rs` which uses a custom timeout on the
/// tcp stream.
///
/// **Note:** In case you do need to use `Client::new` over `imap::connect`, you will need to
/// listen for the IMAP protocol server greeting before authenticating:
/// **Note:** In case you do need to use `Client::new` instead of the `ClientBuilder`
/// you will need to listen for the IMAP protocol server greeting before authenticating:
///
/// ```rust,no_run
/// # extern crate imap;
/// # extern crate native_tls;
/// # use imap::Client;
/// # use native_tls::TlsConnector;
/// # use std::io;
/// # use std::net::TcpStream;
/// # {} #[cfg(feature = "tls")]
/// # fn main() {
/// # let server = "imap.example.com";
/// # let username = "";
/// # let password = "";
/// # let tcp = TcpStream::connect((server, 993)).unwrap();
/// # use native_tls::TlsConnector;
/// # let ssl_connector = TlsConnector::builder().build().unwrap();
/// # let tls = TlsConnector::connect(&ssl_connector, server.as_ref(), tcp).unwrap();
/// 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
/// 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
@ -421,16 +325,10 @@ impl<T: Read + Write> Client<T> {
/// transferred back to the caller.
///
/// ```rust,no_run
/// # extern crate imap;
/// # extern crate native_tls;
/// # use std::io;
/// # use native_tls::TlsConnector;
/// # {} #[cfg(feature = "tls")]
/// # fn main() {
/// # let tls_connector = TlsConnector::builder().build().unwrap();
/// let client = imap::connect(
/// ("imap.example.org", 993),
/// "imap.example.org",
/// &tls_connector).unwrap();
/// let client = imap::ClientBuilder::new("imap.example.org", 993)
/// .native_tls().unwrap();
///
/// match client.login("user", "pass") {
/// Ok(s) => {
@ -463,10 +361,6 @@ impl<T: Read + Write> Client<T> {
/// challenge.
///
/// ```no_run
/// extern crate imap;
/// extern crate native_tls;
/// use native_tls::TlsConnector;
///
/// struct OAuth2 {
/// user: String,
/// access_token: String,
@ -482,14 +376,15 @@ impl<T: Read + Write> Client<T> {
/// }
/// }
///
/// # {} #[cfg(feature = "tls")]
/// fn main() {
/// let auth = OAuth2 {
/// user: String::from("me@example.com"),
/// access_token: String::from("<access_token>"),
/// };
/// let domain = "imap.example.com";
/// let tls = TlsConnector::builder().build().unwrap();
/// let client = imap::connect((domain, 993), domain, &tls).unwrap();
/// let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()
/// .expect("Could not connect to server");
///
/// match client.authenticate("XOAUTH2", &auth) {
/// Ok(session) => {
/// // you are successfully authenticated!
@ -1379,7 +1274,7 @@ impl<T: Read + Write> Connection<T> {
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(|_| ())
}

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

View file

@ -7,6 +7,8 @@ use crate::parse::parse_idle;
use crate::types::UnsolicitedResponse;
#[cfg(feature = "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::time::Duration;
@ -25,10 +27,10 @@ use std::time::Duration;
/// a convenience callback function [`stop_on_any`] is provided.
///
/// ```no_run
/// # use native_tls::TlsConnector;
/// use imap::extensions::idle;
/// let ssl_conn = TlsConnector::builder().build().unwrap();
/// let client = imap::connect(("example.com", 993), "example.com", &ssl_conn)
/// # #[cfg(feature = "tls")]
/// # {
/// let client = imap::ClientBuilder::new("example.com", 993).native_tls()
/// .expect("Could not connect to imap server");
/// let mut imap = client.login("user@example.com", "password")
/// .expect("Could not authenticate");
@ -39,6 +41,7 @@ use std::time::Duration;
///
/// // Exit on any mailbox change
/// 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
@ -284,3 +287,10 @@ impl<'a> SetReadTimeout for TlsStream<TcpStream> {
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/
//!
//! 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
//! challenge/response authentication respectively. This in turn gives you an authenticated
//! [`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.
//!
//! ```no_run
//! extern crate imap;
//! extern crate native_tls;
//!
//! # #[cfg(feature = "tls")]
//! 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
//! // certificate is valid for the domain we're connecting to.
//! let client = imap::connect((domain, 993), domain, &tls).unwrap();
//! let client = imap::ClientBuilder::new("imap.example.com", 993).native_tls()?;
//!
//! // the client we have here is unauthenticated.
//! // 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`
//! 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)]
#![warn(rust_2018_idioms)]
@ -90,6 +85,8 @@ pub use crate::authenticator::Authenticator;
mod client;
pub use crate::client::*;
mod client_builder;
pub use crate::client_builder::ClientBuilder;
pub mod error;
pub use error::{Error, Result};

View file

@ -18,9 +18,10 @@ use std::ops::RangeInclusive;
///
/// # Examples
/// ```no_run
/// # let domain = "imap.example.com";
/// # let tls = native_tls::TlsConnector::builder().build().unwrap();
/// # let client = imap::connect((domain, 993), domain, &tls).unwrap();
/// # {} #[cfg(feature = "tls")]
/// # fn main() {
/// # let client = imap::ClientBuilder::new("imap.example.com", 993)
/// .native_tls().unwrap();
/// # let mut session = client.login("name", "pw").unwrap();
/// // Iterate over whatever is returned
/// if let Ok(deleted) = session.expunge() {
@ -35,6 +36,7 @@ use std::ops::RangeInclusive;
/// // Do something with uid
/// }
/// }
/// # }
/// ```
#[derive(Debug, Clone)]
pub enum Deleted {

View file

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