Config API changes

This commit is contained in:
mdecimus 2024-03-18 16:16:00 +01:00
parent 110349f5c2
commit 9a4110e343
11 changed files with 202 additions and 65 deletions

View file

@ -329,7 +329,7 @@ impl LdapMappings {
tracing::debug!(
context = "ldap",
event = "fetch_princpal",
event = "fetch_principal",
entry = ?entry,
"LDAP entry"
);

View file

@ -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> {

View file

@ -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",

View file

@ -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

View file

@ -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);
}

View file

@ -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 {

View file

@ -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 = {}

View file

@ -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__"

View file

@ -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

View file

@ -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

View file

@ -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;