Skip to content

Commit 764e796

Browse files
committed
Switch to rsasl SASL provider
1 parent 1a1b5ea commit 764e796

File tree

7 files changed

+162
-81
lines changed

7 files changed

+162
-81
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ regex = "1.0"
2626
bufstream = "0.1.3"
2727
imap-proto = "0.16.1"
2828
nom = { version = "7.1.0", default-features = false }
29-
base64 = "0.13"
29+
rsasl = { git = "https://github.com/dequbed/rsasl", branch = "development", features = ["provider_base64"] }
3030
chrono = { version = "0.4", default-features = false, features = ["std"]}
3131
lazy_static = "1.4"
3232
ouroboros = "0.15.0"
@@ -41,6 +41,8 @@ encoding = "0.2.32"
4141
failure = "0.1.8"
4242
mime = "0.3.4"
4343

44+
rsasl = { git = "https://github.com/dequbed/rsasl", branch = "development", features = ["config_builder", "scram-sha-2", "plain"] }
45+
4446
[[example]]
4547
name = "basic"
4648
required-features = ["default"]

examples/rustls_sasl.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
extern crate imap;
2+
3+
use std::{env, error::Error};
4+
use rsasl::config::SASLConfig;
5+
use rsasl::prelude::Mechname;
6+
7+
fn main() -> Result<(), Box<dyn Error>> {
8+
// Read config from environment or .env file
9+
let host = env::var("HOST").expect("missing envvar host");
10+
let user = env::var("MAILUSER").expect("missing envvar USER");
11+
let password = env::var("PASSWORD").expect("missing envvar password");
12+
let port = 993;
13+
14+
if let Some(email) = fetch_inbox_top(host, user, password, port)? {
15+
println!("{}", &email);
16+
}
17+
18+
Ok(())
19+
}
20+
21+
fn fetch_inbox_top(
22+
host: String,
23+
user: String,
24+
password: String,
25+
port: u16,
26+
) -> Result<Option<String>, Box<dyn Error>> {
27+
let client = imap::ClientBuilder::new(&host, port).rustls()?;
28+
29+
let saslconfig = SASLConfig::with_credentials(None, user, password).unwrap();
30+
31+
let mechanism = Mechname::parse(b"SCRAM-SHA-256").unwrap();
32+
33+
// the client we have here is unauthenticated.
34+
// to do anything useful with the e-mails, we need to log in
35+
let mut imap_session = client.authenticate(saslconfig, mechanism).map_err(|e| e.0)?;
36+
37+
// we want to fetch the first email in the INBOX mailbox
38+
imap_session.select("INBOX")?;
39+
40+
// fetch message number 1 in this mailbox, along with its RFC822 field.
41+
// RFC 822 dictates the format of the body of e-mails
42+
let messages = imap_session.fetch("1", "RFC822")?;
43+
let message = if let Some(m) = messages.iter().next() {
44+
m
45+
} else {
46+
return Ok(None);
47+
};
48+
49+
// extract the message's body
50+
let body = message.body().expect("message did not have a body!");
51+
let body = std::str::from_utf8(body)
52+
.expect("message was not valid utf-8")
53+
.to_string();
54+
55+
// be nice to the server and log out
56+
imap_session.logout()?;
57+
58+
Ok(Some(body))
59+
}

src/authenticator.rs

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/client.rs

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ use std::io::{Read, Write};
66
use std::ops::{Deref, DerefMut};
77
use std::str;
88
use std::sync::mpsc;
9+
use std::sync::Arc;
10+
use rsasl::prelude::{Mechname, SASLClient, SASLConfig, Session as SASLSession, State as SASLState};
911

10-
use super::authenticator::Authenticator;
1112
use super::error::{Bad, Bye, Error, No, ParseError, Result, ValidateError};
1213
use super::extensions;
1314
use super::parse::*;
@@ -426,70 +427,91 @@ impl<T: Read + Write> Client<T> {
426427
/// };
427428
/// }
428429
/// ```
429-
pub fn authenticate<A: Authenticator>(
430+
pub fn authenticate(
430431
mut self,
431-
auth_type: impl AsRef<str>,
432-
authenticator: &A,
432+
config: Arc<SASLConfig>,
433+
mechanism: &Mechname,
433434
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
435+
let client = SASLClient::new(config);
436+
let session = ok_or_unauth_client_err!(
437+
// TODO: Allow rsasl to select the mechanism
438+
client.start_suggested(&[mechanism]).map_err(Error::AuthenticationSetup),
439+
self
440+
);
441+
// TODO: check for `SASL-IR` capability and send initial data if it's supported.
434442
ok_or_unauth_client_err!(
435-
self.run_command(&format!("AUTHENTICATE {}", auth_type.as_ref())),
443+
self.run_command(&format!("AUTHENTICATE {}", mechanism.as_str())),
436444
self
437445
);
438-
self.do_auth_handshake(authenticator)
446+
self.do_auth_handshake(session)
439447
}
440448

441449
/// This func does the handshake process once the authenticate command is made.
442-
fn do_auth_handshake<A: Authenticator>(
450+
fn do_auth_handshake(
443451
mut self,
444-
authenticator: &A,
452+
mut authenticator: SASLSession,
445453
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
446-
// TODO Clean up this code
447-
loop {
448-
let mut line = Vec::new();
449-
450-
// explicit match blocks neccessary to convert error to tuple and not bind self too
451-
// early (see also comment on `login`)
452-
ok_or_unauth_client_err!(self.readline(&mut line), self);
453-
454-
// ignore server comments
455-
if line.starts_with(b"* ") {
456-
continue;
457-
}
458-
459-
// Some servers will only send `+\r\n`.
460-
if line.starts_with(b"+ ") || &line == b"+\r\n" {
461-
let challenge = if &line == b"+\r\n" {
462-
Vec::new()
463-
} else {
464-
let line_str = ok_or_unauth_client_err!(
465-
match str::from_utf8(line.as_slice()) {
466-
Ok(line_str) => Ok(line_str),
467-
Err(e) => Err(Error::Parse(ParseError::DataNotUtf8(line, e))),
468-
},
469-
self
470-
);
471-
let data =
472-
ok_or_unauth_client_err!(parse_authenticate_response(line_str), self);
473-
ok_or_unauth_client_err!(
474-
base64::decode(data).map_err(|e| Error::Parse(ParseError::Authentication(
475-
data.to_string(),
476-
Some(e)
477-
))),
478-
self
479-
)
480-
};
481-
482-
let raw_response = &authenticator.process(&challenge);
483-
let auth_response = base64::encode(raw_response);
484-
ok_or_unauth_client_err!(
485-
self.write_line(auth_response.into_bytes().as_slice()),
454+
let mut line = Vec::new();
455+
let mut output = Vec::new();
456+
let mut state = SASLState::Running;
457+
458+
while {
459+
while {
460+
// Drop all read data
461+
line.clear();
462+
// explicit match blocks necessary to convert error to tuple and not bind self too
463+
// early (see also comment on `login`)
464+
ok_or_unauth_client_err!(self.readline(&mut line), self);
465+
466+
// ignore server comments — keep going until a line not starting with * is found
467+
line.starts_with(b"* ")
468+
} {}
469+
470+
line.starts_with(b"+ ") || &line == b"+\r\n"
471+
} {
472+
let challenge = if &line == b"+\r\n" {
473+
None
474+
} else {
475+
let line_str = ok_or_unauth_client_err!(
476+
match str::from_utf8(line.as_slice()) {
477+
Ok(line_str) => Ok(line_str),
478+
Err(e) => Err(Error::Parse(ParseError::DataNotUtf8(line.clone(), e))),
479+
},
486480
self
487481
);
488-
} else {
489-
ok_or_unauth_client_err!(self.read_response_onto(&mut line), self);
490-
return Ok(Session::new(self.conn));
491-
}
482+
let data = ok_or_unauth_client_err!(parse_authenticate_response(line_str), self);
483+
if data.is_empty() {
484+
None
485+
} else {
486+
Some(data.as_bytes())
487+
}
488+
};
489+
490+
// Remove all data written in a previous round
491+
output.clear();
492+
state = ok_or_unauth_client_err!(
493+
authenticator.step64(challenge, &mut output).map_err(Error::Authentication),
494+
self
495+
);
496+
497+
ok_or_unauth_client_err!(
498+
self.write_line(output.as_slice()),
499+
self
500+
);
501+
}
502+
503+
// It's important to call the SASL mechanism one last time when the server indicates
504+
// finished authentication but the SASL session doesn't; otherwise a server could subvert
505+
// mutual authentication.
506+
if state.is_running() {
507+
ok_or_unauth_client_err!(
508+
authenticator.step64(None, &mut output).map_err(Error::Authentication),
509+
self
510+
);
492511
}
512+
513+
ok_or_unauth_client_err!(self.read_response_onto(&mut line), self);
514+
return Ok(Session::new(self.conn));
493515
}
494516
}
495517

src/error.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ use std::net::TcpStream;
77
use std::result;
88
use std::str::Utf8Error;
99

10-
use base64::DecodeError;
1110
use bufstream::IntoInnerError as BufError;
1211
use imap_proto::{types::ResponseCode, Response};
1312
#[cfg(feature = "native-tls")]
1413
use native_tls::Error as TlsError;
1514
#[cfg(feature = "native-tls")]
1615
use native_tls::HandshakeError as TlsHandshakeError;
16+
use rsasl::prelude::{SASLError, SessionError as SASLSessionError};
1717
#[cfg(feature = "rustls-tls")]
1818
use rustls_connector::HandshakeError as RustlsHandshakeError;
1919

@@ -92,6 +92,10 @@ pub enum Error {
9292
ConnectionLost,
9393
/// Error parsing a server response.
9494
Parse(ParseError),
95+
/// Error occurred when tyring to set up authentication
96+
AuthenticationSetup(SASLError),
97+
/// Error occurred during authentication
98+
Authentication(SASLSessionError),
9599
/// Command inputs were not valid [IMAP
96100
/// strings](https://tools.ietf.org/html/rfc3501#section-4.3).
97101
Validate(ValidateError),
@@ -118,6 +122,17 @@ impl From<ParseError> for Error {
118122
}
119123
}
120124

125+
impl From<SASLError> for Error {
126+
fn from(err: SASLError) -> Self {
127+
Error::AuthenticationSetup(err)
128+
}
129+
}
130+
impl From<SASLSessionError> for Error {
131+
fn from(err: SASLSessionError) -> Self {
132+
Error::Authentication(err)
133+
}
134+
}
135+
121136
impl<T> From<BufError<T>> for Error {
122137
fn from(err: BufError<T>) -> Error {
123138
Error::Io(err.into())
@@ -170,6 +185,8 @@ impl fmt::Display for Error {
170185
Error::Append => f.write_str("Could not append mail to mailbox"),
171186
Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r),
172187
Error::MissingStatusResponse => write!(f, "Missing STATUS Response"),
188+
Error::AuthenticationSetup(ref e) => fmt::Display::fmt(e, f),
189+
Error::Authentication(ref e) => fmt::Display::fmt(e, f),
173190
}
174191
}
175192
}
@@ -194,6 +211,8 @@ impl StdError for Error {
194211
Error::Append => "Could not append mail to mailbox",
195212
Error::Unexpected(_) => "Unexpected Response",
196213
Error::MissingStatusResponse => "Missing STATUS Response",
214+
Error::AuthenticationSetup(_) => "Failed to setup authentication",
215+
Error::Authentication(_) => "Authentication Failed",
197216
}
198217
}
199218

@@ -207,6 +226,8 @@ impl StdError for Error {
207226
#[cfg(feature = "native-tls")]
208227
Error::TlsHandshake(ref e) => Some(e),
209228
Error::Parse(ParseError::DataNotUtf8(_, ref e)) => Some(e),
229+
Error::AuthenticationSetup(ref e) => Some(e),
230+
Error::Authentication(ref e) => Some(e),
210231
_ => None,
211232
}
212233
}
@@ -218,7 +239,7 @@ pub enum ParseError {
218239
/// Indicates an error parsing the status response. Such as OK, NO, and BAD.
219240
Invalid(Vec<u8>),
220241
/// The client could not find or decode the server's authentication challenge.
221-
Authentication(String, Option<DecodeError>),
242+
Authentication(String),
222243
/// The client received data that was not UTF-8 encoded.
223244
DataNotUtf8(Vec<u8>, Utf8Error),
224245
}
@@ -227,7 +248,7 @@ impl fmt::Display for ParseError {
227248
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228249
match *self {
229250
ParseError::Invalid(_) => f.write_str("Unable to parse status response"),
230-
ParseError::Authentication(_, _) => {
251+
ParseError::Authentication(_) => {
231252
f.write_str("Unable to parse authentication response")
232253
}
233254
ParseError::DataNotUtf8(_, _) => f.write_str("Unable to parse data as UTF-8 text"),
@@ -239,17 +260,10 @@ impl StdError for ParseError {
239260
fn description(&self) -> &str {
240261
match *self {
241262
ParseError::Invalid(_) => "Unable to parse status response",
242-
ParseError::Authentication(_, _) => "Unable to parse authentication response",
263+
ParseError::Authentication(_) => "Unable to parse authentication response",
243264
ParseError::DataNotUtf8(_, _) => "Unable to parse data as UTF-8 text",
244265
}
245266
}
246-
247-
fn cause(&self) -> Option<&dyn StdError> {
248-
match *self {
249-
ParseError::Authentication(_, Some(ref e)) => Some(e),
250-
_ => None,
251-
}
252-
}
253267
}
254268

255269
/// An [invalid character](https://tools.ietf.org/html/rfc3501#section-4.3) was found in a command

src/lib.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ mod utils;
8282

8383
pub mod types;
8484

85-
mod authenticator;
86-
pub use crate::authenticator::Authenticator;
87-
8885
mod client;
8986
pub use crate::client::*;
9087
mod client_builder;

src/parse.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ pub fn parse_authenticate_response(line: &str) -> Result<&str> {
1818
let data = cap.get(1).map(|x| x.as_str()).unwrap_or("");
1919
return Ok(data);
2020
}
21-
Err(Error::Parse(ParseError::Authentication(
22-
line.to_string(),
23-
None,
24-
)))
21+
Err(Error::Parse(ParseError::Authentication(line.to_string())))
2522
}
2623

2724
pub(crate) enum MapOrNot<'a, T> {

0 commit comments

Comments
 (0)