Skip to content

Commit d645422

Browse files
committed
Switch to rsasl SASL provider
1 parent 3a65407 commit d645422

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::*;
@@ -432,70 +433,91 @@ impl<T: Read + Write> Client<T> {
432433
/// };
433434
/// }
434435
/// ```
435-
pub fn authenticate<A: Authenticator>(
436+
pub fn authenticate(
436437
mut self,
437-
auth_type: impl AsRef<str>,
438-
authenticator: &A,
438+
config: Arc<SASLConfig>,
439+
mechanism: &Mechname,
439440
) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
441+
let client = SASLClient::new(config);
442+
let session = ok_or_unauth_client_err!(
443+
// TODO: Allow rsasl to select the mechanism
444+
client.start_suggested(&[mechanism]).map_err(Error::AuthenticationSetup),
445+
self
446+
);
447+
// TODO: check for `SASL-IR` capability and send initial data if it's supported.
440448
ok_or_unauth_client_err!(
441-
self.run_command(&format!("AUTHENTICATE {}", auth_type.as_ref())),
449+
self.run_command(&format!("AUTHENTICATE {}", mechanism.as_str())),
442450
self
443451
);
444-
self.do_auth_handshake(authenticator)
452+
self.do_auth_handshake(session)
445453
}
446454

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

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
@@ -81,9 +81,6 @@ mod parse;
8181

8282
pub mod types;
8383

84-
mod authenticator;
85-
pub use crate::authenticator::Authenticator;
86-
8784
mod client;
8885
pub use crate::client::*;
8986
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)