IMAP authentication

This commit is contained in:
Mauro D 2022-12-21 17:06:48 +00:00
parent fc5b297581
commit 6da860cc21
7 changed files with 399 additions and 67 deletions

86
Cargo.lock generated
View file

@ -61,9 +61,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "async-trait"
version = "0.1.59"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3"
dependencies = [
"proc-macro2",
"quote",
@ -76,7 +76,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"hermit-abi 0.1.19",
"libc",
"winapi",
]
@ -93,6 +93,12 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]]
name = "base64ct"
version = "1.5.3"
@ -621,6 +627,15 @@ dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "hmac"
version = "0.12.1"
@ -701,9 +716,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "jobserver"
@ -814,6 +829,20 @@ dependencies = [
"encoding_rs",
]
[[package]]
name = "mail-send"
version = "0.3.0"
dependencies = [
"base64 0.20.0",
"gethostname",
"md5",
"rustls",
"smtp-proto",
"tokio",
"tokio-rustls",
"webpki-roots",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
@ -826,6 +855,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.5.0"
@ -922,11 +957,11 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi",
"hermit-abi 0.2.6",
"libc",
]
@ -1097,9 +1132,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.47"
version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
dependencies = [
"unicode-ident",
]
@ -1121,9 +1156,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.21"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
@ -1311,14 +1346,14 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
dependencies = [
"base64",
"base64 0.13.1",
]
[[package]]
name = "ryu"
version = "1.0.11"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
[[package]]
name = "same-file"
@ -1367,9 +1402,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.89"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
dependencies = [
"itoa",
"ryu",
@ -1467,6 +1502,7 @@ dependencies = [
"criterion",
"dashmap",
"mail-auth",
"mail-send",
"parking_lot",
"regex",
"rustls",
@ -1513,9 +1549,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.105"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [
"proc-macro2",
"quote",
@ -1542,18 +1578,18 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
[[package]]
name = "thiserror"
version = "1.0.37"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.37"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote",
@ -1787,9 +1823,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.5"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
[[package]]
name = "unicode-normalization"

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
mail-auth = { path = "/home/vagrant/code/mail-auth" }
mail-send = { path = "/home/vagrant/code/mail-send", default-features = false, features = ["cram-md5"] }
smtp-proto = { path = "/home/vagrant/code/smtp-proto" }
ahash = { version = "0.8" }
rustls = "0.20"

View file

@ -7,8 +7,10 @@ use std::{
use dashmap::DashMap;
use smtp_proto::{
request::receiver::{BdatReceiver, DataReceiver, DummyDataReceiver, Receiver},
MtPriority, Request,
request::receiver::{
BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, RequestReceiver,
},
MtPriority,
};
use tokio::io::{AsyncRead, AsyncWrite};
use tracing::Span;
@ -30,10 +32,11 @@ pub struct Core {
}
pub enum State {
Request(Receiver<Request<String>>),
Request(RequestReceiver),
Bdat(BdatReceiver),
Data(DataReceiver),
DataTooLarge(DummyDataReceiver),
RequestTooLarge(DummyLineReceiver),
None,
}
@ -151,7 +154,7 @@ impl SessionData {
impl Default for State {
fn default() -> Self {
State::Request(Receiver::default())
State::Request(RequestReceiver::default())
}
}

View file

@ -1,3 +1,4 @@
pub mod config;
pub mod core;
pub mod listener;
pub mod remote;

View file

@ -1,8 +1,8 @@
use std::{net::IpAddr, time::SystemTime};
use smtp_proto::{
request::receiver::{BdatReceiver, DataReceiver, DummyDataReceiver},
Capability, EhloResponse, Error, Request,
request::receiver::{BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver},
*,
};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
@ -254,6 +254,10 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
)
.await?;
}
Error::ResponseTooLong => {
state = State::RequestTooLarge(DummyLineReceiver::default());
continue 'outer;
}
},
}
},
@ -301,6 +305,14 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
break 'outer;
}
}
State::RequestTooLarge(receiver) => {
if receiver.ingest(&mut iter) {
self.write(b"554 5.3.4 Line is too long.\r\n").await?;
state = State::default();
} else {
break 'outer;
}
}
State::None => unreachable!(),
}
}
@ -318,63 +330,55 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
self.eval_mail_params();
let mut response = EhloResponse::new(self.instance.hostname.as_str());
response.capabilities.push(Capability::EnhancedStatusCodes);
response.capabilities.push(Capability::EightBitMime);
response.capabilities.push(Capability::BinaryMime);
response.capabilities.push(Capability::SmtpUtf8);
response.capabilities =
EXT_ENHANCED_STATUS_CODES | EXT_8BIT_MIME | EXT_BINARY_MIME | EXT_SMTP_UTF8;
if self.params.starttls {
response.capabilities.push(Capability::StartTls);
response.capabilities |= EXT_START_TLS;
}
if self.params.pipelining {
response.capabilities.push(Capability::Pipelining);
response.capabilities |= EXT_PIPELINING;
}
if self.params.chunking {
response.capabilities.push(Capability::Chunking);
response.capabilities |= EXT_CHUNKING;
}
if self.params.expn {
response.capabilities.push(Capability::Expn);
response.capabilities |= EXT_EXPN;
}
if self.params.requiretls {
response.capabilities.push(Capability::RequireTls);
response.capabilities |= EXT_REQUIRE_TLS;
}
if self.params.auth_mechanisms != 0 {
response.capabilities.push(Capability::Auth {
mechanisms: self.params.auth_mechanisms,
});
response.capabilities |= EXT_AUTH;
response.auth_mechanisms = self.params.auth_mechanisms;
}
if let Some(value) = &self.params.future_release {
response.capabilities.push(Capability::FutureRelease {
max_interval: value.as_secs(),
max_datetime: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
+ value.as_secs(),
});
response.capabilities |= EXT_FUTURE_RELEASE;
response.future_release_interval = value.as_secs();
response.future_release_datetime = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
+ value.as_secs();
}
if let Some(value) = &self.params.deliver_by {
response.capabilities.push(Capability::DeliverBy {
min: value.as_secs(),
});
response.capabilities |= EXT_DELIVER_BY;
response.deliver_by = value.as_secs();
}
if let Some(value) = &self.params.mt_priority {
response
.capabilities
.push(Capability::MtPriority { priority: *value });
response.capabilities |= EXT_MT_PRIORITY;
response.mt_priority = *value;
}
if let Some(value) = &self.params.size {
response
.capabilities
.push(Capability::Size { size: *value });
response.capabilities |= EXT_SIZE;
response.size = *value;
}
if let Some(value) = &self.params.no_soliciting {
response.capabilities.push(Capability::NoSoliciting {
keywords: if !value.is_empty() {
value.to_string().into()
} else {
None
},
});
response.capabilities |= EXT_NO_SOLICITING;
response.no_soliciting = if !value.is_empty() {
value.to_string().into()
} else {
None
};
}
// Generate response

286
src/remote/imap.rs Normal file
View file

@ -0,0 +1,286 @@
use std::time::Duration;
use mail_send::Credentials;
use rustls::ServerName;
use smtp_proto::{
request::{parser::Rfc5321Parser, AUTH},
response::generate::BitToString,
IntoString, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2,
};
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
net::{TcpStream, ToSocketAddrs},
};
use tokio_rustls::{client::TlsStream, TlsConnector};
pub struct ImapAuthClient<T: AsyncRead + AsyncWrite> {
stream: T,
timeout: Duration,
}
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Timeout,
InvalidResponse(String),
InvalidChallenge(String),
TLSInvalidName,
Disconnected,
}
impl ImapAuthClient<TcpStream> {
async fn start_tls(
mut self,
tls_connector: &TlsConnector,
tls_hostname: &str,
) -> Result<ImapAuthClient<TlsStream<TcpStream>>, Error> {
let line = tokio::time::timeout(self.timeout, async {
self.stream.write_all(b"C7 STARTTLS\r\n").await?;
self.read_line().await
})
.await
.map_err(|_| Error::Timeout)??;
if matches!(line.get(..5), Some(b"C7 OK")) {
self.into_tls(tls_connector, tls_hostname).await
} else {
Err(Error::InvalidResponse(line.into_string()))
}
}
async fn into_tls(
self,
tls_connector: &TlsConnector,
tls_hostname: &str,
) -> Result<ImapAuthClient<TlsStream<TcpStream>>, Error> {
tokio::time::timeout(self.timeout, async {
Ok(ImapAuthClient {
stream: tls_connector
.connect(
ServerName::try_from(tls_hostname).map_err(|_| Error::TLSInvalidName)?,
self.stream,
)
.await?,
timeout: self.timeout,
})
})
.await
.map_err(|_| Error::Timeout)?
}
}
impl ImapAuthClient<TlsStream<TcpStream>> {
pub async fn connect(
addr: impl ToSocketAddrs,
timeout: Duration,
tls_connector: &TlsConnector,
tls_hostname: &str,
tls_implicit: bool,
) -> Result<Self, Error> {
let mut client: ImapAuthClient<TcpStream> = tokio::time::timeout(timeout, async {
match TcpStream::connect(addr).await {
Ok(stream) => Ok(ImapAuthClient { stream, timeout }),
Err(err) => Err(Error::Io(err)),
}
})
.await
.map_err(|_| Error::Timeout)??;
if tls_implicit {
let mut client = client.into_tls(tls_connector, tls_hostname).await?;
client.expect_greeting().await?;
Ok(client)
} else {
client.expect_greeting().await?;
client.start_tls(tls_connector, tls_hostname).await
}
}
}
impl<T: AsyncRead + AsyncWrite + Unpin> ImapAuthClient<T> {
pub async fn authenticate(
&mut self,
mechanism: u64,
credentials: &Credentials<String>,
) -> Result<(), Error> {
if (mechanism & (AUTH_PLAIN | AUTH_XOAUTH2 | AUTH_OAUTHBEARER)) != 0 {
self.stream
.write_all(
format!(
"C3 AUTHENTICATE {} {}\r\n",
mechanism.to_mechanism(),
credentials
.encode(mechanism, "")
.map_err(|err| Error::InvalidChallenge(err.to_string()))?
)
.as_bytes(),
)
.await?;
} else {
self.stream
.write_all(format!("C3 AUTHENTICATE {}\r\n", mechanism.to_mechanism()).as_bytes())
.await?;
}
let mut line = self.read_line().await?;
for _ in 0..3 {
if matches!(line.first(), Some(b'+')) {
self.stream
.write_all(
format!(
"{}\r\n",
credentials
.encode(
mechanism,
std::str::from_utf8(line.get(2..).unwrap_or_default())
.unwrap_or_default()
)
.map_err(|err| Error::InvalidChallenge(err.to_string()))?
)
.as_bytes(),
)
.await?;
line = self.read_line().await?;
} else if matches!(line.get(..5), Some(b"C3 OK")) {
return Ok(());
} else {
return Err(Error::InvalidResponse(line.into_string()));
}
}
Err(Error::InvalidResponse(line.into_string()))
}
pub async fn authentication_mechanisms(&mut self) -> Result<u64, Error> {
tokio::time::timeout(self.timeout, async {
self.stream.write_all(b"C0 CAPABILITY\r\n").await?;
let line = self.read_line().await?;
if !matches!(line.get(..12), Some(b"* CAPABILITY")) {
return Err(Error::InvalidResponse(line.into_string()));
}
let mut line_iter = line.iter();
let mut parser = Rfc5321Parser::new(&mut line_iter);
let mut mechanisms = 0;
'outer: while let Ok(ch) = parser.read_char() {
if ch == b' ' {
loop {
if parser.hashed_value().unwrap_or(0) == AUTH && parser.stop_char == b'=' {
if let Ok(Some(mechanism)) = parser.mechanism() {
mechanisms |= mechanism;
}
match parser.stop_char {
b' ' => (),
b'\n' => break 'outer,
_ => break,
}
}
}
} else if ch == b'\n' {
break;
}
}
Ok(mechanisms)
})
.await
.map_err(|_| Error::Timeout)?
}
pub async fn noop(&mut self) -> Result<(), Error> {
tokio::time::timeout(self.timeout, async {
self.stream.write_all(b"C8 NOOP\r\n").await?;
self.read_line().await?;
Ok(())
})
.await
.map_err(|_| Error::Timeout)?
}
pub async fn logout(&mut self) -> Result<(), Error> {
tokio::time::timeout(self.timeout, async {
self.stream.write_all(b"C9 LOGOUT\r\n").await?;
Ok(())
})
.await
.map_err(|_| Error::Timeout)?
}
pub async fn expect_greeting(&mut self) -> Result<(), Error> {
tokio::time::timeout(self.timeout, async {
let line = self.read_line().await?;
return if matches!(line.get(..4), Some(b"* OK")) {
Ok(())
} else {
Err(Error::InvalidResponse(line.into_string()))
};
})
.await
.map_err(|_| Error::Timeout)?
}
pub async fn read_line(&mut self) -> Result<Vec<u8>, Error> {
let mut buf = vec![0u8; 1024];
let mut buf_extended = Vec::with_capacity(0);
loop {
let br = self.stream.read(&mut buf).await?;
if br > 0 {
if matches!(buf.get(br - 1), Some(b'\n')) {
//println!("{:?}", std::str::from_utf8(&buf[..br]).unwrap());
return Ok(if buf_extended.is_empty() {
buf.truncate(br);
buf
} else {
buf_extended.extend_from_slice(&buf[..br]);
buf_extended
});
} else if buf_extended.is_empty() {
buf_extended = buf[..br].to_vec();
} else {
buf_extended.extend_from_slice(&buf[..br]);
}
} else {
return Err(Error::Disconnected);
}
}
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Error::Io(error)
}
}
#[cfg(test)]
mod test {
use crate::remote::imap::ImapAuthClient;
use mail_send::smtp::tls::build_tls_connector;
use smtp_proto::{AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH, AUTH_XOAUTH2};
use std::time::Duration;
#[ignore]
#[tokio::test]
async fn imap_auth() {
let connector = build_tls_connector(false);
let mut client = ImapAuthClient::connect(
"imap.gmail.com:993",
Duration::from_secs(5),
&connector,
"imap.gmail.com",
true,
)
.await
.unwrap();
assert_eq!(
AUTH_PLAIN | AUTH_XOAUTH | AUTH_XOAUTH2 | AUTH_OAUTHBEARER,
client.authentication_mechanisms().await.unwrap()
);
client.logout().await.unwrap();
}
}

1
src/remote/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod imap;