Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e442c3a770 | ||
|
|
20b9411687 | ||
|
|
d7d94904d5 | ||
|
|
3510b28ce1 |
No files matched your search
@@ -11,12 +11,17 @@
|
|||||||
//! and mails it. The weekly digest is a built-in report (RP-21).
|
//! and mails it. The weekly digest is a built-in report (RP-21).
|
||||||
//!
|
//!
|
||||||
//! Kept in the fork's subspace (`store::SUBSPACE_INBUXA`). Every key starts
|
//! Kept in the fork's subspace (`store::SUBSPACE_INBUXA`). Every key starts
|
||||||
//! with `S`, then one byte for the kind:
|
//! with `G`, then one byte for the kind:
|
||||||
//!
|
//!
|
||||||
//! - `r` + report id (u64): a report, as JSON.
|
//! - `r` + report id (u64): a report, as JSON.
|
||||||
//! - `s`: the settings, as JSON.
|
//! - `s`: the settings, as JSON.
|
||||||
//!
|
//!
|
||||||
//! Numbers are big-endian.
|
//! Numbers are big-endian.
|
||||||
|
//!
|
||||||
|
//! 2026.10.6.2 kept them under `S`, a byte scale-out storage (`Sb`, `Sm`,
|
||||||
|
//! `Sl`, replica markers `Sr` + index) and the spam-rules updater (`Sr`,
|
||||||
|
//! `Sf`, `Sn`) already use. `ensure_digest` moves them once, deleting only
|
||||||
|
//! the exact keys it moved.
|
||||||
|
|
||||||
use chrono::{Datelike, Duration, LocalResult, NaiveDate, TimeZone, Utc};
|
use chrono::{Datelike, Duration, LocalResult, NaiveDate, TimeZone, Utc};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
@@ -27,7 +32,9 @@ use store::{
|
|||||||
};
|
};
|
||||||
use trc::AddContext;
|
use trc::AddContext;
|
||||||
|
|
||||||
const FEATURE: u8 = b'S';
|
const FEATURE: u8 = b'G';
|
||||||
|
/// Where 2026.10.6.2 kept them; see the module notes.
|
||||||
|
const LEGACY_FEATURE: u8 = b'S';
|
||||||
const KIND_REPORT: u8 = b'r';
|
const KIND_REPORT: u8 = b'r';
|
||||||
const KIND_SETTINGS: u8 = b's';
|
const KIND_SETTINGS: u8 = b's';
|
||||||
|
|
||||||
@@ -388,8 +395,12 @@ impl<T: for<'de> SerdeDeserialize<'de> + Send + Sync> Deserialize for Json<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn class(kind: u8, id: Option<u64>) -> ValueClass {
|
fn class(kind: u8, id: Option<u64>) -> ValueClass {
|
||||||
|
class_in(FEATURE, kind, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn class_in(feature: u8, kind: u8, id: Option<u64>) -> ValueClass {
|
||||||
let mut key = Vec::with_capacity(10);
|
let mut key = Vec::with_capacity(10);
|
||||||
key.push(FEATURE);
|
key.push(feature);
|
||||||
key.push(kind);
|
key.push(kind);
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
key.extend_from_slice(&id.to_be_bytes());
|
key.extend_from_slice(&id.to_be_bytes());
|
||||||
@@ -461,8 +472,66 @@ pub fn next_id(reports: &[Report]) -> u64 {
|
|||||||
+ 1
|
+ 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static MOVED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Moves reports and settings from where 2026.10.6.2 kept them. Only what
|
||||||
|
/// reads as a report or the settings is moved, and only those exact keys
|
||||||
|
/// are deleted, so the spam-rules marker and replica markers stay put.
|
||||||
|
async fn move_legacy(data: &Store) -> trc::Result<()> {
|
||||||
|
let mut found = Vec::new();
|
||||||
|
data.iterate(
|
||||||
|
IterateParams::new(
|
||||||
|
ValueKey::from(class_in(LEGACY_FEATURE, KIND_REPORT, Some(0))),
|
||||||
|
ValueKey::from(class_in(LEGACY_FEATURE, KIND_REPORT, Some(u64::MAX))),
|
||||||
|
),
|
||||||
|
|_, value| {
|
||||||
|
if let Ok(Json(report)) = Json::<Report>::deserialize(value)
|
||||||
|
&& !report.name.is_empty()
|
||||||
|
{
|
||||||
|
found.push(report);
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?;
|
||||||
|
let legacy_settings = data
|
||||||
|
.get_value::<Json<Settings>>(ValueKey::from(class_in(
|
||||||
|
LEGACY_FEATURE,
|
||||||
|
KIND_SETTINGS,
|
||||||
|
None,
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?;
|
||||||
|
if found.is_empty() && legacy_settings.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut batch = BatchBuilder::new();
|
||||||
|
for report in &found {
|
||||||
|
if self::report(data, report.id).await?.is_none() {
|
||||||
|
batch.set(
|
||||||
|
class(KIND_REPORT, Some(report.id)),
|
||||||
|
Json(report).serialize()?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
batch.clear(class_in(LEGACY_FEATURE, KIND_REPORT, Some(report.id)));
|
||||||
|
}
|
||||||
|
if let Some(Json(settings)) = legacy_settings {
|
||||||
|
batch.set(class(KIND_SETTINGS, None), Json(&settings).serialize()?);
|
||||||
|
batch.clear(class_in(LEGACY_FEATURE, KIND_SETTINGS, None));
|
||||||
|
}
|
||||||
|
data.write(batch.build_all())
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Every server has the digest (RP-21); written the first time it's missed.
|
/// Every server has the digest (RP-21); written the first time it's missed.
|
||||||
pub async fn ensure_digest(data: &Store, now: u64) -> trc::Result<()> {
|
pub async fn ensure_digest(data: &Store, now: u64) -> trc::Result<()> {
|
||||||
|
if !MOVED.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
move_legacy(data).await?;
|
||||||
|
MOVED.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
if report(data, DIGEST_ID).await?.is_none() {
|
if report(data, DIGEST_ID).await?.is_none() {
|
||||||
put_report(data, &Report::digest(now)).await?;
|
put_report(data, &Report::digest(now)).await?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ fn legacy_setting(name: &str, is_set: impl Fn(&str) -> bool) -> Option<String> {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! brand_version {
|
macro_rules! brand_version {
|
||||||
() => {
|
() => {
|
||||||
"2026.10.6.2"
|
"2026.10.6.3"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ use registry::schema::{
|
|||||||
};
|
};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use store::{
|
||||||
|
SUBSPACE_INBUXA, ValueKey,
|
||||||
|
write::{AnyClass, BatchBuilder, ValueClass},
|
||||||
|
};
|
||||||
|
|
||||||
const USING: &[&str] = &[
|
const USING: &[&str] = &[
|
||||||
"urn:ietf:params:jmap:core",
|
"urn:ietf:params:jmap:core",
|
||||||
@@ -53,6 +57,80 @@ pub async fn test(test: &mut TestServer) {
|
|||||||
let admin = test.account("[email protected]");
|
let admin = test.account("[email protected]");
|
||||||
let me = "[email protected]";
|
let me = "[email protected]";
|
||||||
|
|
||||||
|
// --- Moved from where 2026.10.6.2 kept them (`S`, shared with the spam
|
||||||
|
// rules marker and replica markers), touching nothing else ---------------
|
||||||
|
let any = |key: Vec<u8>| {
|
||||||
|
ValueClass::Any(AnyClass {
|
||||||
|
subspace: SUBSPACE_INBUXA,
|
||||||
|
key,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let legacy_digest = [b"Sr".as_slice(), &1u64.to_be_bytes()].concat();
|
||||||
|
let mut batch = BatchBuilder::new();
|
||||||
|
batch.set(
|
||||||
|
any(legacy_digest.clone()),
|
||||||
|
serde_json::to_vec(&json!({
|
||||||
|
"id": 1, "name": "Weekly digest (moved)",
|
||||||
|
"enabled": true,
|
||||||
|
"builtIn": true,
|
||||||
|
"sections": ["mailFlow", "queue", "spoofing", "tlsFailures", "deliverability",
|
||||||
|
"security", "storage", "certificates"],
|
||||||
|
"createdAt": 1790000000,
|
||||||
|
"lastDue": 1790000000
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
batch.set(
|
||||||
|
any(b"Ss".to_vec()),
|
||||||
|
serde_json::to_vec(&json!({"fromName": "Moved sender"})).unwrap(),
|
||||||
|
);
|
||||||
|
batch.set(any(b"Sr".to_vec()), b"3.0.2+2".to_vec());
|
||||||
|
test.server.store().write(batch.build_all()).await.unwrap();
|
||||||
|
let list = reports(admin).await;
|
||||||
|
assert!(
|
||||||
|
list.iter().any(|r| r["name"] == "Weekly digest (moved)"),
|
||||||
|
"the digest wasn't moved: {list:?}"
|
||||||
|
);
|
||||||
|
let (_, response) = call(
|
||||||
|
admin,
|
||||||
|
"inbuxa:ScheduledReportSettings/get",
|
||||||
|
json!({"ids": null}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
response["list"][0]["fromName"], "Moved sender",
|
||||||
|
"{response}"
|
||||||
|
);
|
||||||
|
let raw = |key: Vec<u8>| {
|
||||||
|
let store = test.server.store().clone();
|
||||||
|
async move {
|
||||||
|
store
|
||||||
|
.get_value::<String>(ValueKey::from(any(key)))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
raw(legacy_digest).await.is_none(),
|
||||||
|
"the old digest key is still there"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
raw(b"Ss".to_vec()).await.is_none(),
|
||||||
|
"the old settings key is still there"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
raw(b"Sr".to_vec()).await.as_deref(),
|
||||||
|
Some("3.0.2+2"),
|
||||||
|
"the spam rules marker was touched"
|
||||||
|
);
|
||||||
|
// Back to the default sender for what follows
|
||||||
|
call(
|
||||||
|
admin,
|
||||||
|
"inbuxa:ScheduledReportSettings/set",
|
||||||
|
json!({"update": {"singleton": {"fromName": ""}}}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// --- The weekly digest every server has (RP-21) ------------------------
|
// --- The weekly digest every server has (RP-21) ------------------------
|
||||||
let list = reports(admin).await;
|
let list = reports(admin).await;
|
||||||
let digest = list
|
let digest = list
|
||||||
|
|||||||
Reference in new issue
Block a user