mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Automatic spam filter and webadmin downloading + quickstart
This commit is contained in:
parent
89433f3f06
commit
ab47eab1d9
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,7 +2,6 @@
|
||||||
.vscode
|
.vscode
|
||||||
*.failed
|
*.failed
|
||||||
*_failed
|
*_failed
|
||||||
/resources/config/config.toml
|
|
||||||
run.sh
|
run.sh
|
||||||
_ignore
|
_ignore
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -995,6 +995,7 @@ dependencies = [
|
||||||
"decancer",
|
"decancer",
|
||||||
"directory",
|
"directory",
|
||||||
"futures",
|
"futures",
|
||||||
|
"hostname 0.4.0",
|
||||||
"hyper 1.2.0",
|
"hyper 1.2.0",
|
||||||
"idna 0.5.0",
|
"idna 0.5.0",
|
||||||
"imagesize",
|
"imagesize",
|
||||||
|
@ -1013,6 +1014,7 @@ dependencies = [
|
||||||
"pem",
|
"pem",
|
||||||
"privdrop",
|
"privdrop",
|
||||||
"proxy-header",
|
"proxy-header",
|
||||||
|
"pwhash",
|
||||||
"rcgen 0.12.1",
|
"rcgen 0.12.1",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.12.2",
|
"reqwest 0.12.2",
|
||||||
|
@ -1038,6 +1040,7 @@ dependencies = [
|
||||||
"utils",
|
"utils",
|
||||||
"whatlang",
|
"whatlang",
|
||||||
"x509-parser 0.16.0",
|
"x509-parser 0.16.0",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2426,6 +2429,17 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
|
@ -4748,7 +4762,7 @@ version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
|
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hostname",
|
"hostname 0.3.1",
|
||||||
"quick-error",
|
"quick-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -6923,6 +6937,16 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-targets 0.52.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|
|
@ -54,7 +54,9 @@ decancer = "3.0.1"
|
||||||
unicode-security = "0.1.0"
|
unicode-security = "0.1.0"
|
||||||
infer = "0.15.0"
|
infer = "0.15.0"
|
||||||
bincode = "1.3.1"
|
bincode = "1.3.1"
|
||||||
|
hostname = "0.4.0"
|
||||||
|
zip = "0.6.6"
|
||||||
|
pwhash = "1.0.0"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
privdrop = "0.5.3"
|
privdrop = "0.5.3"
|
||||||
|
|
|
@ -61,6 +61,7 @@ pub struct JmapConfig {
|
||||||
pub oauth_expiry_refresh_token: u64,
|
pub oauth_expiry_refresh_token: u64,
|
||||||
pub oauth_expiry_refresh_token_renew: u64,
|
pub oauth_expiry_refresh_token_renew: u64,
|
||||||
pub oauth_max_auth_attempts: u32,
|
pub oauth_max_auth_attempts: u32,
|
||||||
|
pub fallback_admin: Option<(String, String)>,
|
||||||
|
|
||||||
pub spam_header: Option<(HeaderName<'static>, String)>,
|
pub spam_header: Option<(HeaderName<'static>, String)>,
|
||||||
|
|
||||||
|
@ -78,6 +79,59 @@ pub struct JmapConfig {
|
||||||
|
|
||||||
impl JmapConfig {
|
impl JmapConfig {
|
||||||
pub fn parse(config: &mut Config) -> Self {
|
pub fn parse(config: &mut Config) -> Self {
|
||||||
|
// Parse HTTP headers
|
||||||
|
let mut http_headers = config
|
||||||
|
.values("server.http.headers")
|
||||||
|
.map(|(_, v)| {
|
||||||
|
if let Some((k, v)) = v.split_once(':') {
|
||||||
|
Ok((
|
||||||
|
hyper::header::HeaderName::from_str(k.trim()).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Invalid header found in property \"server.http.headers\": {}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
hyper::header::HeaderValue::from_str(v.trim()).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Invalid header found in property \"server.http.headers\": {}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Invalid header found in property \"server.http.headers\": {}",
|
||||||
|
v
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, String>>()
|
||||||
|
.map_err(|e| config.new_parse_error("server.http.headers", e))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Add permissive CORS headers
|
||||||
|
if config
|
||||||
|
.property::<bool>("server.http.permissive-cors")
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
http_headers.push((
|
||||||
|
hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||||
|
hyper::header::HeaderValue::from_static("*"),
|
||||||
|
));
|
||||||
|
http_headers.push((
|
||||||
|
hyper::header::ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
|
hyper::header::HeaderValue::from_static(
|
||||||
|
"Authorization, Content-Type, Accept, X-Requested-With",
|
||||||
|
),
|
||||||
|
));
|
||||||
|
http_headers.push((
|
||||||
|
hyper::header::ACCESS_CONTROL_ALLOW_METHODS,
|
||||||
|
hyper::header::HeaderValue::from_static(
|
||||||
|
"POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS",
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let mut jmap = JmapConfig {
|
let mut jmap = JmapConfig {
|
||||||
default_language: Language::from_iso_639(
|
default_language: Language::from_iso_639(
|
||||||
config
|
config
|
||||||
|
@ -210,45 +264,21 @@ impl JmapConfig {
|
||||||
encrypt_append: config
|
encrypt_append: config
|
||||||
.property_or_default("storage.encryption.append", "false")
|
.property_or_default("storage.encryption.append", "false")
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
spam_header: config.value("spam.header.is-spam").and_then(|v| {
|
spam_header: config
|
||||||
v.split_once(':').map(|(k, v)| {
|
.property_or_default::<Option<String>>("spam.header.is-spam", "X-Spam-Status: Yes")
|
||||||
(
|
.unwrap_or_default()
|
||||||
mail_parser::HeaderName::parse(k.trim().to_string()).unwrap(),
|
.and_then(|v| {
|
||||||
v.trim().to_string(),
|
v.split_once(':').map(|(k, v)| {
|
||||||
)
|
(
|
||||||
})
|
mail_parser::HeaderName::parse(k.trim().to_string()).unwrap(),
|
||||||
}),
|
v.trim().to_string(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}),
|
||||||
http_use_forwarded: config
|
http_use_forwarded: config
|
||||||
.property("server.http.use-x-forwarded")
|
.property("server.http.use-x-forwarded")
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
http_headers: config
|
http_headers,
|
||||||
.values("server.http.headers")
|
|
||||||
.map(|(_, v)| {
|
|
||||||
if let Some((k, v)) = v.split_once(':') {
|
|
||||||
Ok((
|
|
||||||
hyper::header::HeaderName::from_str(k.trim()).map_err(|err| {
|
|
||||||
format!(
|
|
||||||
"Invalid header found in property \"server.http.headers\": {}",
|
|
||||||
err
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
hyper::header::HeaderValue::from_str(v.trim()).map_err(|err| {
|
|
||||||
format!(
|
|
||||||
"Invalid header found in property \"server.http.headers\": {}",
|
|
||||||
err
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"Invalid header found in property \"server.http.headers\": {}",
|
|
||||||
v
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, String>>()
|
|
||||||
.map_err(|e| config.new_parse_error("server.http.headers", e))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
push_attempt_interval: config
|
push_attempt_interval: config
|
||||||
.property_or_default("jmap.push.attempts.interval", "1m")
|
.property_or_default("jmap.push.attempts.interval", "1m")
|
||||||
.unwrap_or_else(|| Duration::from_secs(60)),
|
.unwrap_or_else(|| Duration::from_secs(60)),
|
||||||
|
@ -270,6 +300,13 @@ impl JmapConfig {
|
||||||
session_purge_frequency: config
|
session_purge_frequency: config
|
||||||
.property_or_default::<SimpleCron>("jmap.session.purge.frequency", "15 * *")
|
.property_or_default::<SimpleCron>("jmap.session.purge.frequency", "15 * *")
|
||||||
.unwrap_or_else(|| SimpleCron::parse_value("15 * *").unwrap()),
|
.unwrap_or_else(|| SimpleCron::parse_value("15 * *").unwrap()),
|
||||||
|
fallback_admin: config
|
||||||
|
.value("authentication.fallback-admin.user")
|
||||||
|
.and_then(|u| {
|
||||||
|
config
|
||||||
|
.value("authentication.fallback-admin.secret")
|
||||||
|
.map(|p| (u.to_string(), p.to_string()))
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add capabilities
|
// Add capabilities
|
||||||
|
|
|
@ -5,16 +5,15 @@ use directory::{Directories, Directory};
|
||||||
use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores};
|
use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores};
|
||||||
use utils::config::Config;
|
use utils::config::Config;
|
||||||
|
|
||||||
use crate::{expr::*, listener::tls::TlsManager, Core, Network};
|
use crate::{expr::*, listener::tls::TlsManager, manager::config::ConfigManager, Core, Network};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
imap::ImapConfig, jmap::settings::JmapConfig, manager::ConfigManager, scripts::Scripting,
|
imap::ImapConfig, jmap::settings::JmapConfig, scripts::Scripting, smtp::SmtpConfig,
|
||||||
smtp::SmtpConfig, storage::Storage,
|
storage::Storage,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod imap;
|
pub mod imap;
|
||||||
pub mod jmap;
|
pub mod jmap;
|
||||||
pub mod manager;
|
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod scripts;
|
pub mod scripts;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
|
@ -629,7 +629,14 @@ impl Default for SessionConfig {
|
||||||
subaddressing: AddressMapping::Enable,
|
subaddressing: AddressMapping::Enable,
|
||||||
},
|
},
|
||||||
data: Data {
|
data: Data {
|
||||||
|
#[cfg(not(feature = "test_mode"))]
|
||||||
script: IfBlock::empty("session.data.script"),
|
script: IfBlock::empty("session.data.script"),
|
||||||
|
#[cfg(feature = "test_mode")]
|
||||||
|
script: IfBlock::new::<()>(
|
||||||
|
"session.data.script",
|
||||||
|
[("is_empty(authenticated_as)", "'spam-filter'")],
|
||||||
|
"'track-replies'",
|
||||||
|
),
|
||||||
pipe_commands: Default::default(),
|
pipe_commands: Default::default(),
|
||||||
milters: Default::default(),
|
milters: Default::default(),
|
||||||
max_messages: IfBlock::new::<()>("session.data.limits.messages", [], "10"),
|
max_messages: IfBlock::new::<()>("session.data.limits.messages", [], "10"),
|
||||||
|
|
|
@ -4,7 +4,7 @@ use ahash::AHashMap;
|
||||||
use directory::Directory;
|
use directory::Directory;
|
||||||
use store::{write::purge::PurgeSchedule, BlobStore, FtsStore, LookupStore, Store};
|
use store::{write::purge::PurgeSchedule, BlobStore, FtsStore, LookupStore, Store};
|
||||||
|
|
||||||
use super::manager::ConfigManager;
|
use crate::manager::config::ConfigManager;
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct Storage {
|
pub struct Storage {
|
||||||
|
|
|
@ -13,7 +13,7 @@ use config::{
|
||||||
storage::Storage,
|
storage::Storage,
|
||||||
tracers::{OtelTracer, Tracer, Tracers},
|
tracers::{OtelTracer, Tracer, Tracers},
|
||||||
};
|
};
|
||||||
use directory::{Directory, Principal, QueryBy};
|
use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy};
|
||||||
use expr::if_block::IfBlock;
|
use expr::if_block::IfBlock;
|
||||||
use listener::{blocked::BlockedIps, tls::TlsManager};
|
use listener::{blocked::BlockedIps, tls::TlsManager};
|
||||||
use mail_send::Credentials;
|
use mail_send::Credentials;
|
||||||
|
@ -26,15 +26,17 @@ use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION
|
||||||
use sieve::Sieve;
|
use sieve::Sieve;
|
||||||
use store::LookupStore;
|
use store::LookupStore;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tracing::{level_filters::LevelFilter, Level};
|
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
|
use tracing_subscriber::{
|
||||||
|
layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry,
|
||||||
|
};
|
||||||
use utils::{config::Config, BlobHash};
|
use utils::{config::Config, BlobHash};
|
||||||
|
|
||||||
pub mod addresses;
|
pub mod addresses;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod expr;
|
pub mod expr;
|
||||||
pub mod listener;
|
pub mod listener;
|
||||||
|
pub mod manager;
|
||||||
pub mod scripts;
|
pub mod scripts;
|
||||||
|
|
||||||
pub static USER_AGENT: &str = concat!("StalwartMail/", env!("CARGO_PKG_VERSION"),);
|
pub static USER_AGENT: &str = concat!("StalwartMail/", env!("CARGO_PKG_VERSION"),);
|
||||||
|
@ -205,11 +207,29 @@ impl Core {
|
||||||
remote_ip: IpAddr,
|
remote_ip: IpAddr,
|
||||||
return_member_of: bool,
|
return_member_of: bool,
|
||||||
) -> directory::Result<AuthResult<Principal<u32>>> {
|
) -> directory::Result<AuthResult<Principal<u32>>> {
|
||||||
if let Some(principal) = directory
|
// First try to authenticate the user against the default directory
|
||||||
|
let result = match directory
|
||||||
.query(QueryBy::Credentials(credentials), return_member_of)
|
.query(QueryBy::Credentials(credentials), return_member_of)
|
||||||
.await?
|
.await
|
||||||
{
|
{
|
||||||
Ok(AuthResult::Success(principal))
|
Ok(Some(principal)) => return Ok(AuthResult::Success(principal)),
|
||||||
|
Ok(None) => Ok(()),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Then check if the credentials match the fallback admin
|
||||||
|
if let (Some((fallback_admin, fallback_pass)), Credentials::Plain { username, secret }) =
|
||||||
|
(&self.jmap.fallback_admin, credentials)
|
||||||
|
{
|
||||||
|
if username == fallback_admin && verify_secret_hash(fallback_pass, secret).await {
|
||||||
|
return Ok(AuthResult::Success(Principal::fallback_admin(
|
||||||
|
fallback_pass,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
Err(err)
|
||||||
} else if self.has_fail2ban() {
|
} else if self.has_fail2ban() {
|
||||||
let login = match credentials {
|
let login = match credentials {
|
||||||
Credentials::Plain { username, .. }
|
Credentials::Plain { username, .. }
|
||||||
|
@ -237,60 +257,42 @@ impl Core {
|
||||||
|
|
||||||
impl Tracers {
|
impl Tracers {
|
||||||
pub fn enable(self, config: &mut Config) -> Option<Vec<WorkerGuard>> {
|
pub fn enable(self, config: &mut Config) -> Option<Vec<WorkerGuard>> {
|
||||||
let mut layers = Vec::new();
|
let mut layers: Option<Box<dyn Layer<Registry> + Sync + Send>> = None;
|
||||||
let mut level = Level::TRACE;
|
|
||||||
|
|
||||||
for tracer in &self.tracers {
|
|
||||||
let tracer_level = *match tracer {
|
|
||||||
Tracer::Stdout { level, .. }
|
|
||||||
| Tracer::Log { level, .. }
|
|
||||||
| Tracer::Journal { level }
|
|
||||||
| Tracer::Otel { level, .. } => level,
|
|
||||||
};
|
|
||||||
|
|
||||||
if tracer_level < level {
|
|
||||||
level = tracer_level;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut guards = Vec::new();
|
let mut guards = Vec::new();
|
||||||
match EnvFilter::builder().parse(format!(
|
|
||||||
"smtp={level},imap={level},jmap={level},store={level},common={level},utils={level},directory={level}"
|
|
||||||
)) {
|
|
||||||
Ok(layer) => {
|
|
||||||
layers.push(layer.boxed());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
config.new_build_error("tracer", format!("Failed to set env filter: {err}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for tracer in self.tracers {
|
for tracer in self.tracers {
|
||||||
match tracer {
|
let (Tracer::Stdout { level, .. }
|
||||||
Tracer::Stdout { level, ansi } => {
|
| Tracer::Log { level, .. }
|
||||||
layers.push(
|
| Tracer::Journal { level }
|
||||||
tracing_subscriber::fmt::layer()
|
| Tracer::Otel { level, .. }) = tracer;
|
||||||
.with_ansi(ansi)
|
|
||||||
.with_filter(LevelFilter::from_level(level))
|
let filter = match EnvFilter::builder().parse(format!(
|
||||||
.boxed(),
|
"smtp={level},imap={level},jmap={level},store={level},common={level},utils={level},directory={level}"
|
||||||
);
|
)) {
|
||||||
|
Ok(filter) => {
|
||||||
|
filter
|
||||||
}
|
}
|
||||||
Tracer::Log {
|
Err(err) => {
|
||||||
level,
|
config.new_build_error("tracer", format!("Failed to set env filter: {err}"));
|
||||||
appender,
|
continue;
|
||||||
ansi,
|
}
|
||||||
} => {
|
};
|
||||||
|
|
||||||
|
let layer = match tracer {
|
||||||
|
Tracer::Stdout { ansi, .. } => tracing_subscriber::fmt::layer()
|
||||||
|
.with_ansi(ansi)
|
||||||
|
.with_filter(filter)
|
||||||
|
.boxed(),
|
||||||
|
Tracer::Log { appender, ansi, .. } => {
|
||||||
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
||||||
guards.push(guard);
|
guards.push(guard);
|
||||||
layers.push(
|
tracing_subscriber::fmt::layer()
|
||||||
tracing_subscriber::fmt::layer()
|
.with_writer(non_blocking)
|
||||||
.with_writer(non_blocking)
|
.with_ansi(ansi)
|
||||||
.with_ansi(ansi)
|
.with_filter(filter)
|
||||||
.with_filter(LevelFilter::from_level(level))
|
.boxed()
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Tracer::Otel { level, tracer } => {
|
Tracer::Otel { tracer, .. } => {
|
||||||
let tracer = match tracer {
|
let tracer = match tracer {
|
||||||
OtelTracer::Gprc(exporter) => opentelemetry_otlp::new_pipeline()
|
OtelTracer::Gprc(exporter) => opentelemetry_otlp::new_pipeline()
|
||||||
.tracing()
|
.tracing()
|
||||||
|
@ -313,36 +315,30 @@ impl Tracers {
|
||||||
.install_batch(opentelemetry_sdk::runtime::Tokio);
|
.install_batch(opentelemetry_sdk::runtime::Tokio);
|
||||||
|
|
||||||
match tracer {
|
match tracer {
|
||||||
Ok(tracer) => {
|
Ok(tracer) => tracing_opentelemetry::layer()
|
||||||
layers.push(
|
.with_tracer(tracer)
|
||||||
tracing_opentelemetry::layer()
|
.with_filter(filter)
|
||||||
.with_tracer(tracer)
|
.boxed(),
|
||||||
.with_filter(LevelFilter::from_level(level))
|
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
config.new_build_error(
|
config.new_build_error(
|
||||||
"tracer",
|
"tracer",
|
||||||
format!("Failed to start OpenTelemetry: {err}"),
|
format!("Failed to start OpenTelemetry: {err}"),
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tracer::Journal { level } => {
|
Tracer::Journal { .. } => {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
match tracing_journald::layer() {
|
match tracing_journald::layer() {
|
||||||
Ok(layer) => {
|
Ok(layer) => layer.with_filter(filter).boxed(),
|
||||||
layers.push(
|
|
||||||
layer.with_filter(LevelFilter::from_level(level)).boxed(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
config.new_build_error(
|
config.new_build_error(
|
||||||
"tracer",
|
"tracer",
|
||||||
format!("Failed to start Journald: {err}"),
|
format!("Failed to start Journald: {err}"),
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,21 +349,23 @@ impl Tracers {
|
||||||
"tracer",
|
"tracer",
|
||||||
"Journald is only available on Unix systems.",
|
"Journald is only available on Unix systems.",
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
layers = Some(match layers {
|
||||||
|
Some(layers) => layers.and_then(layer).boxed(),
|
||||||
|
None => layer,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if layers.len() > 1 {
|
match tracing_subscriber::registry().with(layers?).try_init() {
|
||||||
match tracing_subscriber::registry().with(layers).try_init() {
|
Ok(_) => Some(guards),
|
||||||
Ok(_) => Some(guards),
|
Err(err) => {
|
||||||
Err(err) => {
|
config.new_build_error("tracer", format!("Failed to start tracing: {err}"));
|
||||||
config.new_build_error("tracer", format!("Failed to start tracing: {err}"));
|
None
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
373
crates/common/src/manager/boot.rs
Normal file
373
crates/common/src/manager/boot.rs
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use arc_swap::ArcSwap;
|
||||||
|
use pwhash::sha512_crypt;
|
||||||
|
use store::{
|
||||||
|
rand::{distributions::Alphanumeric, thread_rng, Rng},
|
||||||
|
Stores,
|
||||||
|
};
|
||||||
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
|
use utils::{
|
||||||
|
config::{Config, ConfigKey},
|
||||||
|
failed, UnwrapFailure,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{server::Servers, tracers::Tracers},
|
||||||
|
manager::SPAMFILTER_URL,
|
||||||
|
Core, SharedCore,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
config::{ConfigManager, Patterns},
|
||||||
|
download_resource, WEBADMIN_KEY, WEBADMIN_URL,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct BootManager {
|
||||||
|
pub config: Config,
|
||||||
|
pub core: SharedCore,
|
||||||
|
pub servers: Servers,
|
||||||
|
pub guards: Option<Vec<WorkerGuard>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BootManager {
|
||||||
|
pub async fn init() -> Self {
|
||||||
|
let mut config_path = std::env::var("CONFIG_PATH").ok();
|
||||||
|
|
||||||
|
if config_path.is_none() {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
|
||||||
|
if let Some(arg) = args
|
||||||
|
.next()
|
||||||
|
.and_then(|arg| arg.strip_prefix("--").map(|arg| arg.to_string()))
|
||||||
|
{
|
||||||
|
let (key, value) = if let Some((key, value)) = arg.split_once('=') {
|
||||||
|
(key.to_string(), value.trim().to_string())
|
||||||
|
} else if let Some(value) = args.next() {
|
||||||
|
(arg, value)
|
||||||
|
} else {
|
||||||
|
failed(&format!("Invalid command line argument: {arg}"));
|
||||||
|
};
|
||||||
|
|
||||||
|
match key.as_str() {
|
||||||
|
"config" => {
|
||||||
|
config_path = Some(value);
|
||||||
|
}
|
||||||
|
"init" => {
|
||||||
|
quickstart(value);
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
failed(&format!("Invalid command line argument: {key}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read main configuration file
|
||||||
|
let cfg_local_path =
|
||||||
|
PathBuf::from(config_path.failed("Missing parameter --config=<path-to-config>."));
|
||||||
|
let mut config = Config::default();
|
||||||
|
match std::fs::read_to_string(&cfg_local_path) {
|
||||||
|
Ok(value) => {
|
||||||
|
config.parse(&value).failed("Invalid configuration file");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
config.new_build_error("*", format!("Could not read configuration file: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cfg_local = config.keys.clone();
|
||||||
|
|
||||||
|
// Resolve macros
|
||||||
|
config.resolve_macros().await;
|
||||||
|
|
||||||
|
// Parser servers
|
||||||
|
let mut servers = Servers::parse(&mut config);
|
||||||
|
|
||||||
|
// Bind ports and drop privileges
|
||||||
|
servers.bind_and_drop_priv(&mut config);
|
||||||
|
|
||||||
|
// Load stores
|
||||||
|
let mut stores = Stores::parse(&mut config).await;
|
||||||
|
|
||||||
|
// Build manager
|
||||||
|
let manager = ConfigManager {
|
||||||
|
cfg_local: ArcSwap::from_pointee(cfg_local),
|
||||||
|
cfg_local_path,
|
||||||
|
cfg_local_patterns: Patterns::parse(&mut config).into(),
|
||||||
|
cfg_store: config
|
||||||
|
.value("storage.data")
|
||||||
|
.and_then(|id| stores.stores.get(id))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extend configuration with settings stored in the db
|
||||||
|
if !manager.cfg_store.is_none() {
|
||||||
|
manager
|
||||||
|
.extend_config(&mut config, "")
|
||||||
|
.await
|
||||||
|
.failed("Failed to read configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable tracing
|
||||||
|
let guards = Tracers::parse(&mut config).enable(&mut config);
|
||||||
|
|
||||||
|
// Add hostname lookup if missing
|
||||||
|
let mut insert_keys = Vec::new();
|
||||||
|
if config
|
||||||
|
.value("lookup.default.hostname")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
insert_keys.push(ConfigKey::from((
|
||||||
|
"lookup.default.hostname",
|
||||||
|
hostname::get()
|
||||||
|
.map(|v| v.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|_| "localhost".to_string()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate an OAuth key if missing
|
||||||
|
if config
|
||||||
|
.value("oauth.key")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
insert_keys.push(ConfigKey::from((
|
||||||
|
"oauth.key",
|
||||||
|
thread_rng()
|
||||||
|
.sample_iter(Alphanumeric)
|
||||||
|
.take(64)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download SPAM filters if missing
|
||||||
|
if config
|
||||||
|
.value("version.spam-filter")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
match manager.fetch_external_config(SPAMFILTER_URL).await {
|
||||||
|
Ok(external_config) => {
|
||||||
|
tracing::info!(
|
||||||
|
context = "config",
|
||||||
|
event = "import",
|
||||||
|
url = SPAMFILTER_URL,
|
||||||
|
version = external_config.version,
|
||||||
|
"Imported spam filter rules"
|
||||||
|
);
|
||||||
|
insert_keys.extend(external_config.keys);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
config.new_build_error("*", format!("Failed to fetch spam filter: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default settings
|
||||||
|
for key in [
|
||||||
|
("queue.quota.size.messages", "100000"),
|
||||||
|
("queue.quota.size.size", "10737418240"),
|
||||||
|
("queue.quota.size.enable", "true"),
|
||||||
|
("queue.throttle.rcpt.key", "rcpt_domain"),
|
||||||
|
("queue.throttle.rcpt.concurrency", "5"),
|
||||||
|
("queue.throttle.rcpt.enable", "true"),
|
||||||
|
("session.throttle.ip.key", "remote_ip"),
|
||||||
|
("session.throttle.ip.concurrency", "5"),
|
||||||
|
("session.throttle.ip.enable", "true"),
|
||||||
|
("session.throttle.sender.key.0", "sender_domain"),
|
||||||
|
("session.throttle.sender.key.1", "rcpt"),
|
||||||
|
("session.throttle.sender.rate", "25/1h"),
|
||||||
|
("session.throttle.sender.enable", "true"),
|
||||||
|
("report.analysis.addresses", "postmaster@*"),
|
||||||
|
] {
|
||||||
|
insert_keys.push(ConfigKey::from(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download webadmin if missing
|
||||||
|
if let Some(blob_store) = config
|
||||||
|
.value("storage.blob")
|
||||||
|
.and_then(|id| stores.blob_stores.get(id))
|
||||||
|
{
|
||||||
|
match blob_store.get_blob(WEBADMIN_KEY, 0..usize::MAX).await {
|
||||||
|
Ok(Some(_)) => (),
|
||||||
|
Ok(None) => match download_resource(WEBADMIN_URL).await {
|
||||||
|
Ok(bytes) => match blob_store.put_blob(WEBADMIN_KEY, &bytes).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!(
|
||||||
|
context = "webadmin",
|
||||||
|
event = "download",
|
||||||
|
url = WEBADMIN_URL,
|
||||||
|
"Downloaded webadmin bundle"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
config.new_build_error(
|
||||||
|
"*",
|
||||||
|
format!("Failed to store webadmin blob: {err}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
config.new_build_error("*", format!("Failed to download webadmin: {err}"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
config.new_build_error("*", format!("Failed to access webadmin blob: {err}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add missing settings
|
||||||
|
if !insert_keys.is_empty() {
|
||||||
|
for item in &insert_keys {
|
||||||
|
config.keys.insert(item.key.clone(), item.value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = manager.set(insert_keys).await {
|
||||||
|
config.new_build_error("*", format!("Failed to update configuration: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse lookup stores
|
||||||
|
stores.parse_lookups(&mut config).await;
|
||||||
|
|
||||||
|
// Parse settings and build shared core
|
||||||
|
let core = Core::parse(&mut config, stores, manager)
|
||||||
|
.await
|
||||||
|
.into_shared();
|
||||||
|
|
||||||
|
// Parse TCP acceptors
|
||||||
|
servers.parse_tcp_acceptors(&mut config, core.clone());
|
||||||
|
|
||||||
|
BootManager {
|
||||||
|
core,
|
||||||
|
guards,
|
||||||
|
config,
|
||||||
|
servers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quickstart(path: impl Into<PathBuf>) {
|
||||||
|
let path = path.into();
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
std::fs::create_dir_all(&path).failed("Failed to create directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in &["etc", "data", "logs"] {
|
||||||
|
std::fs::create_dir(path.join(dir)).failed(&format!("Failed to create {dir} directory"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let admin_pass = thread_rng()
|
||||||
|
.sample_iter(Alphanumeric)
|
||||||
|
.take(10)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
path.join("etc").join("config.toml"),
|
||||||
|
QUICKSTART_CONFIG
|
||||||
|
.replace("_P_", &path.to_string_lossy())
|
||||||
|
.replace("_S_", &sha512_crypt::hash(&admin_pass).unwrap()),
|
||||||
|
)
|
||||||
|
.failed("Failed to write configuration file");
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"✅ Configuration file written to {}/etc/config.toml",
|
||||||
|
path.to_string_lossy()
|
||||||
|
);
|
||||||
|
eprintln!("🔑 Your administrator account is 'admin' with password '{admin_pass}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUICKSTART_CONFIG: &str = r#"[server.listener.smtp]
|
||||||
|
bind = "[::]:25"
|
||||||
|
protocol = "smtp"
|
||||||
|
|
||||||
|
[server.listener.submission]
|
||||||
|
bind = "[::]:587"
|
||||||
|
protocol = "smtp"
|
||||||
|
|
||||||
|
[server.listener.submissions]
|
||||||
|
bind = "[::]:465"
|
||||||
|
protocol = "smtp"
|
||||||
|
tls.implicit = true
|
||||||
|
|
||||||
|
[server.listener.imap]
|
||||||
|
bind = "[::]:143"
|
||||||
|
protocol = "imap"
|
||||||
|
|
||||||
|
[server.listener.imaptls]
|
||||||
|
bind = "[::]:993"
|
||||||
|
protocol = "imap"
|
||||||
|
tls.implicit = true
|
||||||
|
|
||||||
|
[server.listener.sieve]
|
||||||
|
bind = "[::]:4190"
|
||||||
|
protocol = "managesieve"
|
||||||
|
|
||||||
|
[server.listener.https]
|
||||||
|
protocol = "http"
|
||||||
|
bind = "[::]:443"
|
||||||
|
tls.implicit = true
|
||||||
|
|
||||||
|
[server.listener.http]
|
||||||
|
protocol = "http"
|
||||||
|
bind = "[::]:8080"
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
data = "rocksdb"
|
||||||
|
fts = "rocksdb"
|
||||||
|
blob = "rocksdb"
|
||||||
|
lookup = "rocksdb"
|
||||||
|
directory = "internal"
|
||||||
|
|
||||||
|
[store.rocksdb]
|
||||||
|
type = "rocksdb"
|
||||||
|
path = "_P_/data"
|
||||||
|
compression = "lz4"
|
||||||
|
|
||||||
|
[directory.internal]
|
||||||
|
type = "internal"
|
||||||
|
store = "rocksdb"
|
||||||
|
|
||||||
|
[tracer.log]
|
||||||
|
type = "log"
|
||||||
|
level = "info"
|
||||||
|
path = "_P_/logs"
|
||||||
|
prefix = "stalwart.log"
|
||||||
|
rotate = "daily"
|
||||||
|
ansi = false
|
||||||
|
enable = true
|
||||||
|
|
||||||
|
[authentication.fallback-admin]
|
||||||
|
user = "admin"
|
||||||
|
secret = "_S_"
|
||||||
|
"#;
|
|
@ -1,37 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{btree_map::Entry, BTreeMap},
|
collections::{btree_map::Entry, BTreeMap},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ahash::AHashSet;
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use parking_lot::RwLock;
|
|
||||||
use store::{
|
use store::{
|
||||||
write::{BatchBuilder, ValueClass},
|
write::{BatchBuilder, ValueClass},
|
||||||
Deserialize, IterateParams, Store, Stores, ValueKey,
|
Deserialize, IterateParams, Store, ValueKey,
|
||||||
};
|
};
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
|
||||||
use utils::{
|
use utils::{
|
||||||
config::{ipmask::IpAddrOrMask, utils::ParseValue, Config, ConfigKey},
|
config::{Config, ConfigKey},
|
||||||
failed,
|
|
||||||
glob::GlobPattern,
|
glob::GlobPattern,
|
||||||
UnwrapFailure,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{listener::blocked::BLOCKED_IP_KEY, Core, SharedCore};
|
use super::download_resource;
|
||||||
|
|
||||||
use super::{
|
|
||||||
server::{tls::parse_certificates, Servers},
|
|
||||||
tracers::Tracers,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ConfigManager {
|
pub struct ConfigManager {
|
||||||
cfg_local: ArcSwap<BTreeMap<String, String>>,
|
pub cfg_local: ArcSwap<BTreeMap<String, String>>,
|
||||||
cfg_local_path: PathBuf,
|
pub cfg_local_path: PathBuf,
|
||||||
cfg_local_patterns: Arc<Patterns>,
|
pub cfg_local_patterns: Arc<Patterns>,
|
||||||
cfg_store: Store,
|
pub cfg_store: Store,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -44,11 +57,6 @@ enum Pattern {
|
||||||
Exclude(MatchType),
|
Exclude(MatchType),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ReloadResult {
|
|
||||||
pub config: Config,
|
|
||||||
pub new_core: Option<Core>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MatchType {
|
enum MatchType {
|
||||||
Equal(String),
|
Equal(String),
|
||||||
StartsWith(String),
|
StartsWith(String),
|
||||||
|
@ -57,102 +65,10 @@ enum MatchType {
|
||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BootManager {
|
pub(crate) struct ExternalConfig {
|
||||||
pub config: Config,
|
pub id: String,
|
||||||
pub core: SharedCore,
|
pub version: String,
|
||||||
pub servers: Servers,
|
pub keys: Vec<ConfigKey>,
|
||||||
pub guards: Option<Vec<WorkerGuard>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BootManager {
|
|
||||||
pub async fn init() -> Self {
|
|
||||||
let mut config_path = std::env::var("CONFIG_PATH").ok();
|
|
||||||
let mut found_param = false;
|
|
||||||
|
|
||||||
if config_path.is_none() {
|
|
||||||
for arg in std::env::args().skip(1) {
|
|
||||||
if let Some((key, value)) = arg.split_once('=') {
|
|
||||||
if key.starts_with("--config") {
|
|
||||||
config_path = value.trim().to_string().into();
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
failed(&format!("Invalid command line argument: {key}"));
|
|
||||||
}
|
|
||||||
} else if found_param {
|
|
||||||
config_path = arg.into();
|
|
||||||
break;
|
|
||||||
} else if arg.starts_with("--config") {
|
|
||||||
found_param = true;
|
|
||||||
} else {
|
|
||||||
failed(&format!("Invalid command line argument: {arg}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read main configuration file
|
|
||||||
let cfg_local_path =
|
|
||||||
PathBuf::from(config_path.failed("Missing parameter --config=<path-to-config>."));
|
|
||||||
let mut config = Config::default();
|
|
||||||
match std::fs::read_to_string(&cfg_local_path) {
|
|
||||||
Ok(value) => {
|
|
||||||
config.parse(&value).failed("Invalid configuration file");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
config.new_build_error("*", format!("Could not read configuration file: {err}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let cfg_local = config.keys.clone();
|
|
||||||
|
|
||||||
// Resolve macros
|
|
||||||
config.resolve_macros().await;
|
|
||||||
|
|
||||||
// Parser servers
|
|
||||||
let mut servers = Servers::parse(&mut config);
|
|
||||||
|
|
||||||
// Bind ports and drop privileges
|
|
||||||
servers.bind_and_drop_priv(&mut config);
|
|
||||||
|
|
||||||
// Load stores
|
|
||||||
let mut stores = Stores::parse(&mut config).await;
|
|
||||||
|
|
||||||
// Build manager
|
|
||||||
let manager = ConfigManager {
|
|
||||||
cfg_local: ArcSwap::from_pointee(cfg_local),
|
|
||||||
cfg_local_path,
|
|
||||||
cfg_local_patterns: Patterns::parse(&mut config).into(),
|
|
||||||
cfg_store: config
|
|
||||||
.value("storage.data")
|
|
||||||
.and_then(|id| stores.stores.get(id))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extend configuration with settings stored in the db
|
|
||||||
if !manager.cfg_store.is_none() {
|
|
||||||
manager
|
|
||||||
.extend_config(&mut config, "")
|
|
||||||
.await
|
|
||||||
.failed("Failed to read configuration");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse lookup stores
|
|
||||||
stores.parse_lookups(&mut config).await;
|
|
||||||
|
|
||||||
// Parse settings and build shared core
|
|
||||||
let core = Core::parse(&mut config, stores, manager)
|
|
||||||
.await
|
|
||||||
.into_shared();
|
|
||||||
|
|
||||||
// Parse TCP acceptors
|
|
||||||
servers.parse_tcp_acceptors(&mut config, core.clone());
|
|
||||||
|
|
||||||
BootManager {
|
|
||||||
core,
|
|
||||||
guards: Tracers::parse(&mut config).enable(&mut config),
|
|
||||||
config,
|
|
||||||
servers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigManager {
|
impl ConfigManager {
|
||||||
|
@ -167,7 +83,11 @@ impl ConfigManager {
|
||||||
.map(|_| config)
|
.map(|_| config)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn extend_config(&self, config: &mut Config, prefix: &str) -> store::Result<()> {
|
pub(crate) async fn extend_config(
|
||||||
|
&self,
|
||||||
|
config: &mut Config,
|
||||||
|
prefix: &str,
|
||||||
|
) -> store::Result<()> {
|
||||||
for (key, value) in self.db_list(prefix, false).await? {
|
for (key, value) in self.db_list(prefix, false).await? {
|
||||||
config.keys.entry(key).or_insert(value);
|
config.keys.entry(key).or_insert(value);
|
||||||
}
|
}
|
||||||
|
@ -399,124 +319,73 @@ impl ConfigManager {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Core {
|
pub async fn update_external_config(&self, url: &str) -> store::Result<Option<String>> {
|
||||||
pub async fn reload_blocked_ips(&self) -> store::Result<ReloadResult> {
|
let external = self
|
||||||
let mut ip_addresses = AHashSet::new();
|
.fetch_external_config(url)
|
||||||
let mut config = self.storage.config.build_config(BLOCKED_IP_KEY).await?;
|
.await
|
||||||
|
.map_err(store::Error::InternalError)?;
|
||||||
|
|
||||||
for ip in config
|
if self
|
||||||
.set_values(BLOCKED_IP_KEY)
|
.get(&external.id)
|
||||||
.map(IpAddrOrMask::parse_value)
|
.await?
|
||||||
.collect::<Vec<_>>()
|
.map_or(true, |v| v != external.version)
|
||||||
{
|
{
|
||||||
match ip {
|
self.set(external.keys).await?;
|
||||||
Ok(IpAddrOrMask::Ip(ip)) => {
|
Ok(Some(external.version))
|
||||||
ip_addresses.insert(ip);
|
|
||||||
}
|
|
||||||
Ok(IpAddrOrMask::Mask(_)) => {}
|
|
||||||
Err(err) => {
|
|
||||||
config.new_parse_error(BLOCKED_IP_KEY, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*self.network.blocked_ips.ip_addresses.write() = ip_addresses;
|
|
||||||
|
|
||||||
Ok(config.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reload_certificates(&self) -> store::Result<ReloadResult> {
|
|
||||||
let mut config = self.storage.config.build_config("certificate").await?;
|
|
||||||
let mut certificates = self.tls.certificates.load().as_ref().clone();
|
|
||||||
|
|
||||||
parse_certificates(&mut config, &mut certificates, &mut Default::default());
|
|
||||||
|
|
||||||
self.tls.certificates.store(certificates.into());
|
|
||||||
|
|
||||||
Ok(config.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reload_lookups(&self) -> store::Result<ReloadResult> {
|
|
||||||
let mut config = self.storage.config.build_config("certificate").await?;
|
|
||||||
let mut stores = Stores::default();
|
|
||||||
stores.parse_memory_stores(&mut config);
|
|
||||||
|
|
||||||
let mut core = self.clone();
|
|
||||||
for (id, store) in stores.lookup_stores {
|
|
||||||
core.storage.lookups.insert(id, store);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ReloadResult {
|
|
||||||
config,
|
|
||||||
new_core: core.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reload(&self) -> store::Result<ReloadResult> {
|
|
||||||
let mut config = self.storage.config.build_config("").await?;
|
|
||||||
|
|
||||||
// Parse tracers
|
|
||||||
Tracers::parse(&mut config);
|
|
||||||
|
|
||||||
// Load stores
|
|
||||||
let mut stores = Stores {
|
|
||||||
stores: self.storage.stores.clone(),
|
|
||||||
blob_stores: self.storage.blobs.clone(),
|
|
||||||
fts_stores: self.storage.ftss.clone(),
|
|
||||||
lookup_stores: self.storage.lookups.clone(),
|
|
||||||
purge_schedules: Default::default(),
|
|
||||||
};
|
|
||||||
stores.parse_stores(&mut config).await;
|
|
||||||
stores.parse_lookups(&mut config).await;
|
|
||||||
if !config.errors.is_empty() {
|
|
||||||
return Ok(config.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build manager
|
|
||||||
let manager = ConfigManager {
|
|
||||||
cfg_local: ArcSwap::from_pointee(self.storage.config.cfg_local.load().as_ref().clone()),
|
|
||||||
cfg_local_path: self.storage.config.cfg_local_path.clone(),
|
|
||||||
cfg_local_patterns: Patterns::parse(&mut config).into(),
|
|
||||||
cfg_store: config
|
|
||||||
.value("storage.data")
|
|
||||||
.and_then(|id| stores.stores.get(id))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse settings and build shared core
|
|
||||||
let mut core = Core::parse(&mut config, stores, manager).await;
|
|
||||||
if !config.errors.is_empty() {
|
|
||||||
return Ok(config.into());
|
|
||||||
}
|
|
||||||
// Transfer Sieve cache
|
|
||||||
core.sieve.bayes_cache = self.sieve.bayes_cache.clone();
|
|
||||||
core.sieve.remote_lists = RwLock::new(self.sieve.remote_lists.read().clone());
|
|
||||||
|
|
||||||
// Copy ACME certificates
|
|
||||||
let mut certificates = core.tls.certificates.load().as_ref().clone();
|
|
||||||
for (cert_id, cert) in self.tls.certificates.load().iter() {
|
|
||||||
certificates
|
|
||||||
.entry(cert_id.to_string())
|
|
||||||
.or_insert(cert.clone());
|
|
||||||
}
|
|
||||||
core.tls.certificates.store(certificates.into());
|
|
||||||
core.tls.self_signed_cert = self.tls.self_signed_cert.clone();
|
|
||||||
|
|
||||||
// Parser servers
|
|
||||||
let mut servers = Servers::parse(&mut config);
|
|
||||||
servers.parse_tcp_acceptors(&mut config, core.clone().into_shared());
|
|
||||||
|
|
||||||
Ok(if config.errors.is_empty() {
|
|
||||||
ReloadResult {
|
|
||||||
config,
|
|
||||||
new_core: core.into(),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
config.into()
|
tracing::debug!(
|
||||||
})
|
context = "config",
|
||||||
|
event = "update",
|
||||||
|
url = url,
|
||||||
|
version = external.version,
|
||||||
|
"Configuration version is up-to-date"
|
||||||
|
);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn fetch_external_config(&self, url: &str) -> Result<ExternalConfig, String> {
|
||||||
|
let config = String::from_utf8(download_resource(url).await?)
|
||||||
|
.map_err(|err| format!("Configuration file has invalid UTF-8: {err}"))?;
|
||||||
|
let config = Config::new(config)
|
||||||
|
.map_err(|err| format!("Failed to parse external configuration: {err}"))?;
|
||||||
|
|
||||||
|
// Import configuration
|
||||||
|
let mut external = ExternalConfig {
|
||||||
|
id: String::new(),
|
||||||
|
version: String::new(),
|
||||||
|
keys: Vec::new(),
|
||||||
|
};
|
||||||
|
for (key, value) in config.keys {
|
||||||
|
if key.starts_with("version.") {
|
||||||
|
external.id = key.clone();
|
||||||
|
external.version = value.clone();
|
||||||
|
external.keys.push(ConfigKey::from((key, value)));
|
||||||
|
} else if key.starts_with("queue.quota.")
|
||||||
|
|| key.starts_with("queue.throttle.")
|
||||||
|
|| key.starts_with("session.throttle.")
|
||||||
|
|| (key.starts_with("lookup.") && !key.starts_with("lookup.default."))
|
||||||
|
|| key.starts_with("sieve.trusted.scripts.")
|
||||||
|
{
|
||||||
|
external.keys.push(ConfigKey::from((key, value)));
|
||||||
|
} else {
|
||||||
|
tracing::debug!(
|
||||||
|
context = "config",
|
||||||
|
event = "import",
|
||||||
|
key = key,
|
||||||
|
value = value,
|
||||||
|
url = url,
|
||||||
|
"Ignoring key"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !external.version.is_empty() {
|
||||||
|
Ok(external)
|
||||||
|
} else {
|
||||||
|
Err("External configuration file does not contain a version key".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,20 +429,19 @@ impl Patterns {
|
||||||
if cfg_local_patterns.is_empty() {
|
if cfg_local_patterns.is_empty() {
|
||||||
cfg_local_patterns = vec![
|
cfg_local_patterns = vec![
|
||||||
Pattern::Include(MatchType::StartsWith("store.".to_string())),
|
Pattern::Include(MatchType::StartsWith("store.".to_string())),
|
||||||
Pattern::Include(MatchType::StartsWith("server.listener.".to_string())),
|
Pattern::Include(MatchType::StartsWith("directory.".to_string())),
|
||||||
Pattern::Include(MatchType::StartsWith("server.socket.".to_string())),
|
Pattern::Include(MatchType::StartsWith("tracer.".to_string())),
|
||||||
Pattern::Include(MatchType::StartsWith("server.tls.".to_string())),
|
Pattern::Include(MatchType::StartsWith("server.".to_string())),
|
||||||
|
Pattern::Include(MatchType::StartsWith(
|
||||||
|
"authentication.fallback-admin.".to_string(),
|
||||||
|
)),
|
||||||
Pattern::Include(MatchType::Equal("cluster.node-id".to_string())),
|
Pattern::Include(MatchType::Equal("cluster.node-id".to_string())),
|
||||||
Pattern::Include(MatchType::Equal("storage.data".to_string())),
|
Pattern::Include(MatchType::Equal("storage.data".to_string())),
|
||||||
Pattern::Include(MatchType::Equal("storage.blob".to_string())),
|
Pattern::Include(MatchType::Equal("storage.blob".to_string())),
|
||||||
Pattern::Include(MatchType::Equal("storage.lookup".to_string())),
|
Pattern::Include(MatchType::Equal("storage.lookup".to_string())),
|
||||||
Pattern::Include(MatchType::Equal("storage.fts".to_string())),
|
Pattern::Include(MatchType::Equal("storage.fts".to_string())),
|
||||||
Pattern::Include(MatchType::Equal("server.run-as.user".to_string())),
|
Pattern::Include(MatchType::Equal("storage.directory".to_string())),
|
||||||
Pattern::Include(MatchType::Equal("server.run-as.group".to_string())),
|
Pattern::Include(MatchType::Equal("lookup.default.hostname".to_string())),
|
||||||
Pattern::Exclude(MatchType::Matches(GlobPattern::compile(
|
|
||||||
"store.*.query.*",
|
|
||||||
false,
|
|
||||||
))),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -626,12 +494,3 @@ impl Clone for ConfigManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Config> for ReloadResult {
|
|
||||||
fn from(config: Config) -> Self {
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
new_core: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
56
crates/common/src/manager/mod.rs
Normal file
56
crates/common/src/manager/mod.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::USER_AGENT;
|
||||||
|
|
||||||
|
pub mod boot;
|
||||||
|
pub mod config;
|
||||||
|
pub mod reload;
|
||||||
|
pub mod webadmin;
|
||||||
|
|
||||||
|
pub const SPAMFILTER_URL: &str = "https://get.stalw.art/resources/config/spamfilter.toml";
|
||||||
|
pub const WEBADMIN_URL: &str = "file://get.stalw.art/resources/config/webadmin.toml";
|
||||||
|
pub const WEBADMIN_KEY: &[u8] = "STALWART_WEBADMIN".as_bytes();
|
||||||
|
|
||||||
|
async fn download_resource(url: &str) -> Result<Vec<u8>, String> {
|
||||||
|
let todo = "remove";
|
||||||
|
if url == WEBADMIN_URL {
|
||||||
|
return Ok(tokio::fs::read("/tmp/dist.zip").await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(60))
|
||||||
|
.user_agent(USER_AGENT)
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Failed to fetch {url}: {err}"))?
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Failed to fetch {url}: {err}"))
|
||||||
|
.map(|bytes| bytes.to_vec())
|
||||||
|
}
|
172
crates/common/src/manager/reload.rs
Normal file
172
crates/common/src/manager/reload.rs
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use ahash::AHashSet;
|
||||||
|
use arc_swap::ArcSwap;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use store::Stores;
|
||||||
|
use utils::config::{ipmask::IpAddrOrMask, utils::ParseValue, Config};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{
|
||||||
|
server::{tls::parse_certificates, Servers},
|
||||||
|
tracers::Tracers,
|
||||||
|
},
|
||||||
|
listener::blocked::BLOCKED_IP_KEY,
|
||||||
|
Core,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::config::{ConfigManager, Patterns};
|
||||||
|
|
||||||
|
pub struct ReloadResult {
|
||||||
|
pub config: Config,
|
||||||
|
pub new_core: Option<Core>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Core {
|
||||||
|
pub async fn reload_blocked_ips(&self) -> store::Result<ReloadResult> {
|
||||||
|
let mut ip_addresses = AHashSet::new();
|
||||||
|
let mut config = self.storage.config.build_config(BLOCKED_IP_KEY).await?;
|
||||||
|
|
||||||
|
for ip in config
|
||||||
|
.set_values(BLOCKED_IP_KEY)
|
||||||
|
.map(IpAddrOrMask::parse_value)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
match ip {
|
||||||
|
Ok(IpAddrOrMask::Ip(ip)) => {
|
||||||
|
ip_addresses.insert(ip);
|
||||||
|
}
|
||||||
|
Ok(IpAddrOrMask::Mask(_)) => {}
|
||||||
|
Err(err) => {
|
||||||
|
config.new_parse_error(BLOCKED_IP_KEY, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.network.blocked_ips.ip_addresses.write() = ip_addresses;
|
||||||
|
|
||||||
|
Ok(config.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reload_certificates(&self) -> store::Result<ReloadResult> {
|
||||||
|
let mut config = self.storage.config.build_config("certificate").await?;
|
||||||
|
let mut certificates = self.tls.certificates.load().as_ref().clone();
|
||||||
|
|
||||||
|
parse_certificates(&mut config, &mut certificates, &mut Default::default());
|
||||||
|
|
||||||
|
self.tls.certificates.store(certificates.into());
|
||||||
|
|
||||||
|
Ok(config.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reload_lookups(&self) -> store::Result<ReloadResult> {
|
||||||
|
let mut config = self.storage.config.build_config("certificate").await?;
|
||||||
|
let mut stores = Stores::default();
|
||||||
|
stores.parse_memory_stores(&mut config);
|
||||||
|
|
||||||
|
let mut core = self.clone();
|
||||||
|
for (id, store) in stores.lookup_stores {
|
||||||
|
core.storage.lookups.insert(id, store);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ReloadResult {
|
||||||
|
config,
|
||||||
|
new_core: core.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reload(&self) -> store::Result<ReloadResult> {
|
||||||
|
let mut config = self.storage.config.build_config("").await?;
|
||||||
|
|
||||||
|
// Parse tracers
|
||||||
|
Tracers::parse(&mut config);
|
||||||
|
|
||||||
|
// Load stores
|
||||||
|
let mut stores = Stores {
|
||||||
|
stores: self.storage.stores.clone(),
|
||||||
|
blob_stores: self.storage.blobs.clone(),
|
||||||
|
fts_stores: self.storage.ftss.clone(),
|
||||||
|
lookup_stores: self.storage.lookups.clone(),
|
||||||
|
purge_schedules: Default::default(),
|
||||||
|
};
|
||||||
|
stores.parse_stores(&mut config).await;
|
||||||
|
stores.parse_lookups(&mut config).await;
|
||||||
|
if !config.errors.is_empty() {
|
||||||
|
return Ok(config.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build manager
|
||||||
|
let manager = ConfigManager {
|
||||||
|
cfg_local: ArcSwap::from_pointee(self.storage.config.cfg_local.load().as_ref().clone()),
|
||||||
|
cfg_local_path: self.storage.config.cfg_local_path.clone(),
|
||||||
|
cfg_local_patterns: Patterns::parse(&mut config).into(),
|
||||||
|
cfg_store: config
|
||||||
|
.value("storage.data")
|
||||||
|
.and_then(|id| stores.stores.get(id))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse settings and build shared core
|
||||||
|
let mut core = Core::parse(&mut config, stores, manager).await;
|
||||||
|
if !config.errors.is_empty() {
|
||||||
|
return Ok(config.into());
|
||||||
|
}
|
||||||
|
// Transfer Sieve cache
|
||||||
|
core.sieve.bayes_cache = self.sieve.bayes_cache.clone();
|
||||||
|
core.sieve.remote_lists = RwLock::new(self.sieve.remote_lists.read().clone());
|
||||||
|
|
||||||
|
// Copy ACME certificates
|
||||||
|
let mut certificates = core.tls.certificates.load().as_ref().clone();
|
||||||
|
for (cert_id, cert) in self.tls.certificates.load().iter() {
|
||||||
|
certificates
|
||||||
|
.entry(cert_id.to_string())
|
||||||
|
.or_insert(cert.clone());
|
||||||
|
}
|
||||||
|
core.tls.certificates.store(certificates.into());
|
||||||
|
core.tls.self_signed_cert = self.tls.self_signed_cert.clone();
|
||||||
|
|
||||||
|
// Parser servers
|
||||||
|
let mut servers = Servers::parse(&mut config);
|
||||||
|
servers.parse_tcp_acceptors(&mut config, core.clone().into_shared());
|
||||||
|
|
||||||
|
Ok(if config.errors.is_empty() {
|
||||||
|
ReloadResult {
|
||||||
|
config,
|
||||||
|
new_core: core.into(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Config> for ReloadResult {
|
||||||
|
fn from(config: Config) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
new_core: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
175
crates/common/src/manager/webadmin.rs
Normal file
175
crates/common/src/manager/webadmin.rs
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||||
|
*
|
||||||
|
* This file is part of Stalwart Mail Server.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
* in the LICENSE file at the top-level directory of this distribution.
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* You can be released from the requirements of the AGPLv3 license by
|
||||||
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io::{self, Cursor, Read},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use ahash::AHashMap;
|
||||||
|
use arc_swap::ArcSwap;
|
||||||
|
use store::BlobStore;
|
||||||
|
|
||||||
|
use super::{download_resource, WEBADMIN_KEY, WEBADMIN_URL};
|
||||||
|
|
||||||
|
pub struct WebAdminManager {
|
||||||
|
bundle_path: TempDir,
|
||||||
|
routes: ArcSwap<AHashMap<String, Resource<PathBuf>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Resource<T> {
|
||||||
|
pub content_type: &'static str,
|
||||||
|
pub contents: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebAdminManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
bundle_path: TempDir::new(),
|
||||||
|
routes: ArcSwap::from_pointee(Default::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(&self, path: &str) -> io::Result<Resource<Vec<u8>>> {
|
||||||
|
let routes = self.routes.load();
|
||||||
|
if let Some(resource) = routes.get(path).or_else(|| routes.get("index.html")) {
|
||||||
|
tokio::fs::read(&resource.contents)
|
||||||
|
.await
|
||||||
|
.map(|contents| Resource {
|
||||||
|
content_type: resource.content_type,
|
||||||
|
contents,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(Resource::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unpack(&self, blob_store: &BlobStore) -> store::Result<()> {
|
||||||
|
// Delete any existing bundles
|
||||||
|
self.bundle_path.clean().await?;
|
||||||
|
|
||||||
|
// Obtain webadmin bundle
|
||||||
|
let bundle = blob_store
|
||||||
|
.get_blob(WEBADMIN_KEY, 0..usize::MAX)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| store::Error::InternalError("WebAdmin bundle not found".to_string()))?;
|
||||||
|
|
||||||
|
// Uncompress
|
||||||
|
let mut bundle = zip::ZipArchive::new(Cursor::new(bundle))
|
||||||
|
.map_err(|err| store::Error::InternalError(format!("Unzip error: {err}")))?;
|
||||||
|
let mut routes = AHashMap::new();
|
||||||
|
for i in 0..bundle.len() {
|
||||||
|
let (file_name, contents) = {
|
||||||
|
let mut file = bundle
|
||||||
|
.by_index(i)
|
||||||
|
.map_err(|err| store::Error::InternalError(format!("Unzip error: {err}")))?;
|
||||||
|
if file.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
file.read_to_end(&mut contents)?;
|
||||||
|
(file.name().to_string(), contents)
|
||||||
|
};
|
||||||
|
let path = self.bundle_path.path.join(format!("{i:02}"));
|
||||||
|
tokio::fs::write(&path, contents).await?;
|
||||||
|
|
||||||
|
let resource = Resource {
|
||||||
|
content_type: match file_name
|
||||||
|
.rsplit_once('.')
|
||||||
|
.map(|(_, ext)| ext)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
"html" => "text/html",
|
||||||
|
"css" => "text/css",
|
||||||
|
"js" => "application/javascript",
|
||||||
|
"json" => "application/json",
|
||||||
|
"png" => "image/png",
|
||||||
|
"svg" => "image/svg+xml",
|
||||||
|
"ico" => "image/x-icon",
|
||||||
|
_ => "application/octet-stream",
|
||||||
|
},
|
||||||
|
contents: path,
|
||||||
|
};
|
||||||
|
|
||||||
|
routes.insert(file_name, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update routes
|
||||||
|
self.routes.store(routes.into());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_and_unpack(&self, blob_store: &BlobStore) -> store::Result<()> {
|
||||||
|
let bytes = download_resource(WEBADMIN_URL).await.map_err(|err| {
|
||||||
|
store::Error::InternalError(format!("Failed to download webadmin: {err}"))
|
||||||
|
})?;
|
||||||
|
blob_store.put_blob(WEBADMIN_KEY, &bytes).await?;
|
||||||
|
self.unpack(blob_store).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resource<Vec<u8>> {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.content_type.is_empty() && self.contents.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TempDir {
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempDir {
|
||||||
|
pub fn new() -> TempDir {
|
||||||
|
TempDir {
|
||||||
|
path: std::env::temp_dir().join(std::str::from_utf8(WEBADMIN_KEY).unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clean(&self) -> io::Result<()> {
|
||||||
|
if tokio::fs::metadata(&self.path).await.is_ok() {
|
||||||
|
let _ = tokio::fs::remove_dir_all(&self.path).await;
|
||||||
|
}
|
||||||
|
tokio::fs::create_dir(&self.path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WebAdminManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TempDir {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.path);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,14 +22,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use jmap_proto::types::collection::Collection;
|
use jmap_proto::types::collection::Collection;
|
||||||
use pwhash::sha512_crypt;
|
|
||||||
use store::{
|
use store::{
|
||||||
rand::{distributions::Alphanumeric, thread_rng, Rng},
|
|
||||||
write::{
|
write::{
|
||||||
assert::HashedValue, key::DeserializeBigEndian, BatchBuilder, BitmapClass, DirectoryClass,
|
assert::HashedValue, key::DeserializeBigEndian, BatchBuilder, DirectoryClass, ValueClass,
|
||||||
ValueClass,
|
|
||||||
},
|
},
|
||||||
BitmapKey, Deserialize, IterateParams, Serialize, Store, ValueKey, U32_LEN,
|
Deserialize, IterateParams, Serialize, Store, ValueKey, U32_LEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{DirectoryError, ManagementError, Principal, QueryBy, Type};
|
use crate::{DirectoryError, ManagementError, Principal, QueryBy, Type};
|
||||||
|
@ -76,7 +73,6 @@ pub trait ManageDirectory: Sized {
|
||||||
async fn create_domain(&self, domain: &str) -> crate::Result<()>;
|
async fn create_domain(&self, domain: &str) -> crate::Result<()>;
|
||||||
async fn delete_domain(&self, domain: &str) -> crate::Result<()>;
|
async fn delete_domain(&self, domain: &str) -> crate::Result<()>;
|
||||||
async fn list_domains(&self, filter: Option<&str>) -> crate::Result<Vec<String>>;
|
async fn list_domains(&self, filter: Option<&str>) -> crate::Result<Vec<String>>;
|
||||||
async fn init(self) -> crate::Result<Self>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManageDirectory for Store {
|
impl ManageDirectory for Store {
|
||||||
|
@ -973,85 +969,6 @@ impl ManageDirectory for Store {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init(self) -> crate::Result<Self> {
|
|
||||||
// Create admin account if requested
|
|
||||||
if let (Ok(admin_user), Ok(admin_pass)) = (
|
|
||||||
std::env::var("SET_ADMIN_USER"),
|
|
||||||
std::env::var("SET_ADMIN_PASS"),
|
|
||||||
) {
|
|
||||||
if let Some(account_id) = self.get_account_id(&admin_user).await? {
|
|
||||||
self.update_account(
|
|
||||||
QueryBy::Id(account_id),
|
|
||||||
vec![PrincipalUpdate {
|
|
||||||
action: PrincipalAction::Set,
|
|
||||||
field: PrincipalField::Secrets,
|
|
||||||
value: PrincipalValue::StringList(vec![admin_pass]),
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
eprintln!("Successfully updated password for {admin_user:?}.");
|
|
||||||
} else {
|
|
||||||
self.create_account(
|
|
||||||
Principal {
|
|
||||||
typ: Type::Superuser,
|
|
||||||
quota: 0,
|
|
||||||
name: admin_user.clone(),
|
|
||||||
secrets: vec![admin_pass],
|
|
||||||
emails: vec![],
|
|
||||||
member_of: vec![],
|
|
||||||
description: "Superuser".to_string().into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
eprintln!("Successfully created administrator account {admin_user:?}.");
|
|
||||||
}
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a default administrator account if none exists
|
|
||||||
if self
|
|
||||||
.get_bitmap(BitmapKey {
|
|
||||||
account_id: u32::MAX,
|
|
||||||
collection: Collection::Principal.into(),
|
|
||||||
class: BitmapClass::DocumentIds,
|
|
||||||
block_num: 0,
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
.unwrap_or_default()
|
|
||||||
.is_empty()
|
|
||||||
{
|
|
||||||
let secret = thread_rng()
|
|
||||||
.sample_iter(Alphanumeric)
|
|
||||||
.take(12)
|
|
||||||
.map(char::from)
|
|
||||||
.collect::<String>();
|
|
||||||
let hashed_secret = sha512_crypt::hash(&secret).unwrap();
|
|
||||||
|
|
||||||
self.create_account(
|
|
||||||
Principal {
|
|
||||||
typ: Type::Superuser,
|
|
||||||
quota: 0,
|
|
||||||
name: "admin".to_string(),
|
|
||||||
secrets: vec![hashed_secret],
|
|
||||||
emails: vec![],
|
|
||||||
member_of: vec![],
|
|
||||||
description: "Superuser".to_string().into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"Created default administrator account \"admin\" with password {secret:?}."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Principal<String>> for Principal<u32> {
|
impl From<Principal<String>> for Principal<u32> {
|
||||||
|
|
|
@ -155,9 +155,9 @@ pub enum PrincipalField {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct PrincipalUpdate {
|
pub struct PrincipalUpdate {
|
||||||
action: PrincipalAction,
|
pub action: PrincipalAction,
|
||||||
field: PrincipalField,
|
pub field: PrincipalField,
|
||||||
value: PrincipalValue,
|
pub value: PrincipalValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
|
|
@ -33,8 +33,8 @@ use ahash::AHashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{
|
backend::{
|
||||||
imap::ImapDirectory, internal::manage::ManageDirectory, ldap::LdapDirectory,
|
imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory,
|
||||||
memory::MemoryDirectory, smtp::SmtpDirectory, sql::SqlDirectory,
|
sql::SqlDirectory,
|
||||||
},
|
},
|
||||||
Directories, Directory, DirectoryInner,
|
Directories, Directory, DirectoryInner,
|
||||||
};
|
};
|
||||||
|
@ -68,15 +68,7 @@ impl Directories {
|
||||||
"internal" => Some(DirectoryInner::Internal(
|
"internal" => Some(DirectoryInner::Internal(
|
||||||
if let Some(store_id) = config.value_require(("directory", id, "store")) {
|
if let Some(store_id) = config.value_require(("directory", id, "store")) {
|
||||||
if let Some(data) = stores.stores.get(store_id) {
|
if let Some(data) = stores.stores.get(store_id) {
|
||||||
match data.clone().init().await {
|
data.clone()
|
||||||
Ok(data) => data,
|
|
||||||
Err(err) => {
|
|
||||||
let err =
|
|
||||||
format!("Failed to initialize store {store_id:?}: {err:?}");
|
|
||||||
config.new_parse_error(("directory", id, "store"), err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
config.new_parse_error(
|
config.new_parse_error(
|
||||||
("directory", id, "store"),
|
("directory", id, "store"),
|
||||||
|
|
|
@ -109,7 +109,7 @@ async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
|
pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
|
||||||
if hashed_secret.starts_with('$') {
|
if hashed_secret.starts_with('$') {
|
||||||
verify_hash_prefix(hashed_secret, secret).await
|
verify_hash_prefix(hashed_secret, secret).await
|
||||||
} else if hashed_secret.starts_with('_') {
|
} else if hashed_secret.starts_with('_') {
|
||||||
|
|
|
@ -201,6 +201,19 @@ impl From<PoolError<mail_send::Error>> for DirectoryError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Principal<u32> {
|
||||||
|
pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self {
|
||||||
|
Principal {
|
||||||
|
id: u32::MAX,
|
||||||
|
typ: Type::Superuser,
|
||||||
|
quota: 0,
|
||||||
|
name: "Fallback Administrator".to_string(),
|
||||||
|
secrets: vec![fallback_pass.into()],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<LdapError> for DirectoryError {
|
impl From<LdapError> for DirectoryError {
|
||||||
fn from(error: LdapError) -> Self {
|
fn from(error: LdapError) -> Self {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|
|
@ -26,6 +26,7 @@ use std::{net::IpAddr, sync::Arc};
|
||||||
use common::{
|
use common::{
|
||||||
expr::{functions::ResolveVariable, *},
|
expr::{functions::ResolveVariable, *},
|
||||||
listener::{ServerInstance, SessionData, SessionManager, SessionStream},
|
listener::{ServerInstance, SessionData, SessionManager, SessionStream},
|
||||||
|
manager::webadmin::Resource,
|
||||||
Core,
|
Core,
|
||||||
};
|
};
|
||||||
use http_body_util::{BodyExt, Full};
|
use http_body_util::{BodyExt, Full};
|
||||||
|
@ -270,7 +271,19 @@ impl JMAP {
|
||||||
Err(err) => err.into_http_response(),
|
Err(err) => err.into_http_response(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => {
|
||||||
|
let path = req.uri().path();
|
||||||
|
return match self
|
||||||
|
.inner
|
||||||
|
.webadmin
|
||||||
|
.get(path.strip_prefix('/').unwrap_or(path))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(resource) if !resource.is_empty() => resource.into_http_response(),
|
||||||
|
Err(err) => err.into_http_response(),
|
||||||
|
_ => RequestError::not_found().into_http_response(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RequestError::not_found().into_http_response()
|
RequestError::not_found().into_http_response()
|
||||||
}
|
}
|
||||||
|
@ -451,6 +464,14 @@ impl ToHttpResponse for store::Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToHttpResponse for std::io::Error {
|
||||||
|
fn into_http_response(self) -> HttpResponse {
|
||||||
|
tracing::error!(context = "i/o", error = %self, "I/O error");
|
||||||
|
|
||||||
|
RequestError::internal_server_error().into_http_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ToHttpResponse for serde_json::Error {
|
impl ToHttpResponse for serde_json::Error {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
RequestError::blank(
|
RequestError::blank(
|
||||||
|
@ -527,6 +548,20 @@ impl ToHttpResponse for DownloadResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToHttpResponse for Resource<Vec<u8>> {
|
||||||
|
fn into_http_response(self) -> HttpResponse {
|
||||||
|
hyper::Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, self.content_type)
|
||||||
|
.body(
|
||||||
|
Full::new(Bytes::from(self.contents))
|
||||||
|
.map_err(|never| match never {})
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ToHttpResponse for UploadResponse {
|
impl ToHttpResponse for UploadResponse {
|
||||||
fn into_http_response(self) -> HttpResponse {
|
fn into_http_response(self) -> HttpResponse {
|
||||||
JsonResponse::new(self).into_http_response()
|
JsonResponse::new(self).into_http_response()
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use directory::backend::internal::manage::ManageDirectory;
|
use directory::backend::internal::manage::ManageDirectory;
|
||||||
use http_body_util::combinators::BoxBody;
|
|
||||||
use hyper::{body::Bytes, Method};
|
use hyper::Method;
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -34,7 +34,7 @@ use crate::{
|
||||||
api::{
|
api::{
|
||||||
http::ToHttpResponse,
|
http::ToHttpResponse,
|
||||||
management::dkim::{obtain_dkim_public_key, Algorithm},
|
management::dkim::{obtain_dkim_public_key, Algorithm},
|
||||||
HttpRequest, JsonResponse,
|
HttpRequest, HttpResponse, JsonResponse,
|
||||||
},
|
},
|
||||||
JMAP,
|
JMAP,
|
||||||
};
|
};
|
||||||
|
@ -48,11 +48,7 @@ struct DnsRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JMAP {
|
impl JMAP {
|
||||||
pub async fn handle_manage_domain(
|
pub async fn handle_manage_domain(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
|
||||||
&self,
|
|
||||||
req: &HttpRequest,
|
|
||||||
path: Vec<&str>,
|
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
|
||||||
match (path.get(1), req.method()) {
|
match (path.get(1), req.method()) {
|
||||||
(None, &Method::GET) => {
|
(None, &Method::GET) => {
|
||||||
// List domains
|
// List domains
|
||||||
|
@ -97,10 +93,28 @@ impl JMAP {
|
||||||
(Some(domain), &Method::POST) => {
|
(Some(domain), &Method::POST) => {
|
||||||
// Create domain
|
// Create domain
|
||||||
match self.core.storage.data.create_domain(domain).await {
|
match self.core.storage.data.create_domain(domain).await {
|
||||||
Ok(_) => JsonResponse::new(json!({
|
Ok(_) => {
|
||||||
"data": (),
|
// Set default domain name if missing
|
||||||
}))
|
if matches!(
|
||||||
.into_http_response(),
|
self.core.storage.config.get("lookup.default.domain").await,
|
||||||
|
Ok(None)
|
||||||
|
) {
|
||||||
|
if let Err(err) = self
|
||||||
|
.core
|
||||||
|
.storage
|
||||||
|
.config
|
||||||
|
.set([("lookup.default.domain", *domain)])
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("Failed to set default domain name: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonResponse::new(json!({
|
||||||
|
"data": (),
|
||||||
|
}))
|
||||||
|
.into_http_response()
|
||||||
|
}
|
||||||
Err(err) => err.into_http_response(),
|
Err(err) => err.into_http_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,14 +32,13 @@ pub mod stores;
|
||||||
|
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use http_body_util::combinators::BoxBody;
|
use hyper::Method;
|
||||||
use hyper::{body::Bytes, Method};
|
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{auth::AccessToken, JMAP};
|
use crate::{auth::AccessToken, JMAP};
|
||||||
|
|
||||||
use super::{http::ToHttpResponse, HttpRequest, JsonResponse};
|
use super::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(tag = "error")]
|
#[serde(tag = "error")]
|
||||||
|
@ -72,7 +71,7 @@ impl JMAP {
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
body: Option<Vec<u8>>,
|
body: Option<Vec<u8>>,
|
||||||
access_token: Arc<AccessToken>,
|
access_token: Arc<AccessToken>,
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
) -> HttpResponse {
|
||||||
let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>();
|
let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>();
|
||||||
let is_superuser = access_token.is_super_user();
|
let is_superuser = access_token.is_super_user();
|
||||||
|
|
||||||
|
@ -85,16 +84,16 @@ impl JMAP {
|
||||||
"queue" if is_superuser => self.handle_manage_queue(req, path).await,
|
"queue" if is_superuser => self.handle_manage_queue(req, path).await,
|
||||||
"reports" if is_superuser => self.handle_manage_reports(req, path).await,
|
"reports" if is_superuser => self.handle_manage_reports(req, path).await,
|
||||||
"dkim" if is_superuser => self.handle_manage_dkim(req, path, body).await,
|
"dkim" if is_superuser => self.handle_manage_dkim(req, path, body).await,
|
||||||
|
"update" if is_superuser => self.handle_manage_update(req, path).await,
|
||||||
"oauth" => self.handle_oauth_api_request(access_token, body).await,
|
"oauth" => self.handle_oauth_api_request(access_token, body).await,
|
||||||
"crypto" => match *req.method() {
|
"crypto" => match *req.method() {
|
||||||
Method::POST => self.handle_crypto_post(access_token, body).await,
|
Method::POST => self.handle_crypto_post(access_token, body).await,
|
||||||
Method::GET => self.handle_crypto_get(access_token).await,
|
Method::GET => self.handle_crypto_get(access_token).await,
|
||||||
_ => RequestError::not_found().into_http_response(),
|
_ => RequestError::not_found().into_http_response(),
|
||||||
},
|
},
|
||||||
"password" => match *req.method() {
|
"password" if req.method() == Method::POST => {
|
||||||
Method::POST => self.handle_change_password(req, access_token, body).await,
|
self.handle_change_password(req, access_token, body).await
|
||||||
_ => RequestError::not_found().into_http_response(),
|
}
|
||||||
},
|
|
||||||
_ => RequestError::not_found().into_http_response(),
|
_ => RequestError::not_found().into_http_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,8 @@ use directory::{
|
||||||
},
|
},
|
||||||
DirectoryError, DirectoryInner, ManagementError, Principal, QueryBy, Type,
|
DirectoryError, DirectoryInner, ManagementError, Principal, QueryBy, Type,
|
||||||
};
|
};
|
||||||
use http_body_util::combinators::BoxBody;
|
|
||||||
use hyper::{body::Bytes, header, Method, StatusCode};
|
use hyper::{header, Method, StatusCode};
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use utils::url_params::UrlParams;
|
use utils::url_params::UrlParams;
|
||||||
|
@ -76,7 +76,7 @@ impl JMAP {
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
path: Vec<&str>,
|
path: Vec<&str>,
|
||||||
body: Option<Vec<u8>>,
|
body: Option<Vec<u8>>,
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
) -> HttpResponse {
|
||||||
match (path.get(1), req.method()) {
|
match (path.get(1), req.method()) {
|
||||||
(None, &Method::POST) => {
|
(None, &Method::POST) => {
|
||||||
// Make sure the current directory supports updates
|
// Make sure the current directory supports updates
|
||||||
|
@ -251,6 +251,18 @@ impl JMAP {
|
||||||
body.as_deref().unwrap_or_default(),
|
body.as_deref().unwrap_or_default(),
|
||||||
) {
|
) {
|
||||||
Ok(changes) => {
|
Ok(changes) => {
|
||||||
|
// Make sure the current directory supports updates
|
||||||
|
if let Some(response) = self.assert_supported_directory() {
|
||||||
|
if changes.iter().any(|change| {
|
||||||
|
!matches!(
|
||||||
|
change.field,
|
||||||
|
PrincipalField::Quota | PrincipalField::Description
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match self
|
match self
|
||||||
.core
|
.core
|
||||||
.storage
|
.storage
|
||||||
|
@ -281,7 +293,7 @@ impl JMAP {
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
access_token: Arc<AccessToken>,
|
access_token: Arc<AccessToken>,
|
||||||
body: Option<Vec<u8>>,
|
body: Option<Vec<u8>>,
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
) -> HttpResponse {
|
||||||
// Make sure the user authenticated using Basic auth
|
// Make sure the user authenticated using Basic auth
|
||||||
if req
|
if req
|
||||||
.headers()
|
.headers()
|
||||||
|
@ -295,11 +307,7 @@ impl JMAP {
|
||||||
.into_http_response();
|
.into_http_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the current directory supports updates
|
// Obtain new password
|
||||||
if let Some(response) = self.assert_supported_directory() {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_password = match String::from_utf8(body.unwrap_or_default()) {
|
let new_password = match String::from_utf8(body.unwrap_or_default()) {
|
||||||
Ok(new_password) if !new_password.is_empty() => new_password,
|
Ok(new_password) if !new_password.is_empty() => new_password,
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -310,6 +318,28 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle Fallback admin password changes
|
||||||
|
if access_token.is_super_user() && access_token.primary_id() == u32::MAX {
|
||||||
|
return match self
|
||||||
|
.core
|
||||||
|
.storage
|
||||||
|
.config
|
||||||
|
.set([("authentication.fallback-admin.secret", new_password)])
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => JsonResponse::new(json!({
|
||||||
|
"data": (),
|
||||||
|
}))
|
||||||
|
.into_http_response(),
|
||||||
|
Err(err) => err.into_http_response(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the current directory supports updates
|
||||||
|
if let Some(response) = self.assert_supported_directory() {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
// Update password
|
// Update password
|
||||||
match self
|
match self
|
||||||
.core
|
.core
|
||||||
|
@ -332,9 +362,7 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_supported_directory(
|
pub fn assert_supported_directory(&self) -> Option<HttpResponse> {
|
||||||
&self,
|
|
||||||
) -> Option<hyper::Response<BoxBody<Bytes, hyper::Error>>> {
|
|
||||||
ManagementApiError::UnsupportedDirectoryOperation {
|
ManagementApiError::UnsupportedDirectoryOperation {
|
||||||
class: match &self.core.storage.directory.store {
|
class: match &self.core.storage.directory.store {
|
||||||
DirectoryInner::Internal(_) => return None,
|
DirectoryInner::Internal(_) => return None,
|
||||||
|
|
|
@ -23,8 +23,7 @@
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use http_body_util::combinators::BoxBody;
|
use hyper::Method;
|
||||||
use hyper::{body::Bytes, Method};
|
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use mail_auth::{
|
use mail_auth::{
|
||||||
dmarc::URI,
|
dmarc::URI,
|
||||||
|
@ -42,7 +41,7 @@ use store::{
|
||||||
use utils::url_params::UrlParams;
|
use utils::url_params::UrlParams;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{http::ToHttpResponse, HttpRequest, JsonResponse},
|
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
|
||||||
JMAP,
|
JMAP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -118,11 +117,7 @@ pub enum Report {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JMAP {
|
impl JMAP {
|
||||||
pub async fn handle_manage_queue(
|
pub async fn handle_manage_queue(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
|
||||||
&self,
|
|
||||||
req: &HttpRequest,
|
|
||||||
path: Vec<&str>,
|
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
|
||||||
let params = UrlParams::new(req.uri().query());
|
let params = UrlParams::new(req.uri().query());
|
||||||
|
|
||||||
match (
|
match (
|
||||||
|
|
|
@ -21,23 +21,19 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use http_body_util::combinators::BoxBody;
|
use common::manager::SPAMFILTER_URL;
|
||||||
use hyper::{body::Bytes, Method};
|
use hyper::Method;
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use utils::url_params::UrlParams;
|
use utils::url_params::UrlParams;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{http::ToHttpResponse, HttpRequest, JsonResponse},
|
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
|
||||||
JMAP,
|
JMAP,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl JMAP {
|
impl JMAP {
|
||||||
pub async fn handle_manage_reload(
|
pub async fn handle_manage_reload(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
|
||||||
&self,
|
|
||||||
req: &HttpRequest,
|
|
||||||
path: Vec<&str>,
|
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
|
||||||
match (path.get(1).copied(), req.method()) {
|
match (path.get(1).copied(), req.method()) {
|
||||||
(Some("lookup"), &Method::GET) => {
|
(Some("lookup"), &Method::GET) => {
|
||||||
match self.core.reload_lookups().await {
|
match self.core.reload_lookups().await {
|
||||||
|
@ -92,4 +88,39 @@ impl JMAP {
|
||||||
_ => RequestError::not_found().into_http_response(),
|
_ => RequestError::not_found().into_http_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_manage_update(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
|
||||||
|
match (path.get(1).copied(), req.method()) {
|
||||||
|
(Some("spam-filter"), &Method::GET) => {
|
||||||
|
match self
|
||||||
|
.core
|
||||||
|
.storage
|
||||||
|
.config
|
||||||
|
.update_external_config(SPAMFILTER_URL)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => JsonResponse::new(json!({
|
||||||
|
"data": result,
|
||||||
|
}))
|
||||||
|
.into_http_response(),
|
||||||
|
Err(err) => err.into_http_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some("webadmin"), &Method::GET) => {
|
||||||
|
match self
|
||||||
|
.inner
|
||||||
|
.webadmin
|
||||||
|
.update_and_unpack(&self.core.storage.blob)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => JsonResponse::new(json!({
|
||||||
|
"data": (),
|
||||||
|
}))
|
||||||
|
.into_http_response(),
|
||||||
|
Err(err) => err.into_http_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => RequestError::not_found().into_http_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,7 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use http_body_util::combinators::BoxBody;
|
use hyper::Method;
|
||||||
use hyper::{body::Bytes, Method};
|
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use mail_auth::report::{
|
use mail_auth::report::{
|
||||||
tlsrpt::{FailureDetails, Policy, TlsReport},
|
tlsrpt::{FailureDetails, Policy, TlsReport},
|
||||||
|
@ -37,7 +36,7 @@ use store::{
|
||||||
use utils::url_params::UrlParams;
|
use utils::url_params::UrlParams;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{http::ToHttpResponse, HttpRequest, JsonResponse},
|
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
|
||||||
JMAP,
|
JMAP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,11 +47,7 @@ enum ReportType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JMAP {
|
impl JMAP {
|
||||||
pub async fn handle_manage_reports(
|
pub async fn handle_manage_reports(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
|
||||||
&self,
|
|
||||||
req: &HttpRequest,
|
|
||||||
path: Vec<&str>,
|
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
|
||||||
match (
|
match (
|
||||||
path.get(1).copied().unwrap_or_default(),
|
path.get(1).copied().unwrap_or_default(),
|
||||||
path.get(2).copied(),
|
path.get(2).copied(),
|
||||||
|
|
|
@ -21,15 +21,14 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use http_body_util::combinators::BoxBody;
|
use hyper::Method;
|
||||||
use hyper::{body::Bytes, Method};
|
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use store::ahash::AHashMap;
|
use store::ahash::AHashMap;
|
||||||
use utils::{config::ConfigKey, url_params::UrlParams};
|
use utils::{config::ConfigKey, url_params::UrlParams};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{http::ToHttpResponse, HttpRequest, JsonResponse},
|
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
|
||||||
JMAP,
|
JMAP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,7 +56,7 @@ impl JMAP {
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
path: Vec<&str>,
|
path: Vec<&str>,
|
||||||
body: Option<Vec<u8>>,
|
body: Option<Vec<u8>>,
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
) -> HttpResponse {
|
||||||
match (path.get(1).copied(), req.method()) {
|
match (path.get(1).copied(), req.method()) {
|
||||||
(Some("group"), &Method::GET) => {
|
(Some("group"), &Method::GET) => {
|
||||||
// List settings
|
// List settings
|
||||||
|
|
|
@ -21,22 +21,17 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use http_body_util::combinators::BoxBody;
|
use hyper::Method;
|
||||||
use hyper::{body::Bytes, Method};
|
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{http::ToHttpResponse, HttpRequest, JsonResponse},
|
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
|
||||||
JMAP,
|
JMAP,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl JMAP {
|
impl JMAP {
|
||||||
pub async fn handle_manage_store(
|
pub async fn handle_manage_store(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
|
||||||
&self,
|
|
||||||
req: &HttpRequest,
|
|
||||||
path: Vec<&str>,
|
|
||||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
|
||||||
match (path.get(1).copied(), req.method()) {
|
match (path.get(1).copied(), req.method()) {
|
||||||
(Some("maintenance"), &Method::GET) => {
|
(Some("maintenance"), &Method::GET) => {
|
||||||
match self
|
match self
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
use std::{net::IpAddr, sync::Arc, time::Instant};
|
use std::{net::IpAddr, sync::Arc, time::Instant};
|
||||||
|
|
||||||
use common::{listener::limiter::InFlight, AuthResult};
|
use common::{listener::limiter::InFlight, AuthResult};
|
||||||
use directory::QueryBy;
|
use directory::{Principal, QueryBy};
|
||||||
use hyper::header;
|
use hyper::header;
|
||||||
use jmap_proto::error::request::RequestError;
|
use jmap_proto::error::request::RequestError;
|
||||||
use mail_parser::decoders::base64::base64_decode;
|
use mail_parser::decoders::base64::base64_decode;
|
||||||
|
@ -179,15 +179,21 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_access_token(&self, account_id: u32) -> Option<AccessToken> {
|
pub async fn get_access_token(&self, account_id: u32) -> Option<AccessToken> {
|
||||||
// Create access token
|
match self
|
||||||
self.update_access_token(AccessToken::new(
|
.core
|
||||||
self.core
|
.storage
|
||||||
.storage
|
.directory
|
||||||
.directory
|
.query(QueryBy::Id(account_id), true)
|
||||||
.query(QueryBy::Id(account_id), true)
|
.await
|
||||||
.await
|
{
|
||||||
.ok()??,
|
Ok(Some(principal)) => self.update_access_token(AccessToken::new(principal)).await,
|
||||||
))
|
_ => match &self.core.jmap.fallback_admin {
|
||||||
.await
|
Some((_, secret)) if account_id == u32::MAX => {
|
||||||
|
self.update_access_token(AccessToken::new(Principal::fallback_admin(secret)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,24 +198,33 @@ impl JMAP {
|
||||||
.into_http_response()
|
.into_http_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn password_hash(&self, account_id: u32) -> Result<String, &'static str> {
|
||||||
|
if account_id != u32::MAX {
|
||||||
|
self.core
|
||||||
|
.storage
|
||||||
|
.directory
|
||||||
|
.query(QueryBy::Id(account_id), false)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Temporary lookup error")?
|
||||||
|
.ok_or("Account no longer exists")?
|
||||||
|
.secrets
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or("Failed to obtain password hash")
|
||||||
|
} else if let Some((_, secret)) = &self.core.jmap.fallback_admin {
|
||||||
|
Ok(secret.clone())
|
||||||
|
} else {
|
||||||
|
Err("Invalid account id.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn issue_token(
|
pub async fn issue_token(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
with_refresh_token: bool,
|
with_refresh_token: bool,
|
||||||
) -> Result<OAuthResponse, &'static str> {
|
) -> Result<OAuthResponse, &'static str> {
|
||||||
let password_hash = self
|
let password_hash = self.password_hash(account_id).await?;
|
||||||
.core
|
|
||||||
.storage
|
|
||||||
.directory
|
|
||||||
.query(QueryBy::Id(account_id), false)
|
|
||||||
.await
|
|
||||||
.map_err(|_| "Temporary lookup error")?
|
|
||||||
.ok_or("Account no longer exists")?
|
|
||||||
.secrets
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.ok_or("Failed to obtain password hash")?;
|
|
||||||
|
|
||||||
Ok(OAuthResponse {
|
Ok(OAuthResponse {
|
||||||
access_token: self.encode_access_token(
|
access_token: self.encode_access_token(
|
||||||
|
@ -324,18 +333,7 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain password hash
|
// Obtain password hash
|
||||||
let password_hash = self
|
let password_hash = self.password_hash(account_id).await?;
|
||||||
.core
|
|
||||||
.storage
|
|
||||||
.directory
|
|
||||||
.query(QueryBy::Id(account_id), false)
|
|
||||||
.await
|
|
||||||
.map_err(|_| "Temporary lookup error")?
|
|
||||||
.ok_or("Account no longer exists")?
|
|
||||||
.secrets
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.ok_or("Failed to obtain password hash")?;
|
|
||||||
|
|
||||||
// Build context
|
// Build context
|
||||||
let key = self.core.jmap.oauth_key.clone();
|
let key = self.core.jmap.oauth_key.clone();
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
use std::{collections::hash_map::RandomState, fmt::Display, sync::Arc, time::Duration};
|
use std::{collections::hash_map::RandomState, fmt::Display, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use auth::{rate_limit::ConcurrencyLimiters, AccessToken};
|
use auth::{rate_limit::ConcurrencyLimiters, AccessToken};
|
||||||
use common::{Core, DeliveryEvent, SharedCore};
|
use common::{manager::webadmin::WebAdminManager, Core, DeliveryEvent, SharedCore};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use directory::QueryBy;
|
use directory::QueryBy;
|
||||||
use email::cache::Threads;
|
use email::cache::Threads;
|
||||||
|
@ -98,6 +98,7 @@ pub struct Inner {
|
||||||
pub sessions: TtlDashMap<String, u32>,
|
pub sessions: TtlDashMap<String, u32>,
|
||||||
pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>,
|
pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>,
|
||||||
pub snowflake_id: SnowflakeIdGenerator,
|
pub snowflake_id: SnowflakeIdGenerator,
|
||||||
|
pub webadmin: WebAdminManager,
|
||||||
|
|
||||||
pub concurrency_limiter: DashMap<u32, Arc<ConcurrencyLimiters>>,
|
pub concurrency_limiter: DashMap<u32, Arc<ConcurrencyLimiters>>,
|
||||||
|
|
||||||
|
@ -131,6 +132,7 @@ impl JMAP {
|
||||||
let capacity = config.property("cache.capacity").unwrap_or(100);
|
let capacity = config.property("cache.capacity").unwrap_or(100);
|
||||||
|
|
||||||
let inner = Inner {
|
let inner = Inner {
|
||||||
|
webadmin: WebAdminManager::new(),
|
||||||
sessions: TtlDashMap::with_capacity(capacity, shard_amount),
|
sessions: TtlDashMap::with_capacity(capacity, shard_amount),
|
||||||
access_tokens: TtlDashMap::with_capacity(capacity, shard_amount),
|
access_tokens: TtlDashMap::with_capacity(capacity, shard_amount),
|
||||||
snowflake_id: config
|
snowflake_id: config
|
||||||
|
@ -149,6 +151,11 @@ impl JMAP {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Unpack webadmin
|
||||||
|
if let Err(err) = inner.webadmin.unpack(&core.load().storage.blob).await {
|
||||||
|
tracing::warn!(event = "error", error = ?err, "Failed to unpack webadmin bundle.");
|
||||||
|
}
|
||||||
|
|
||||||
let jmap_instance = JmapInstance {
|
let jmap_instance = JmapInstance {
|
||||||
core,
|
core,
|
||||||
jmap_inner: Arc::new(inner),
|
jmap_inner: Arc::new(inner),
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use common::config::{manager::BootManager, server::ServerProtocol};
|
use common::{config::server::ServerProtocol, manager::boot::BootManager};
|
||||||
use imap::core::{ImapSessionManager, IMAP};
|
use imap::core::{ImapSessionManager, IMAP};
|
||||||
use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP};
|
use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP};
|
||||||
use managesieve::core::ManageSieveSessionManager;
|
use managesieve::core::ManageSieveSessionManager;
|
||||||
|
|
|
@ -1,30 +1,14 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
BASE_DIR="/Users/me/Downloads/stalwart-test"
|
BASE_DIR="/Users/me/Downloads/stalwart-test"
|
||||||
DOMAIN="example.org"
|
|
||||||
FEATURES="sqlite foundationdb postgres mysql rocks elastic s3 redis"
|
FEATURES="sqlite foundationdb postgres mysql rocks elastic s3 redis"
|
||||||
|
|
||||||
# Delete previous tests
|
# Delete previous tests
|
||||||
rm -rf $BASE_DIR
|
rm -rf $BASE_DIR
|
||||||
|
|
||||||
# Create directories
|
|
||||||
mkdir -p $BASE_DIR $BASE_DIR/data $BASE_DIR/etc
|
|
||||||
|
|
||||||
# Copy resources
|
|
||||||
cp -r resources/config/config.toml $BASE_DIR/etc
|
|
||||||
|
|
||||||
# Replace settings
|
|
||||||
|
|
||||||
sed -i '' -e "s|%{env:STALWART_PATH}%|$BASE_DIR|g" \
|
|
||||||
-e "s|%{env:DOMAIN}%|$DOMAIN|g" \
|
|
||||||
-e "s|%{env:HOSTNAME}%|mail.$DOMAIN|g" \
|
|
||||||
-e "s|%{env:OAUTH_KEY}%|12345|g" \
|
|
||||||
-e 's/level = "info"/level = "trace"/g' "$BASE_DIR/etc/config.toml"
|
|
||||||
|
|
||||||
#sed -i '' -e 's/allow-plain-text = false/allow-plain-text = true/g' \
|
|
||||||
# -e 's/2000\/1m/9999999\/100m/g' \
|
|
||||||
# -e 's/concurrent = 4/concurrent = 90000/g' "$BASE_DIR/etc/imap/settings.toml"
|
|
||||||
|
|
||||||
# Create admin user
|
# Create admin user
|
||||||
SET_ADMIN_USER="admin" SET_ADMIN_PASS="secret" cargo run -p mail-server --no-default-features --features "$FEATURES" -- --config=$BASE_DIR/etc/config.toml
|
cargo run -p mail-server --no-default-features --features "$FEATURES" -- --init=$BASE_DIR
|
||||||
cargo run -p mail-server --no-default-features --features "$FEATURES" -- --config=$BASE_DIR/etc/config.toml
|
|
||||||
|
echo "[server.http]\npermissive-cors = true\n" >> $BASE_DIR/etc/config.toml
|
||||||
|
echo "[tracer.stdout]\ntype = 'stdout'\nlevel = 'info'\nansi = true\nenable = true" >> $BASE_DIR/etc/config.toml
|
||||||
|
#cargo run -p mail-server --no-default-features --features "$FEATURES" -- --config=$BASE_DIR/etc/config.toml
|
||||||
|
|
Loading…
Reference in a new issue