mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-02-25 00:12:58 +08:00
Config API changes
This commit is contained in:
parent
110349f5c2
commit
9a4110e343
11 changed files with 202 additions and 65 deletions
|
@ -329,7 +329,7 @@ impl LdapMappings {
|
|||
|
||||
tracing::debug!(
|
||||
context = "ldap",
|
||||
event = "fetch_princpal",
|
||||
event = "fetch_principal",
|
||||
entry = ?entry,
|
||||
"LDAP entry"
|
||||
);
|
||||
|
|
|
@ -204,7 +204,12 @@ pub fn parse_date(value: &[u8]) -> Result<i64> {
|
|||
.trim();
|
||||
NaiveDate::parse_from_str(date, "%d-%b-%Y")
|
||||
.map_err(|_| Cow::from(format!("Failed to parse date '{}'.", date)))
|
||||
.map(|dt| dt.and_hms_opt(0, 0, 0).unwrap_or_default().timestamp())
|
||||
.map(|dt| {
|
||||
dt.and_hms_opt(0, 0, 0)
|
||||
.unwrap_or_default()
|
||||
.and_utc()
|
||||
.timestamp()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_number<T: FromStr>(value: &[u8]) -> Result<T> {
|
||||
|
|
|
@ -78,8 +78,9 @@ pub enum UpdateSettings {
|
|||
prefix: String,
|
||||
},
|
||||
Insert {
|
||||
prefix: String,
|
||||
prefix: Option<String>,
|
||||
values: Vec<(String, String)>,
|
||||
assert_empty: bool,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -369,7 +370,7 @@ impl JMAP {
|
|||
}))
|
||||
.into_http_response()
|
||||
}
|
||||
("settings", None, &Method::GET) => {
|
||||
("settings", Some("group"), &Method::GET) => {
|
||||
// List settings
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let prefix = params
|
||||
|
@ -382,8 +383,8 @@ impl JMAP {
|
|||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let groupby = params
|
||||
.get("groupby")
|
||||
let suffix = params
|
||||
.get("suffix")
|
||||
.map(|s| {
|
||||
if !s.starts_with('.') {
|
||||
format!(".{s}")
|
||||
|
@ -392,19 +393,20 @@ impl JMAP {
|
|||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let field = params.get("field");
|
||||
let filter = params.get("filter").unwrap_or_default();
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
let mut offset =
|
||||
params.parse::<usize>("page").unwrap_or(0).saturating_sub(1) * limit;
|
||||
let has_filter = !filter.is_empty();
|
||||
|
||||
match self.store.config_list(&prefix).await {
|
||||
Ok(settings) => if groupby.len() > 1 && !settings.is_empty() {
|
||||
match self.store.config_list(&prefix, true).await {
|
||||
Ok(settings) => if !suffix.is_empty() && !settings.is_empty() {
|
||||
// Obtain record ids
|
||||
let mut total = 0;
|
||||
let mut ids = Vec::new();
|
||||
for (key, _) in &settings {
|
||||
if let Some(id) = key.strip_suffix(&groupby) {
|
||||
if let Some(id) = key.strip_suffix(&suffix) {
|
||||
if !id.is_empty() {
|
||||
if !has_filter {
|
||||
if offset == 0 {
|
||||
|
@ -430,7 +432,9 @@ impl JMAP {
|
|||
record.insert("_id".to_string(), id.to_string());
|
||||
for (k, v) in &settings {
|
||||
if let Some(k) = k.strip_prefix(&prefix) {
|
||||
record.insert(k.to_string(), v.to_string());
|
||||
if field.map_or(true, |field| field == k) {
|
||||
record.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
} else if record.len() > 1 {
|
||||
break;
|
||||
}
|
||||
|
@ -458,8 +462,7 @@ impl JMAP {
|
|||
"items": records,
|
||||
},
|
||||
}))
|
||||
} else if !groupby.is_empty() {
|
||||
// groupby=.
|
||||
} else {
|
||||
let total = settings.len();
|
||||
let items = settings
|
||||
.into_iter()
|
||||
|
@ -485,7 +488,34 @@ impl JMAP {
|
|||
"items": items,
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
}
|
||||
.into_http_response(),
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Config fetch failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response(),
|
||||
}
|
||||
}
|
||||
("settings", Some("list"), &Method::GET) => {
|
||||
// List settings
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let prefix = params
|
||||
.get("prefix")
|
||||
.map(|p| {
|
||||
if !p.ends_with('.') {
|
||||
format!("{p}.")
|
||||
} else {
|
||||
p.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
let offset = params.parse::<usize>("page").unwrap_or(0).saturating_sub(1) * limit;
|
||||
|
||||
match self.store.config_list(&prefix, true).await {
|
||||
Ok(settings) => {
|
||||
let total = settings.len();
|
||||
let items = settings
|
||||
.into_iter()
|
||||
|
@ -499,8 +529,8 @@ impl JMAP {
|
|||
"items": items,
|
||||
},
|
||||
}))
|
||||
.into_http_response()
|
||||
}
|
||||
.into_http_response(),
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Config fetch failed",
|
||||
|
@ -509,18 +539,63 @@ impl JMAP {
|
|||
.into_http_response(),
|
||||
}
|
||||
}
|
||||
("settings", Some(key), &Method::GET) => match self.store.config_get(key).await {
|
||||
Ok(value) => JsonResponse::new(json!({
|
||||
"data": value,
|
||||
}))
|
||||
.into_http_response(),
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Config fetch failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response(),
|
||||
},
|
||||
("settings", Some("keys"), &Method::GET) => {
|
||||
// Obtain keys
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let keys = params
|
||||
.get("keys")
|
||||
.map(|s| s.split(',').collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
let prefixes = params
|
||||
.get("prefixes")
|
||||
.map(|s| s.split(',').collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
let mut err = String::new();
|
||||
let mut results = AHashMap::with_capacity(keys.len());
|
||||
|
||||
for key in keys {
|
||||
match self.store.config_get(key).await {
|
||||
Ok(Some(value)) => {
|
||||
results.insert(key.to_string(), value);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err_) => {
|
||||
err = err_.to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for prefix in prefixes {
|
||||
let prefix = if !prefix.ends_with('.') {
|
||||
format!("{prefix}.")
|
||||
} else {
|
||||
prefix.to_string()
|
||||
};
|
||||
match self.store.config_list(&prefix, false).await {
|
||||
Ok(values) => {
|
||||
results.extend(values);
|
||||
}
|
||||
Err(err_) => {
|
||||
err = err_.to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err.is_empty() {
|
||||
JsonResponse::new(json!({
|
||||
"data": results,
|
||||
}))
|
||||
.into_http_response()
|
||||
} else {
|
||||
RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Config fetch failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response()
|
||||
}
|
||||
}
|
||||
("settings", Some(prefix), &Method::DELETE) if !prefix.is_empty() => {
|
||||
match self.store.config_clear(prefix).await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
|
@ -539,36 +614,66 @@ impl JMAP {
|
|||
if let Some(changes) =
|
||||
body.and_then(|body| serde_json::from_slice::<Vec<UpdateSettings>>(&body).ok())
|
||||
{
|
||||
let mut result = Ok(());
|
||||
let mut result = Ok(true);
|
||||
|
||||
'next: for change in changes {
|
||||
match change {
|
||||
UpdateSettings::Delete { keys } => {
|
||||
for key in keys {
|
||||
result = self.store.config_clear(key).await;
|
||||
result = self.store.config_clear(key).await.map(|_| true);
|
||||
if result.is_err() {
|
||||
break 'next;
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateSettings::Clear { prefix } => {
|
||||
result = self.store.config_clear_prefix(&prefix).await;
|
||||
result =
|
||||
self.store.config_clear_prefix(&prefix).await.map(|_| true);
|
||||
if result.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
UpdateSettings::Insert { prefix, values } => {
|
||||
UpdateSettings::Insert {
|
||||
prefix,
|
||||
values,
|
||||
assert_empty,
|
||||
} => {
|
||||
if assert_empty {
|
||||
if let Some(prefix) = &prefix {
|
||||
result = self
|
||||
.store
|
||||
.config_list(&format!("{prefix}."), true)
|
||||
.await
|
||||
.map(|items| items.is_empty());
|
||||
|
||||
if matches!(result, Ok(false) | Err(_)) {
|
||||
break;
|
||||
}
|
||||
} else if let Some((key, _)) = values.first() {
|
||||
result = self
|
||||
.store
|
||||
.config_get(key)
|
||||
.await
|
||||
.map(|items| items.is_none());
|
||||
|
||||
if matches!(result, Ok(false) | Err(_)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = self
|
||||
.store
|
||||
.config_set(values.into_iter().map(|(key, value)| ConfigKey {
|
||||
key: if !prefix.is_empty() {
|
||||
key: if let Some(prefix) = &prefix {
|
||||
format!("{prefix}.{key}")
|
||||
} else {
|
||||
key
|
||||
},
|
||||
value,
|
||||
}))
|
||||
.await;
|
||||
.await
|
||||
.map(|_| true);
|
||||
if result.is_err() {
|
||||
break;
|
||||
}
|
||||
|
@ -577,10 +682,15 @@ impl JMAP {
|
|||
}
|
||||
|
||||
match result {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
Ok(true) => JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
.into_http_response(),
|
||||
Ok(false) => JsonResponse::new(json!({
|
||||
"error": "assertFailed",
|
||||
"details": "Failed to assert empty prefix",
|
||||
}))
|
||||
.into_http_response(),
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Config update failed",
|
||||
|
|
|
@ -110,7 +110,7 @@ pub fn spawn_housekeeper(
|
|||
// for now, we just reload the blocked IP addresses
|
||||
let core = core.clone();
|
||||
tokio::spawn(async move {
|
||||
match core.store.config_list(BLOCKED_IP_PREFIX).await {
|
||||
match core.store.config_list(BLOCKED_IP_PREFIX, true).await {
|
||||
Ok(settings) => {
|
||||
if let Err(err) = core
|
||||
.directory
|
||||
|
|
|
@ -34,7 +34,11 @@ impl Store {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn config_list(&self, prefix: &str) -> crate::Result<Vec<(String, String)>> {
|
||||
pub async fn config_list(
|
||||
&self,
|
||||
prefix: &str,
|
||||
strip_prefix: bool,
|
||||
) -> crate::Result<Vec<(String, String)>> {
|
||||
let key = prefix.as_bytes();
|
||||
let from_key = ValueKey::from(ValueClass::Config(key.to_vec()));
|
||||
let to_key = ValueKey::from(ValueClass::Config(
|
||||
|
@ -51,7 +55,7 @@ impl Store {
|
|||
std::str::from_utf8(key.get(1..).unwrap_or_default()).map_err(|_| {
|
||||
crate::Error::InternalError("Failed to deserialize config key".to_string())
|
||||
})?;
|
||||
if !prefix.is_empty() {
|
||||
if strip_prefix && !prefix.is_empty() {
|
||||
key = key.strip_prefix(prefix).unwrap_or(key);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{Datelike, TimeZone, Timelike};
|
||||
use chrono::{Datelike, Local, TimeDelta, TimeZone, Timelike};
|
||||
|
||||
use super::utils::ParseValue;
|
||||
|
||||
|
@ -36,39 +36,40 @@ pub enum SimpleCron {
|
|||
|
||||
impl SimpleCron {
|
||||
pub fn time_to_next(&self) -> Duration {
|
||||
let now = chrono::Local::now();
|
||||
let now = Local::now();
|
||||
let next = match self {
|
||||
SimpleCron::Day { hour, minute } => {
|
||||
let next = chrono::Local
|
||||
let next = Local
|
||||
.with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
|
||||
.earliest()
|
||||
.unwrap_or_else(|| now - chrono::Duration::seconds(1));
|
||||
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
|
||||
if next < now {
|
||||
next + chrono::Duration::days(1)
|
||||
next + TimeDelta::try_days(1).unwrap_or_default()
|
||||
} else {
|
||||
next
|
||||
}
|
||||
}
|
||||
SimpleCron::Week { day, hour, minute } => {
|
||||
let next = chrono::Local
|
||||
let next = Local
|
||||
.with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
|
||||
.earliest()
|
||||
.unwrap_or_else(|| now - chrono::Duration::seconds(1));
|
||||
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
|
||||
if next < now {
|
||||
next + chrono::Duration::days(
|
||||
next + TimeDelta::try_days(
|
||||
(7 - now.weekday().number_from_monday() + *day).into(),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
next
|
||||
}
|
||||
}
|
||||
SimpleCron::Hour { minute } => {
|
||||
let next = chrono::Local
|
||||
let next = Local
|
||||
.with_ymd_and_hms(now.year(), now.month(), now.day(), now.hour(), *minute, 0)
|
||||
.earliest()
|
||||
.unwrap_or_else(|| now - chrono::Duration::seconds(1));
|
||||
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
|
||||
if next < now {
|
||||
next + chrono::Duration::hours(1)
|
||||
next + TimeDelta::try_hours(1).unwrap_or_default()
|
||||
} else {
|
||||
next
|
||||
}
|
||||
|
@ -96,9 +97,11 @@ impl ParseValue for SimpleCron {
|
|||
));
|
||||
}
|
||||
} else if pos == 1 {
|
||||
if value.as_bytes().first().ok_or_else(|| {
|
||||
format!("Invalid cron key {key:?}: failed to parse cron weekday")
|
||||
})? == &b'*'
|
||||
if value
|
||||
.as_bytes()
|
||||
.first()
|
||||
.ok_or_else(|| format!("Invalid cron key {key:?}: failed to parse cron hour"))?
|
||||
== &b'*'
|
||||
{
|
||||
return Ok(SimpleCron::Hour { minute });
|
||||
} else {
|
||||
|
|
|
@ -7,7 +7,7 @@ hostname = "%{HOST}%"
|
|||
max-connections = 8192
|
||||
|
||||
#[server.proxy]
|
||||
#trusted-networks = {"127.0.0.0/8", "::1", "10.0.0.0/8"}
|
||||
#trusted-networks = ["127.0.0.0/8", "::1", "10.0.0.0/8"]
|
||||
|
||||
[server.security]
|
||||
blocked-networks = {}
|
||||
|
|
|
@ -8,7 +8,6 @@ implicit = false
|
|||
timeout = "1m"
|
||||
certificate = "default"
|
||||
#acme = "letsencrypt"
|
||||
#sni = [{subject = "", certificate = ""}]
|
||||
#protocols = ["TLSv1.2", "TLSv1.3"]
|
||||
#ciphers = [ "TLS13_AES_256_GCM_SHA384", "TLS13_AES_128_GCM_SHA256",
|
||||
# "TLS13_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
|
@ -26,5 +25,6 @@ port = 443
|
|||
renew-before = "30d"
|
||||
|
||||
[certificate."default"]
|
||||
sni-subjects = []
|
||||
cert = "file://__CERT_PATH__"
|
||||
private-key = "file://__PK_PATH__"
|
||||
|
|
|
@ -2,20 +2,23 @@
|
|||
# Tracing & logging configuration
|
||||
#############################################
|
||||
|
||||
#[global.tracing]
|
||||
#method = "stdout"
|
||||
#level = "trace"
|
||||
[tracing."stdout"]
|
||||
method = "stdout"
|
||||
level = "trace"
|
||||
enable = false
|
||||
|
||||
#[global.tracing]
|
||||
#method = "open-telemetry"
|
||||
#transport = "http"
|
||||
#endpoint = "https://127.0.0.1/otel"
|
||||
#headers = ["Authorization: <place_auth_here>"]
|
||||
#level = "debug"
|
||||
[tracing."ot"]
|
||||
method = "open-telemetry"
|
||||
transport = "http"
|
||||
endpoint = "https://127.0.0.1/otel"
|
||||
headers = ["Authorization: <place_auth_here>"]
|
||||
level = "debug"
|
||||
enable = false
|
||||
|
||||
[global.tracing]
|
||||
[tracing."log"]
|
||||
method = "log"
|
||||
path = "%{BASE_PATH}%/logs"
|
||||
prefix = "stalwart.log"
|
||||
rotate = "daily"
|
||||
level = "info"
|
||||
enable = true
|
||||
|
|
|
@ -92,7 +92,9 @@ return-path = false
|
|||
key = ["remote_ip"]
|
||||
concurrency = 5
|
||||
#rate = "5/1h"
|
||||
enable = true
|
||||
|
||||
[[session.throttle]]
|
||||
key = ["sender_domain", "rcpt"]
|
||||
rate = "25/1h"
|
||||
enable = true
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{TimeDelta, Utc};
|
||||
|
||||
use directory::backend::internal::manage::ManageDirectory;
|
||||
use jmap_proto::types::id::Id;
|
||||
|
@ -138,7 +138,12 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
|
||||
// Vacation responses should honor the configured date ranges
|
||||
client
|
||||
.vacation_response_set_dates((Utc::now() + Duration::days(1)).timestamp().into(), None)
|
||||
.vacation_response_set_dates(
|
||||
(Utc::now() + TimeDelta::try_days(1).unwrap_or_default())
|
||||
.timestamp()
|
||||
.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
lmtp.ingest(
|
||||
|
@ -157,7 +162,12 @@ pub async fn test(params: &mut JMAPTest) {
|
|||
expect_nothing(&mut smtp_rx).await;
|
||||
|
||||
client
|
||||
.vacation_response_set_dates((Utc::now() - Duration::days(1)).timestamp().into(), None)
|
||||
.vacation_response_set_dates(
|
||||
(Utc::now() - TimeDelta::try_days(1).unwrap_or_default())
|
||||
.timestamp()
|
||||
.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
smtp_settings.lock().do_stop = true;
|
||||
|
|
Loading…
Reference in a new issue