Skip to content

Commit

Permalink
Change the client builder so that it abstracts away connecting to TLS…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
urkle committed Oct 5, 2023
1 parent 2be8210 commit 779ea7d
Show file tree
Hide file tree
Showing 17 changed files with 595 additions and 112 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/gmail_oauth2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion examples/idle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/rustls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions examples/starttls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
43 changes: 40 additions & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) => {
Expand Down Expand Up @@ -412,7 +424,7 @@ impl<T: Read + Write> Client<T> {
/// user: String::from("[email protected]"),
/// 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) {
Expand Down Expand Up @@ -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"

Check warning on line 1853 in src/client.rs

View check run for this annotation

Codecov / codecov/patch

src/client.rs#L1853

Added line #L1853 was not covered by tests
);
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();
Expand Down
Loading

0 comments on commit 779ea7d

Please sign in to comment.