mirror of
https://github.com/stalwartlabs/smtp-server.git
synced 2024-10-23 06:57:29 +00:00
IMAP authentication
This commit is contained in:
parent
fc5b297581
commit
6da860cc21
7 changed files with 399 additions and 67 deletions
86
Cargo.lock
generated
86
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod config;
|
||||
pub mod core;
|
||||
pub mod listener;
|
||||
pub mod remote;
|
||||
|
|
|
@ -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
286
src/remote/imap.rs
Normal 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
1
src/remote/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod imap;
|
Loading…
Reference in a new issue