Skip to content

Commit 23ac87f

Browse files
committed
Semver compatibility checks (closes #844)
1 parent 553ee6a commit 23ac87f

File tree

5 files changed

+138
-51
lines changed

5 files changed

+138
-51
lines changed

crates/common/src/manager/boot.rs

+17-3
Original file line numberDiff line numberDiff line change
@@ -235,17 +235,17 @@ impl BootManager {
235235
)));
236236
}
237237

238-
// Download SPAM filters if missing
238+
// Download Spam filter rules if missing
239239
if config
240240
.value("version.spam-filter")
241241
.filter(|v| !v.is_empty())
242242
.is_none()
243243
{
244-
match manager.fetch_config_resource("spam-filter").await {
244+
match manager.fetch_spam_rules().await {
245245
Ok(external_config) => {
246246
trc::event!(
247247
Config(trc::ConfigEvent::ImportExternal),
248-
Version = external_config.version,
248+
Version = external_config.version.to_string(),
249249
Id = "spam-filter"
250250
);
251251
insert_keys.extend(external_config.keys);
@@ -362,6 +362,20 @@ impl BootManager {
362362
}
363363
}
364364

365+
// Spam filter auto-update
366+
if config
367+
.property_or_default::<bool>("spam-filter.auto-update", "false")
368+
.unwrap_or_default()
369+
{
370+
if let Err(err) = core.storage.config.update_spam_rules(false).await {
371+
trc::event!(
372+
Resource(trc::ResourceEvent::Error),
373+
Details = "Failed to update spam-filter",
374+
CausedBy = err
375+
);
376+
}
377+
}
378+
365379
// Build shared inner
366380
let (ipc, ipc_rxs) = build_ipc();
367381
let inner = Arc::new(Inner {

crates/common/src/manager/config.rs

+46-44
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use trc::AddContext;
2020
use utils::{
2121
config::{Config, ConfigKey},
2222
glob::GlobPattern,
23+
Semver,
2324
};
2425

2526
#[derive(Default)]
@@ -50,9 +51,8 @@ pub enum MatchType {
5051
All,
5152
}
5253

53-
pub(crate) struct ExternalConfig {
54-
pub id: String,
55-
pub version: String,
54+
pub(crate) struct ExternalSpamRules {
55+
pub version: Semver,
5656
pub keys: Vec<ConfigKey>,
5757
}
5858

@@ -329,89 +329,91 @@ impl ConfigManager {
329329
})
330330
}
331331

332-
pub async fn update_config_resource(
333-
&self,
334-
resource_id: &str,
335-
overwrite: bool,
336-
) -> trc::Result<Option<String>> {
337-
let external = self
338-
.fetch_config_resource(resource_id)
339-
.await
340-
.map_err(|reason| {
341-
trc::EventType::Config(trc::ConfigEvent::FetchError)
342-
.caused_by(trc::location!())
343-
.details("Failed to fetch external configuration")
344-
.ctx(trc::Key::Reason, reason)
345-
})?;
346-
347-
if self
348-
.get(&external.id)
349-
.await?
350-
.map_or(true, |v| v != external.version)
351-
{
332+
pub async fn update_spam_rules(&self, overwrite: bool) -> trc::Result<Option<Semver>> {
333+
let external = self.fetch_spam_rules().await.map_err(|reason| {
334+
trc::EventType::Config(trc::ConfigEvent::FetchError)
335+
.caused_by(trc::location!())
336+
.details("Failed to update spam filter rules")
337+
.ctx(trc::Key::Reason, reason)
338+
})?;
339+
340+
if self.get("version.spam-filter").await?.map_or(true, |v| {
341+
v.as_str().try_into().map_or(true, |v| external.version > v)
342+
}) {
343+
// Delete previous STWT_* rules
344+
for prefix in [
345+
"spam-filter.rule.stwt_",
346+
"spam-filter.dnsbl.server.stwt_",
347+
"http-lookup.stwt_",
348+
] {
349+
self.clear_prefix(prefix).await?;
350+
}
351+
352352
self.set(external.keys, overwrite).await?;
353353

354354
trc::event!(
355355
Config(trc::ConfigEvent::ImportExternal),
356-
Version = external.version.clone(),
357-
Id = resource_id.to_string(),
356+
Version = external.version.to_string(),
357+
Id = "spam-filter",
358358
);
359359

360360
Ok(Some(external.version))
361361
} else {
362362
trc::event!(
363363
Config(trc::ConfigEvent::AlreadyUpToDate),
364-
Version = external.version,
365-
Id = resource_id.to_string(),
364+
Version = external.version.to_string(),
365+
Id = "spam-filter",
366366
);
367367

368368
Ok(None)
369369
}
370370
}
371371

372-
pub(crate) async fn fetch_config_resource(
373-
&self,
374-
resource_id: &str,
375-
) -> Result<ExternalConfig, String> {
376-
let config = String::from_utf8(self.fetch_resource(resource_id).await?)
372+
pub(crate) async fn fetch_spam_rules(&self) -> Result<ExternalSpamRules, String> {
373+
let config = String::from_utf8(self.fetch_resource("spam-filter").await?)
377374
.map_err(|err| format!("Configuration file has invalid UTF-8: {err}"))?;
378375
let config = Config::new(config)
379376
.map_err(|err| format!("Failed to parse external configuration: {err}"))?;
380377

381378
// Import configuration
382-
let mut external = ExternalConfig {
383-
id: String::new(),
384-
version: String::new(),
379+
let mut external = ExternalSpamRules {
380+
version: Semver::default(),
385381
keys: Vec::new(),
386382
};
383+
let mut required_semver = Semver::default();
384+
let server_semver: Semver = env!("CARGO_PKG_VERSION").try_into().unwrap();
387385
for (key, value) in config.keys {
388-
if key.starts_with("version.") {
389-
external.id.clone_from(&key);
390-
external.version.clone_from(&value);
386+
if key == "version.spam-filter" {
387+
external.version = value.as_str().try_into().unwrap_or_default();
391388
external.keys.push(ConfigKey::from((key, value)));
389+
} else if key == "version.server" {
390+
required_semver = value.as_str().try_into().unwrap_or_default();
392391
} else if key.starts_with("spam-filter.")
393392
|| key.starts_with("http-lookup.")
394393
|| (key.starts_with("lookup.") && !key.starts_with("lookup.default."))
395394
|| key.starts_with("server.asn.")
396-
|| key.starts_with("queue.quota.")
397-
|| key.starts_with("queue.throttle.")
398-
|| key.starts_with("session.throttle.")
399395
{
400396
external.keys.push(ConfigKey::from((key, value)));
401397
} else {
402398
trc::event!(
403399
Config(trc::ConfigEvent::ExternalKeyIgnored),
404400
Key = key,
405401
Value = value,
406-
Id = resource_id.to_string(),
402+
Id = "spam-filter",
407403
);
408404
}
409405
}
410406

411-
if !external.version.is_empty() {
407+
if !required_semver.is_valid() {
408+
Err("External spam filter rules do not contain a valid server version".to_string())
409+
} else if required_semver > server_semver {
410+
Err(format!(
411+
"External spam filter rules require server version {required_semver}, but this is version {server_semver}",
412+
))
413+
} else if external.version.is_valid() {
412414
Ok(external)
413415
} else {
414-
Err("External configuration file does not contain a version key".to_string())
416+
Err("External spam filter rules do not contain a version key".to_string())
415417
}
416418
}
417419

crates/jmap/src/api/management/reload.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,13 @@ impl ManageReload for Server {
129129
let overwrite = UrlParams::new(req.uri().query()).has_key("overwrite");
130130

131131
Ok(JsonResponse::new(json!({
132-
"data": self
132+
"data": self
133133
.core
134134
.storage
135135
.config
136-
.update_config_resource("spam-filter", overwrite)
137-
.await?,
136+
.update_spam_rules(overwrite)
137+
.await?
138+
.map(|v| v.to_string()),
138139
}))
139140
.into_http_response())
140141
}

crates/jmap/src/auth/oauth/auth.rs

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ impl OAuthApiHandler for Server {
135135
"data": {
136136
"code": client_code,
137137
"permissions": access_token.permissions(),
138+
"version": env!("CARGO_PKG_VERSION"),
138139
"isEnterprise": is_enterprise,
139140
},
140141
})

crates/utils/src/lib.rs

+70-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
55
*/
66

7-
use std::sync::Arc;
7+
use std::{fmt::Display, sync::Arc};
88

99
pub mod cache;
1010
pub mod codec;
@@ -122,6 +122,75 @@ impl HttpLimitResponse for Response {
122122
}
123123
}
124124

125+
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
126+
#[repr(transparent)]
127+
pub struct Semver(u64);
128+
129+
impl Semver {
130+
pub fn new(major: u16, minor: u16, patch: u16) -> Self {
131+
let mut version: u64 = 0;
132+
version |= (major as u64) << 32;
133+
version |= (minor as u64) << 16;
134+
version |= patch as u64;
135+
Semver(version)
136+
}
137+
138+
pub fn unpack(&self) -> (u16, u16, u16) {
139+
let version = self.0;
140+
let major = ((version >> 32) & 0xFFFF) as u16;
141+
let minor = ((version >> 16) & 0xFFFF) as u16;
142+
let patch = (version & 0xFFFF) as u16;
143+
(major, minor, patch)
144+
}
145+
146+
pub fn major(&self) -> u16 {
147+
(self.0 >> 32) as u16
148+
}
149+
150+
pub fn minor(&self) -> u16 {
151+
(self.0 >> 16) as u16
152+
}
153+
154+
pub fn patch(&self) -> u16 {
155+
self.0 as u16
156+
}
157+
158+
pub fn is_valid(&self) -> bool {
159+
self.0 > 0
160+
}
161+
}
162+
163+
impl AsRef<u64> for Semver {
164+
fn as_ref(&self) -> &u64 {
165+
&self.0
166+
}
167+
}
168+
169+
impl From<u64> for Semver {
170+
fn from(value: u64) -> Self {
171+
Semver(value)
172+
}
173+
}
174+
175+
impl TryFrom<&str> for Semver {
176+
type Error = ();
177+
178+
fn try_from(value: &str) -> Result<Self, Self::Error> {
179+
let mut parts = value.splitn(3, '.');
180+
let major = parts.next().ok_or(())?.parse().map_err(|_| ())?;
181+
let minor = parts.next().ok_or(())?.parse().map_err(|_| ())?;
182+
let patch = parts.next().ok_or(())?.parse().map_err(|_| ())?;
183+
Ok(Semver::new(major, minor, patch))
184+
}
185+
}
186+
187+
impl Display for Semver {
188+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189+
let (major, minor, patch) = self.unpack();
190+
write!(f, "{major}.{minor}.{patch}")
191+
}
192+
}
193+
125194
pub trait UnwrapFailure<T> {
126195
fn failed(self, action: &str) -> T;
127196
}

0 commit comments

Comments
 (0)