diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b482ab..de836567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.11.2] - 2025-01-17 + +To upgrade update the webadmin and then replace the `stalwart-mail` binary. + +### Added +- Automatic revoking of access tokens when secrets, permissions, ACLs or group memberships change (#649). +- Cluster node roles. +- `config_get` expression function. + +### Changed +- `lookup.default.hostname` is now `server.hostname`. +- `lookup.default.domain` is now `report.domain`. + +### Fixed +- Distributed locking issues in non-Redis stores (#1066). +- Panic parsing broken HTMLs. +- Update CLI response serializer to v0.11.x (#1082). +- Histogram bucket counts (#1079). +- Do not rate limit trusted IPs (#1078). +- Avoid double encrypting PGP parts encoded as plain text (#1083). +- Return empty SASL challenge rather than "" (#1064). + ## [0.11.0] - 2025-01-06 This version includes breaking changes to the configuration file, please read [UPGRADING.md](UPGRADING.md) for details. diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 21b9b9e9..98af24c2 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -337,6 +337,7 @@ impl Server { .counter_incr( KeyValue::with_prefix(KV_PRINCIPAL_REVISION, id.to_be_bytes(), 1) .expires(30 * 86400), + false, ) .await { diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index 7c6c97b8..ccf05d61 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -80,7 +80,7 @@ impl Server { let value = params.next_as_integer(); self.get_in_memory_store_or_default(store.as_ref(), session_id) - .counter_incr(KeyValue::new(key.into_owned(), value)) + .counter_incr(KeyValue::new(key.into_owned(), value), true) .await .map(Variable::Integer) .caused_by(trc::location!()) diff --git a/crates/spam-filter/src/modules/bayes.rs b/crates/spam-filter/src/modules/bayes.rs index b5db06c7..f50d839b 100644 --- a/crates/spam-filter/src/modules/bayes.rs +++ b/crates/spam-filter/src/modules/bayes.rs @@ -137,10 +137,13 @@ impl BayesClassifier for Server { }; for (hash, weights) in model.weights { self.in_memory_store() - .counter_incr(KeyValue::new( - hash.serialize(prefix, ctx.input.account_id), - i64::from(weights), - )) + .counter_incr( + KeyValue::new( + hash.serialize(prefix, ctx.input.account_id), + i64::from(weights), + ), + false, + ) .await .caused_by(trc::location!())?; if is_global { @@ -158,10 +161,13 @@ impl BayesClassifier for Server { Weights { spam: 0, ham: 1 } }; self.in_memory_store() - .counter_incr(KeyValue::new( - TokenHash::default().serialize(prefix, ctx.input.account_id), - i64::from(weights), - )) + .counter_incr( + KeyValue::new( + TokenHash::default().serialize(prefix, ctx.input.account_id), + i64::from(weights), + ), + false, + ) .await .caused_by(trc::location!()) .map(|_| ()) diff --git a/crates/store/src/dispatch/lookup.rs b/crates/store/src/dispatch/lookup.rs index 798d4933..e6652fc8 100644 --- a/crates/store/src/dispatch/lookup.rs +++ b/crates/store/src/dispatch/lookup.rs @@ -57,7 +57,7 @@ impl InMemoryStore { .caused_by(trc::location!()) } - pub async fn counter_incr(&self, kv: KeyValue) -> trc::Result { + pub async fn counter_incr(&self, kv: KeyValue, return_value: bool) -> trc::Result { match self { InMemoryStore::Store(store) => { let mut batch = BatchBuilder::new(); @@ -75,15 +75,24 @@ impl InMemoryStore { }); } - batch.ops.push(Operation::Value { - class: ValueClass::InMemory(InMemoryClass::Counter(kv.key)), - op: ValueOp::AddAndGet(kv.value), - }); + if return_value { + batch.ops.push(Operation::Value { + class: ValueClass::InMemory(InMemoryClass::Counter(kv.key)), + op: ValueOp::AddAndGet(kv.value), + }); - store - .write(batch.build()) - .await - .and_then(|r| r.last_counter_id()) + store + .write(batch.build()) + .await + .and_then(|r| r.last_counter_id()) + } else { + batch.ops.push(Operation::Value { + class: ValueClass::InMemory(InMemoryClass::Counter(kv.key)), + op: ValueOp::AtomicAdd(kv.value), + }); + + store.write(batch.build()).await.map(|_| 0) + } } #[cfg(feature = "redis")] InMemoryStore::Redis(store) => store.key_incr(&kv.key, kv.value, kv.expires).await, @@ -260,7 +269,7 @@ impl InMemoryStore { bucket.extend_from_slice(range_start.to_be_bytes().as_slice()); let requests = if !soft_check { - self.counter_incr(KeyValue::new(bucket, 1).expires(expires_in)) + self.counter_incr(KeyValue::new(bucket, 1).expires(expires_in), true) .await .caused_by(trc::location!())? } else { diff --git a/tests/src/store/lookup.rs b/tests/src/store/lookup.rs index 03caa040..7edf434d 100644 --- a/tests/src/store/lookup.rs +++ b/tests/src/store/lookup.rs @@ -28,7 +28,6 @@ pub async fn lookup_tests() { }; for (store_id, store) in stores.in_memory_stores { - let is_mysql = store_id == "mysql"; println!("Testing in-memory store {}...", store_id); if let InMemoryStore::Store(store) = &store { store.destroy().await; @@ -72,30 +71,25 @@ pub async fn lookup_tests() { // Test counter let key = "abc".as_bytes().to_vec(); store - .counter_incr(KeyValue::new(key.clone(), 1)) + .counter_incr(KeyValue::new(key.clone(), 1), true) .await .unwrap(); assert_eq!(1, store.counter_get(key.clone()).await.unwrap()); store - .counter_incr(KeyValue::new(key.clone(), 2)) + .counter_incr(KeyValue::new(key.clone(), 2), true) .await .unwrap(); assert_eq!(3, store.counter_get(key.clone()).await.unwrap()); - if !is_mysql { - store - .counter_incr(KeyValue::new(key.clone(), -3)) - .await - .unwrap(); - } else { - // TODO: Detect mySQL version and use RETURNING - store.counter_delete(key.clone()).await.unwrap(); - } + store + .counter_incr(KeyValue::new(key.clone(), -3), false) + .await + .unwrap(); assert_eq!(0, store.counter_get(key.clone()).await.unwrap()); // Test counter expiry let key = "fgh".as_bytes().to_vec(); store - .counter_incr(KeyValue::new(key.clone(), 1).expires(1)) + .counter_incr(KeyValue::new(key.clone(), 1).expires(1), false) .await .unwrap(); assert_eq!(1, store.counter_get(key.clone()).await.unwrap()); @@ -171,7 +165,10 @@ pub async fn lookup_tests() { .await .unwrap(); store - .counter_incr(KeyValue::with_prefix(0, pack_u32(1, v), 123).expires(3600)) + .counter_incr( + KeyValue::with_prefix(0, pack_u32(1, v), 123).expires(3600), + false, + ) .await .unwrap(); }