mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-10-23 06:57:26 +00:00
This commit is contained in:
parent
0693253dff
commit
d8a73cd0e4
30 changed files with 250 additions and 85 deletions
114
Cargo.lock
generated
114
Cargo.lock
generated
|
@ -480,6 +480,12 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base32"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.11.0"
|
||||
|
@ -632,7 +638,7 @@ dependencies = [
|
|||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"constant_time_eq 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1116,6 +1122,12 @@ version = "0.2.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.0"
|
||||
|
@ -1401,11 +1413,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
version = "6.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
|
@ -1427,18 +1440,6 @@ dependencies = [
|
|||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"deadpool-runtime",
|
||||
"num_cpus",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.1"
|
||||
|
@ -1457,7 +1458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1ab8a4ea925ce79678034870834602a2980f4b88c09e97feb266496dbb4493d2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"deadpool 0.12.1",
|
||||
"deadpool",
|
||||
"getrandom",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
|
@ -1617,8 +1618,7 @@ version = "0.8.2"
|
|||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"argon2",
|
||||
"async-trait",
|
||||
"deadpool 0.10.0",
|
||||
"deadpool",
|
||||
"futures",
|
||||
"jmap_proto",
|
||||
"ldap3",
|
||||
|
@ -1642,6 +1642,7 @@ dependencies = [
|
|||
"store",
|
||||
"tokio",
|
||||
"tokio-rustls 0.25.0",
|
||||
"totp-rs",
|
||||
"tracing",
|
||||
"utils",
|
||||
]
|
||||
|
@ -3158,7 +3159,7 @@ dependencies = [
|
|||
"nlp",
|
||||
"p256",
|
||||
"pkcs8",
|
||||
"quick-xml 0.31.0",
|
||||
"quick-xml 0.34.0",
|
||||
"rand",
|
||||
"rasn",
|
||||
"rasn-cms",
|
||||
|
@ -3176,9 +3177,9 @@ dependencies = [
|
|||
"smtp-proto",
|
||||
"store",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.23.1",
|
||||
"tracing",
|
||||
"tungstenite",
|
||||
"tungstenite 0.23.0",
|
||||
"utils",
|
||||
"x509-parser 0.16.0",
|
||||
]
|
||||
|
@ -3202,7 +3203,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4645,15 +4646,6 @@ version = "1.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.32.0"
|
||||
|
@ -4664,6 +4656,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.2"
|
||||
|
@ -6131,7 +6132,7 @@ dependencies = [
|
|||
"bitpacking",
|
||||
"blake3",
|
||||
"bytes",
|
||||
"deadpool 0.12.1",
|
||||
"deadpool",
|
||||
"deadpool-postgres",
|
||||
"elasticsearch",
|
||||
"farmhash",
|
||||
|
@ -6613,10 +6614,22 @@ dependencies = [
|
|||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tungstenite",
|
||||
"tungstenite 0.21.0",
|
||||
"webpki-roots 0.26.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.11"
|
||||
|
@ -6674,6 +6687,21 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "totp-rs"
|
||||
version = "5.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f"
|
||||
dependencies = [
|
||||
"base32",
|
||||
"constant_time_eq 0.2.6",
|
||||
"hmac 0.12.1",
|
||||
"sha1",
|
||||
"sha2 0.10.8",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
|
@ -6847,6 +6875,24 @@ dependencies = [
|
|||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http 1.1.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twofish"
|
||||
version = "0.7.1"
|
||||
|
@ -7700,7 +7746,7 @@ dependencies = [
|
|||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"constant_time_eq 0.3.0",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"deflate64",
|
||||
|
|
|
@ -119,7 +119,7 @@ All documentation is available at [stalw.art/docs/get-started](https://stalw.art
|
|||
If you are having problems running Stalwart Mail Server, you found a bug or just have a question,
|
||||
do not hesitate to reach us on [Github Discussions](https://github.com/stalwartlabs/mail-server/discussions),
|
||||
[Reddit](https://www.reddit.com/r/stalwartlabs), [Discord](https://discord.gg/aVQr3jF8jd) or [Matrix](https://matrix.to/#/#stalwart:matrix.org).
|
||||
Additionally you may become a sponsor to obtain priority support from Stalwart Labs Ltd.
|
||||
Additionally you may purchase a subscription to obtain priority support from Stalwart Labs Ltd.
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "stalwart-cli"
|
||||
description = "Stalwart Mail Server CLI"
|
||||
authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
|
||||
license = "AGPL-3.0-only"
|
||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||
repository = "https://github.com/stalwartlabs/cli"
|
||||
homepage = "https://github.com/stalwartlabs/cli"
|
||||
version = "0.8.2"
|
||||
|
|
|
@ -66,9 +66,9 @@ impl AccountCommands {
|
|||
));
|
||||
}
|
||||
if let Some(password) = password {
|
||||
changes.push(PrincipalUpdate::set(
|
||||
changes.push(PrincipalUpdate::add_item(
|
||||
PrincipalField::Secrets,
|
||||
PrincipalValue::StringList(vec![sha512_crypt::hash(password).unwrap()]),
|
||||
PrincipalValue::String(sha512_crypt::hash(password).unwrap()),
|
||||
));
|
||||
}
|
||||
if let Some(description) = description {
|
||||
|
|
|
@ -20,7 +20,9 @@ use config::{
|
|||
storage::Storage,
|
||||
tracers::{OtelTracer, Tracer, Tracers},
|
||||
};
|
||||
use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy, Type};
|
||||
use directory::{
|
||||
core::secret::verify_secret_hash, Directory, DirectoryError, Principal, QueryBy, Type,
|
||||
};
|
||||
use expr::if_block::IfBlock;
|
||||
use listener::{
|
||||
blocked::{AllowedIps, BlockedIps},
|
||||
|
@ -94,6 +96,7 @@ pub enum AuthResult<T> {
|
|||
Success(T),
|
||||
Failure,
|
||||
Banned,
|
||||
MissingTotp,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -267,6 +270,7 @@ impl Core {
|
|||
return Ok(AuthResult::Success(principal));
|
||||
}
|
||||
Ok(None) => Ok(()),
|
||||
Err(DirectoryError::MissingTotpCode) => return Ok(AuthResult::MissingTotp),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
|
|
|
@ -17,9 +17,8 @@ tokio-rustls = { version = "0.25.0"}
|
|||
rustls = "0.22"
|
||||
rustls-pki-types = { version = "1" }
|
||||
ldap3 = { version = "0.11.1", default-features = false, features = ["tls-rustls"] }
|
||||
deadpool = { version = "0.10.0", features = ["managed", "rt_tokio_1"] }
|
||||
deadpool = { version = "0.12", features = ["managed", "rt_tokio_1"] }
|
||||
parking_lot = "0.12"
|
||||
async-trait = "0.1.68"
|
||||
ahash = { version = "0.8" }
|
||||
tracing = "0.1"
|
||||
lru-cache = "0.1.2"
|
||||
|
@ -34,6 +33,7 @@ md5 = "0.7.0"
|
|||
futures = "0.3"
|
||||
regex = "1.7.0"
|
||||
serde = { version = "1.0", features = ["derive"]}
|
||||
totp-rs = { version = "5.5.1", features = ["otpauth"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.23", features = ["full"] }
|
||||
|
|
|
@ -6,14 +6,12 @@
|
|||
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use deadpool::managed;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::client::TlsStream;
|
||||
|
||||
use super::{ImapClient, ImapConnectionManager, ImapError};
|
||||
|
||||
#[async_trait]
|
||||
impl managed::Manager for ImapConnectionManager {
|
||||
type Type = ImapClient<TlsStream<TcpStream>>;
|
||||
type Error = ImapError;
|
||||
|
|
|
@ -59,7 +59,7 @@ impl DirectoryStore for Store {
|
|||
.await?,
|
||||
secret,
|
||||
) {
|
||||
(Some(mut principal), Some(secret)) if principal.verify_secret(secret).await => {
|
||||
(Some(mut principal), Some(secret)) if principal.verify_secret(secret).await? => {
|
||||
if return_member_of {
|
||||
principal.member_of = self.get_member_of(principal.id).await?;
|
||||
}
|
||||
|
|
|
@ -414,6 +414,35 @@ impl ManageDirectory for Store {
|
|||
) => {
|
||||
principal.inner.secrets = secrets;
|
||||
}
|
||||
(
|
||||
PrincipalAction::AddItem,
|
||||
PrincipalField::Secrets,
|
||||
PrincipalValue::String(secret),
|
||||
) => {
|
||||
let mut do_add = true;
|
||||
let mut new_secrets = Vec::with_capacity(principal.inner.secrets.len() + 1);
|
||||
for prev_secret in principal.inner.secrets {
|
||||
if prev_secret == secret {
|
||||
do_add = false;
|
||||
} else if prev_secret.starts_with("otpauth://")
|
||||
|| prev_secret == "$disabled$"
|
||||
|| prev_secret.starts_with("$app$")
|
||||
{
|
||||
new_secrets.push(prev_secret);
|
||||
}
|
||||
}
|
||||
if do_add {
|
||||
new_secrets.push(secret);
|
||||
}
|
||||
principal.inner.secrets = new_secrets;
|
||||
}
|
||||
(
|
||||
PrincipalAction::RemoveItem,
|
||||
PrincipalField::Secrets,
|
||||
PrincipalValue::String(secret),
|
||||
) => {
|
||||
principal.inner.secrets.retain(|v| *v != secret);
|
||||
}
|
||||
(
|
||||
PrincipalAction::Set,
|
||||
PrincipalField::Description,
|
||||
|
|
|
@ -87,7 +87,7 @@ impl LdapDirectory {
|
|||
.find_principal(&mut conn, &self.mappings.filter_name.build(username))
|
||||
.await?
|
||||
{
|
||||
if principal.verify_secret(secret).await {
|
||||
if principal.verify_secret(secret).await? {
|
||||
principal
|
||||
} else {
|
||||
tracing::debug!(
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use async_trait::async_trait;
|
||||
use deadpool::managed;
|
||||
use ldap3::{exop::WhoAmI, Ldap, LdapConnAsync, LdapError};
|
||||
|
||||
use super::LdapConnectionManager;
|
||||
|
||||
#[async_trait]
|
||||
impl managed::Manager for LdapConnectionManager {
|
||||
type Type = Ldap;
|
||||
type Error = LdapError;
|
||||
|
|
|
@ -36,7 +36,7 @@ impl MemoryDirectory {
|
|||
|
||||
for principal in &self.principals {
|
||||
if &principal.name == username {
|
||||
return if principal.verify_secret(secret).await {
|
||||
return if principal.verify_secret(secret).await? {
|
||||
Ok(Some(principal.clone()))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use async_trait::async_trait;
|
||||
use deadpool::managed;
|
||||
use mail_send::{smtp::AssertReply, Error};
|
||||
|
||||
use super::{SmtpClient, SmtpConnectionManager};
|
||||
|
||||
#[async_trait]
|
||||
impl managed::Manager for SmtpConnectionManager {
|
||||
type Type = SmtpClient;
|
||||
type Error = Error;
|
||||
|
@ -45,8 +43,10 @@ impl managed::Manager for SmtpConnectionManager {
|
|||
.map(|_| ())
|
||||
.map_err(managed::RecycleError::Backend)
|
||||
} else {
|
||||
Err(managed::RecycleError::StaticMessage(
|
||||
"No longer valid: Too many authentication failures",
|
||||
Err(managed::RecycleError::Message(
|
||||
"No longer valid: Too many authentication failures"
|
||||
.to_string()
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ impl SqlDirectory {
|
|||
|
||||
// Validate password
|
||||
if let Some(secret) = secret {
|
||||
if !principal.verify_secret(secret).await {
|
||||
if !principal.verify_secret(secret).await? {
|
||||
tracing::debug!(
|
||||
context = "directory",
|
||||
event = "invalid_password",
|
||||
|
|
|
@ -16,17 +16,51 @@ use sha1::Sha1;
|
|||
use sha2::Sha256;
|
||||
use sha2::Sha512;
|
||||
use tokio::sync::oneshot;
|
||||
use totp_rs::TOTP;
|
||||
|
||||
use crate::DirectoryError;
|
||||
use crate::Principal;
|
||||
|
||||
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
|
||||
pub async fn verify_secret(&self, secret: &str) -> bool {
|
||||
for hashed_secret in &self.secrets {
|
||||
if verify_secret_hash(hashed_secret, secret).await {
|
||||
return true;
|
||||
pub async fn verify_secret(&self, mut code: &str) -> crate::Result<bool> {
|
||||
let mut totp_token = None;
|
||||
|
||||
for secret in &self.secrets {
|
||||
let mut secret = secret.as_str();
|
||||
|
||||
if secret == "$disabled$" {
|
||||
return Ok(false);
|
||||
} else if secret.starts_with("otpauth://") && totp_token.is_none() {
|
||||
let totp_token = if let Some(totp_token) = totp_token {
|
||||
totp_token
|
||||
} else {
|
||||
let (_code, _totp_token) = code
|
||||
.rsplit_once('$')
|
||||
.filter(|(c, t)| !c.is_empty() && !t.is_empty())
|
||||
.ok_or(DirectoryError::MissingTotpCode)?;
|
||||
totp_token = Some(_totp_token);
|
||||
code = _code;
|
||||
_totp_token
|
||||
};
|
||||
if !TOTP::from_url(secret)
|
||||
.map_err(DirectoryError::InvalidTotpUrl)?
|
||||
.check_current(totp_token)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
} else if let Some((_, app_secret)) =
|
||||
secret.strip_prefix("$app$").and_then(|s| s.split_once('$'))
|
||||
{
|
||||
secret = app_secret;
|
||||
}
|
||||
|
||||
if verify_secret_hash(secret, code).await {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ use deadpool::managed::PoolError;
|
|||
use ldap3::LdapError;
|
||||
use mail_send::Credentials;
|
||||
use store::Store;
|
||||
use totp_rs::TotpUrlError;
|
||||
|
||||
pub mod backend;
|
||||
pub mod core;
|
||||
|
@ -81,6 +82,8 @@ pub enum DirectoryError {
|
|||
Management(ManagementError),
|
||||
TimedOut,
|
||||
Unsupported,
|
||||
InvalidTotpUrl(TotpUrlError),
|
||||
MissingTotpCode,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
@ -309,6 +312,8 @@ impl Display for DirectoryError {
|
|||
Self::Management(error) => write!(f, "Management error: {:?}", error),
|
||||
Self::TimedOut => write!(f, "Directory timed out"),
|
||||
Self::Unsupported => write!(f, "Method not supported by directory"),
|
||||
Self::InvalidTotpUrl(error) => write!(f, "Invalid TOTP URL: {}", error),
|
||||
Self::MissingTotpCode => write!(f, "Missing TOTP code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ parking_lot = "0.12"
|
|||
tracing = "0.1"
|
||||
ahash = { version = "0.8" }
|
||||
md5 = "0.7.0"
|
||||
dashmap = "5.4"
|
||||
dashmap = "6.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
[features]
|
||||
|
|
|
@ -101,6 +101,7 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
// Authenticate
|
||||
let mut is_totp_error = false;
|
||||
let access_token = match credentials {
|
||||
Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => {
|
||||
match self
|
||||
|
@ -110,6 +111,10 @@ impl<T: SessionStream> Session<T> {
|
|||
{
|
||||
AuthResult::Success(token) => Some(token),
|
||||
AuthResult::Failure => None,
|
||||
AuthResult::MissingTotp => {
|
||||
is_totp_error = true;
|
||||
None
|
||||
}
|
||||
AuthResult::Banned => return Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -174,10 +179,14 @@ impl<T: SessionStream> Session<T> {
|
|||
Ok(())
|
||||
} else {
|
||||
self.write_bytes(
|
||||
StatusResponse::no("Authentication failed")
|
||||
.with_tag(tag)
|
||||
.with_code(ResponseCode::AuthenticationFailed)
|
||||
.into_bytes(),
|
||||
StatusResponse::no(if is_totp_error {
|
||||
"Missing TOTP code, try with 'secret$totp_code'."
|
||||
} else {
|
||||
"Authentication failed."
|
||||
})
|
||||
.with_tag(tag)
|
||||
.with_code(ResponseCode::AuthenticationFailed)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -39,10 +39,10 @@ hkdf = "0.12.3"
|
|||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
|
||||
tokio-tungstenite = "0.21"
|
||||
tungstenite = "0.21"
|
||||
tokio-tungstenite = "0.23"
|
||||
tungstenite = "0.23"
|
||||
chrono = "0.4"
|
||||
dashmap = "5.4"
|
||||
dashmap = "6.0"
|
||||
aes = "0.8.3"
|
||||
cbc = { version = "0.1.2", features = ["alloc"] }
|
||||
sequoia-openpgp = { version = "1.16", default-features = false, features = ["crypto-rust", "allow-experimental-crypto", "allow-variable-time-crypto"] }
|
||||
|
@ -56,7 +56,7 @@ async-trait = "0.1.68"
|
|||
lz4_flex = { version = "0.11", default-features = false }
|
||||
rev_lines = "0.3.0"
|
||||
x509-parser = "0.16.0"
|
||||
quick-xml = "0.31"
|
||||
quick-xml = "0.34"
|
||||
|
||||
[features]
|
||||
test_mode = []
|
||||
|
|
|
@ -221,7 +221,7 @@ fn parse_autodiscover_request(bytes: &[u8]) -> Result<String, String> {
|
|||
}
|
||||
|
||||
let mut reader = Reader::from_reader(bytes);
|
||||
reader.trim_text(true);
|
||||
reader.config_mut().trim_text(true);
|
||||
let mut buf = Vec::with_capacity(128);
|
||||
|
||||
'outer: for tag_name in ["Autodiscover", "Request", "EMailAddress"] {
|
||||
|
|
|
@ -331,9 +331,9 @@ impl JMAP {
|
|||
.data
|
||||
.update_account(
|
||||
QueryBy::Id(access_token.primary_id()),
|
||||
vec![PrincipalUpdate::set(
|
||||
vec![PrincipalUpdate::add_item(
|
||||
PrincipalField::Secrets,
|
||||
PrincipalValue::StringList(vec![new_password]),
|
||||
PrincipalValue::String(new_password),
|
||||
)],
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -46,13 +46,22 @@ impl JMAP {
|
|||
})
|
||||
})
|
||||
{
|
||||
if let AuthResult::Success(access_token) = self
|
||||
match self
|
||||
.authenticate_plain(&account, &secret, remote_ip, ServerProtocol::Http)
|
||||
.await
|
||||
{
|
||||
Some(access_token)
|
||||
} else {
|
||||
None
|
||||
AuthResult::Success(access_token) => Some(access_token),
|
||||
AuthResult::MissingTotp => {
|
||||
return Err(RequestError::blank(
|
||||
401,
|
||||
"TOTP code required",
|
||||
concat!(
|
||||
"A TOTP code is required to authenticate this account. ",
|
||||
"Try authenticating again using 'secret$totp_token'."
|
||||
),
|
||||
));
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
|
@ -161,6 +170,7 @@ impl JMAP {
|
|||
AuthResult::Failure
|
||||
}
|
||||
Ok(AuthResult::Banned) => AuthResult::Banned,
|
||||
Ok(AuthResult::MissingTotp) => AuthResult::MissingTotp,
|
||||
Err(_) => AuthResult::Failure,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ repository = "https://github.com/stalwartlabs/jmap-server"
|
|||
homepage = "https://stalw.art"
|
||||
keywords = ["imap", "jmap", "smtp", "email", "mail", "server"]
|
||||
categories = ["email"]
|
||||
license = "AGPL-3.0-only"
|
||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||
version = "0.8.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
|
|
@ -79,6 +79,7 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
// Authenticate
|
||||
let mut is_totp_error = false;
|
||||
let access_token = match credentials {
|
||||
Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => {
|
||||
match self
|
||||
|
@ -93,6 +94,10 @@ impl<T: SessionStream> Session<T> {
|
|||
{
|
||||
AuthResult::Success(token) => Some(token),
|
||||
AuthResult::Failure => None,
|
||||
AuthResult::MissingTotp => {
|
||||
is_totp_error = true;
|
||||
None
|
||||
}
|
||||
AuthResult::Banned => {
|
||||
return Err(StatusResponse::bye(
|
||||
"Too many authentication requests from this IP address.",
|
||||
|
@ -156,7 +161,12 @@ impl<T: SessionStream> Session<T> {
|
|||
self.state = State::NotAuthenticated {
|
||||
auth_failures: auth_failures + 1,
|
||||
};
|
||||
Ok(StatusResponse::no("Authentication failed").into_bytes())
|
||||
Ok(StatusResponse::no(if is_totp_error {
|
||||
"Missing TOTP code, try with 'secret$totp_code'."
|
||||
} else {
|
||||
"Authentication failed."
|
||||
})
|
||||
.into_bytes())
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!(
|
||||
|
|
|
@ -83,6 +83,7 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
// Authenticate
|
||||
let mut is_totp_error = false;
|
||||
let access_token = match credentials {
|
||||
Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => {
|
||||
match self
|
||||
|
@ -92,6 +93,10 @@ impl<T: SessionStream> Session<T> {
|
|||
{
|
||||
AuthResult::Success(token) => Some(token),
|
||||
AuthResult::Failure => None,
|
||||
AuthResult::MissingTotp => {
|
||||
is_totp_error = true;
|
||||
None
|
||||
}
|
||||
AuthResult::Banned => {
|
||||
self.write_err("Too many authentication requests from this IP address.")
|
||||
.await?;
|
||||
|
@ -164,7 +169,12 @@ impl<T: SessionStream> Session<T> {
|
|||
auth_failures: auth_failures + 1,
|
||||
username: username.clone(),
|
||||
};
|
||||
self.write_err("Authentication failed").await
|
||||
self.write_err(if is_totp_error {
|
||||
"Missing TOTP code, try with 'secret$totp_code'."
|
||||
} else {
|
||||
"Authentication failed."
|
||||
})
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!(
|
||||
|
|
|
@ -90,10 +90,8 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
self.write_ok(format!(
|
||||
"Stalwart POP3 bids you farewell (no messages deleted)."
|
||||
))
|
||||
.await?;
|
||||
self.write_ok("Stalwart POP3 bids you farewell (no messages deleted).")
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
self.write_ok("Stalwart POP3 bids you farewell.").await?;
|
||||
|
|
|
@ -6,7 +6,7 @@ repository = "https://github.com/stalwartlabs/smtp-server"
|
|||
homepage = "https://stalw.art/smtp"
|
||||
keywords = ["smtp", "email", "mail", "server"]
|
||||
categories = ["email"]
|
||||
license = "AGPL-3.0-only"
|
||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||
version = "0.8.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
@ -41,7 +41,7 @@ rayon = "1.5"
|
|||
tracing = "0.1"
|
||||
parking_lot = "0.12"
|
||||
regex = "1.7.0"
|
||||
dashmap = "5.4"
|
||||
dashmap = "6.0"
|
||||
blake3 = "1.3"
|
||||
lru-cache = "0.1.2"
|
||||
rand = "0.8.5"
|
||||
|
|
|
@ -217,6 +217,20 @@ impl<T: SessionStream> Session<T> {
|
|||
|
||||
return Err(());
|
||||
}
|
||||
Ok(AuthResult::MissingTotp) => {
|
||||
tracing::debug!(
|
||||
parent: &self.span,
|
||||
context = "auth",
|
||||
event = "authenticate",
|
||||
result = "missing-totp"
|
||||
);
|
||||
|
||||
return self
|
||||
.auth_error(
|
||||
b"334 5.7.8 Missing TOTP token, try with 'secret$totp_code'.\r\n",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -15,7 +15,7 @@ tracing = "0.1"
|
|||
mail-auth = { version = "0.4" }
|
||||
smtp-proto = { version = "0.1" }
|
||||
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
|
||||
dashmap = "5.4"
|
||||
dashmap = "6.0"
|
||||
ahash = { version = "0.8" }
|
||||
chrono = "0.4"
|
||||
rand = "0.8.5"
|
||||
|
|
|
@ -55,7 +55,7 @@ hyper = { version = "1.0.1", features = ["server", "http1", "http2"] }
|
|||
hyper-util = { version = "0.1.1", features = ["tokio"] }
|
||||
http-body-util = "0.1.0"
|
||||
base64 = "0.22"
|
||||
dashmap = "5.4"
|
||||
dashmap = "6.0"
|
||||
ahash = { version = "0.8" }
|
||||
serial_test = "3.0.0"
|
||||
num_cpus = "1.15.0"
|
||||
|
|
Loading…
Reference in a new issue