Merge pull request 'DLP: how long held mail waits is a setting' (#111) from feature/dlp-hold-days into main
This commit was merged in pull request #111.
This commit is contained in:
@@ -25,14 +25,42 @@ use trc::AddContext;
|
|||||||
|
|
||||||
const FEATURE: u8 = b'R';
|
const FEATURE: u8 = b'R';
|
||||||
const KIND_HELD: u8 = b'h';
|
const KIND_HELD: u8 = b'h';
|
||||||
|
const KIND_SETTINGS: u8 = b's';
|
||||||
|
|
||||||
/// How far off a held message's release is set: a century, so it never
|
/// How far off a held message's release is set: a century, so it never
|
||||||
/// comes due on its own.
|
/// comes due on its own.
|
||||||
pub const HOLD_SECONDS: u64 = 100 * 365 * 24 * 60 * 60;
|
pub const HOLD_SECONDS: u64 = 100 * 365 * 24 * 60 * 60;
|
||||||
|
|
||||||
/// How long unreviewed mail waits before it's rejected (settled answer 5).
|
/// How long unreviewed mail waits before it's rejected, unless the setting
|
||||||
|
/// says otherwise (settled answer 5).
|
||||||
pub const KEEP_DAYS: u64 = 7;
|
pub const KEEP_DAYS: u64 = 7;
|
||||||
|
|
||||||
|
/// `inbuxa:DlpSettings`: how many days held mail waits for a reviewer.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerdeSerialize, SerdeDeserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Settings {
|
||||||
|
pub keep_held_days: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Settings {
|
||||||
|
keep_held_days: KEEP_DAYS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
/// The property at fault and why, or fine.
|
||||||
|
pub fn check(&self) -> Result<(), (&'static str, &'static str)> {
|
||||||
|
if (1..=90).contains(&self.keep_held_days) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(("keepHeldDays", "must be from 1 to 90 days"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A rule that held the message, with its notice.
|
/// A rule that held the message, with its notice.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, SerdeSerialize, SerdeDeserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, SerdeSerialize, SerdeDeserialize)]
|
||||||
pub struct HeldRule {
|
pub struct HeldRule {
|
||||||
@@ -59,6 +87,13 @@ pub struct Held {
|
|||||||
/// Seconds since the epoch.
|
/// Seconds since the epoch.
|
||||||
pub held_at: u64,
|
pub held_at: u64,
|
||||||
pub expires_at: u64,
|
pub expires_at: u64,
|
||||||
|
/// The days it was given, for what the sender is told.
|
||||||
|
#[serde(default = "default_keep_days")]
|
||||||
|
pub keep_days: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_keep_days() -> u64 {
|
||||||
|
KEEP_DAYS
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Held {
|
impl Held {
|
||||||
@@ -106,6 +141,31 @@ fn key(queue_id: u64) -> ValueKey<ValueClass> {
|
|||||||
ValueKey::from(class(queue_id))
|
ValueKey::from(class(queue_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn settings_class() -> ValueClass {
|
||||||
|
ValueClass::Any(AnyClass {
|
||||||
|
subspace: SUBSPACE_INBUXA,
|
||||||
|
key: vec![FEATURE, KIND_SETTINGS],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn settings(data: &Store) -> trc::Result<Settings> {
|
||||||
|
Ok(data
|
||||||
|
.get_value::<Json<Settings>>(ValueKey::from(settings_class()))
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?
|
||||||
|
.map(|Json(settings)| settings)
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_settings(data: &Store, settings: &Settings) -> trc::Result<()> {
|
||||||
|
let mut batch = BatchBuilder::new();
|
||||||
|
batch.set(settings_class(), Json(settings).serialize()?);
|
||||||
|
data.write(batch.build_all())
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get(data: &Store, queue_id: u64) -> trc::Result<Option<Held>> {
|
pub async fn get(data: &Store, queue_id: u64) -> trc::Result<Option<Held>> {
|
||||||
Ok(data
|
Ok(data
|
||||||
.get_value::<Json<Held>>(key(queue_id))
|
.get_value::<Json<Held>>(key(queue_id))
|
||||||
@@ -172,6 +232,7 @@ mod tests {
|
|||||||
counts: vec![("payment-card".into(), 5)],
|
counts: vec![("payment-card".into(), 5)],
|
||||||
held_at: 1_000,
|
held_at: 1_000,
|
||||||
expires_at: 1_000 + KEEP_DAYS * 86_400,
|
expires_at: 1_000 + KEEP_DAYS * 86_400,
|
||||||
|
keep_days: KEEP_DAYS,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_value(&held).unwrap();
|
let json = serde_json::to_value(&held).unwrap();
|
||||||
assert_eq!(json["heldAt"], 1_000);
|
assert_eq!(json["heldAt"], 1_000);
|
||||||
@@ -180,4 +241,13 @@ mod tests {
|
|||||||
assert!(held.is_expired(1_000 + KEEP_DAYS * 86_400));
|
assert!(held.is_expired(1_000 + KEEP_DAYS * 86_400));
|
||||||
assert!(HOLD_SECONDS > 90 * 365 * 86_400);
|
assert!(HOLD_SECONDS > 90 * 365 * 86_400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_range() {
|
||||||
|
assert_eq!(Settings::default().keep_held_days, 7);
|
||||||
|
assert!(Settings { keep_held_days: 1 }.check().is_ok());
|
||||||
|
assert!(Settings { keep_held_days: 90 }.check().is_ok());
|
||||||
|
assert!(Settings { keep_held_days: 0 }.check().is_err());
|
||||||
|
assert!(Settings { keep_held_days: 91 }.check().is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! `inbuxa:DlpSettings/get` and `/set` under `urn:inbuxa:jmap`: the DLP
|
||||||
|
//! settings singleton (dlp-and-mail-flow-rules spec, §2.6): how many days
|
||||||
|
//! held mail waits for a reviewer before it goes back to the sender.
|
||||||
|
|
||||||
|
use crate::object::{AnyId, JmapObject, JmapObjectId};
|
||||||
|
use jmap_tools::{Element, Key, Property};
|
||||||
|
use std::{borrow::Cow, str::FromStr};
|
||||||
|
use types::id::Id;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct DlpSettings;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum DlpSettingsProperty {
|
||||||
|
Id,
|
||||||
|
KeepHeldDays,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum DlpSettingsValue {
|
||||||
|
Id(Id),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Property for DlpSettingsProperty {
|
||||||
|
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||||
|
DlpSettingsProperty::parse(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_cow(&self) -> Cow<'static, str> {
|
||||||
|
match self {
|
||||||
|
DlpSettingsProperty::Id => "id",
|
||||||
|
DlpSettingsProperty::KeepHeldDays => "keepHeldDays",
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DlpSettingsProperty {
|
||||||
|
fn parse(value: &str) -> Option<Self> {
|
||||||
|
hashify::tiny_map!(value.as_bytes(),
|
||||||
|
b"id" => DlpSettingsProperty::Id,
|
||||||
|
b"keepHeldDays" => DlpSettingsProperty::KeepHeldDays,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for DlpSettingsProperty {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
DlpSettingsProperty::parse(s).ok_or(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for DlpSettingsValue {
|
||||||
|
type Property = DlpSettingsProperty;
|
||||||
|
|
||||||
|
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||||
|
match key {
|
||||||
|
Key::Property(DlpSettingsProperty::Id) => {
|
||||||
|
Id::from_str(value).ok().map(DlpSettingsValue::Id)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_cow(&self) -> Cow<'static, str> {
|
||||||
|
match self {
|
||||||
|
DlpSettingsValue::Id(id) => id.to_string().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JmapObject for DlpSettings {
|
||||||
|
type Property = DlpSettingsProperty;
|
||||||
|
|
||||||
|
type Element = DlpSettingsValue;
|
||||||
|
|
||||||
|
type Id = Id;
|
||||||
|
|
||||||
|
type Filter = ();
|
||||||
|
|
||||||
|
type Comparator = ();
|
||||||
|
|
||||||
|
type GetArguments = ();
|
||||||
|
|
||||||
|
type SetArguments<'de> = ();
|
||||||
|
|
||||||
|
type QueryArguments = ();
|
||||||
|
|
||||||
|
type CopyArguments = ();
|
||||||
|
|
||||||
|
type ParseArguments = ();
|
||||||
|
|
||||||
|
const ID_PROPERTY: Self::Property = DlpSettingsProperty::Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Id> for DlpSettingsValue {
|
||||||
|
fn from(id: Id) -> Self {
|
||||||
|
DlpSettingsValue::Id(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JmapObjectId for DlpSettingsValue {
|
||||||
|
fn as_id(&self) -> Option<Id> {
|
||||||
|
match self {
|
||||||
|
DlpSettingsValue::Id(id) => Some(*id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_id(&self) -> Option<AnyId> {
|
||||||
|
match self {
|
||||||
|
DlpSettingsValue::Id(id) => Some(AnyId::Id(*id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_id_ref(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||||
|
if let AnyId::Id(id) = new_id {
|
||||||
|
*self = DlpSettingsValue::Id(id);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JmapObjectId for DlpSettingsProperty {
|
||||||
|
fn as_id(&self) -> Option<Id> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_id(&self) -> Option<AnyId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_id_ref(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ pub mod fastmail_masked_email; // inbuxa: masked email
|
|||||||
pub mod inbuxa_account_lock; // inbuxa: account lock with delegation
|
pub mod inbuxa_account_lock; // inbuxa: account lock with delegation
|
||||||
pub mod inbuxa_ai_limits; // inbuxa: AI spam classification
|
pub mod inbuxa_ai_limits; // inbuxa: AI spam classification
|
||||||
pub mod inbuxa_log_settings; // inbuxa: personal-data catalog, D1
|
pub mod inbuxa_log_settings; // inbuxa: personal-data catalog, D1
|
||||||
|
pub mod inbuxa_dlp_settings; // inbuxa: DLP settings
|
||||||
pub mod inbuxa_data_inventory; // inbuxa: personal-data catalog
|
pub mod inbuxa_data_inventory; // inbuxa: personal-data catalog
|
||||||
pub mod inbuxa_inventory_snapshot; // inbuxa: personal-data catalog
|
pub mod inbuxa_inventory_snapshot; // inbuxa: personal-data catalog
|
||||||
pub mod inbuxa_audit; // inbuxa: the audit log
|
pub mod inbuxa_audit; // inbuxa: the audit log
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ impl Response<'_> {
|
|||||||
GetResponseMethod::LogSettings(response) => {
|
GetResponseMethod::LogSettings(response) => {
|
||||||
response.eval_jptr(path, &mut results)
|
response.eval_jptr(path, &mut results)
|
||||||
}
|
}
|
||||||
|
GetResponseMethod::DlpSettings(response) => {
|
||||||
|
response.eval_jptr(path, &mut results)
|
||||||
|
}
|
||||||
GetResponseMethod::DataInventory(response) => {
|
GetResponseMethod::DataInventory(response) => {
|
||||||
response.eval_jptr(path, &mut results)
|
response.eval_jptr(path, &mut results)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ impl Response<'_> {
|
|||||||
GetRequestMethod::DeletedAccount(request) => request.resolve_references(self)?,
|
GetRequestMethod::DeletedAccount(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::AiLimits(request) => request.resolve_references(self)?,
|
GetRequestMethod::AiLimits(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::LogSettings(request) => request.resolve_references(self)?,
|
GetRequestMethod::LogSettings(request) => request.resolve_references(self)?,
|
||||||
|
GetRequestMethod::DlpSettings(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::DataInventory(request) => request.resolve_references(self)?,
|
GetRequestMethod::DataInventory(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::InventorySnapshot(request) => request.resolve_references(self)?,
|
GetRequestMethod::InventorySnapshot(request) => request.resolve_references(self)?,
|
||||||
GetRequestMethod::AuditEvent(request) => request.resolve_references(self)?,
|
GetRequestMethod::AuditEvent(request) => request.resolve_references(self)?,
|
||||||
@@ -106,6 +107,9 @@ impl Response<'_> {
|
|||||||
SetRequestMethod::LogSettings(request) => {
|
SetRequestMethod::LogSettings(request) => {
|
||||||
request.resolve_references(self, 1, false)?
|
request.resolve_references(self, 1, false)?
|
||||||
}
|
}
|
||||||
|
SetRequestMethod::DlpSettings(request) => {
|
||||||
|
request.resolve_references(self, 1, false)?
|
||||||
|
}
|
||||||
SetRequestMethod::Explanation(request) => {
|
SetRequestMethod::Explanation(request) => {
|
||||||
request.resolve_references(self, 1, false)?
|
request.resolve_references(self, 1, false)?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ pub enum MethodObject {
|
|||||||
// inbuxa: AI call limits
|
// inbuxa: AI call limits
|
||||||
AiLimits,
|
AiLimits,
|
||||||
LogSettings,
|
LogSettings,
|
||||||
|
DlpSettings,
|
||||||
DataInventory,
|
DataInventory,
|
||||||
InventorySnapshot,
|
InventorySnapshot,
|
||||||
// inbuxa: "Explain this" with the local model
|
// inbuxa: "Explain this" with the local model
|
||||||
@@ -96,6 +97,7 @@ impl MethodObject {
|
|||||||
MethodObject::DeletedAccount => Capability::Inbuxa,
|
MethodObject::DeletedAccount => Capability::Inbuxa,
|
||||||
MethodObject::AiLimits => Capability::Inbuxa,
|
MethodObject::AiLimits => Capability::Inbuxa,
|
||||||
MethodObject::LogSettings => Capability::Inbuxa,
|
MethodObject::LogSettings => Capability::Inbuxa,
|
||||||
|
MethodObject::DlpSettings => Capability::Inbuxa,
|
||||||
MethodObject::DataInventory => Capability::Inbuxa,
|
MethodObject::DataInventory => Capability::Inbuxa,
|
||||||
MethodObject::InventorySnapshot => Capability::Inbuxa,
|
MethodObject::InventorySnapshot => Capability::Inbuxa,
|
||||||
MethodObject::Explanation => Capability::Inbuxa,
|
MethodObject::Explanation => Capability::Inbuxa,
|
||||||
@@ -288,9 +290,11 @@ impl MethodName {
|
|||||||
(MethodFunction::Get, MethodObject::AiLimits) => "inbuxa:AiLimits/get",
|
(MethodFunction::Get, MethodObject::AiLimits) => "inbuxa:AiLimits/get",
|
||||||
(MethodFunction::Set, MethodObject::AiLimits) => "inbuxa:AiLimits/set",
|
(MethodFunction::Set, MethodObject::AiLimits) => "inbuxa:AiLimits/set",
|
||||||
(MethodFunction::Get, MethodObject::LogSettings) => "inbuxa:LogSettings/get",
|
(MethodFunction::Get, MethodObject::LogSettings) => "inbuxa:LogSettings/get",
|
||||||
|
(MethodFunction::Get, MethodObject::DlpSettings) => "inbuxa:DlpSettings/get",
|
||||||
(MethodFunction::Get, MethodObject::DataInventory) => "inbuxa:DataInventory/get",
|
(MethodFunction::Get, MethodObject::DataInventory) => "inbuxa:DataInventory/get",
|
||||||
(MethodFunction::Get, MethodObject::InventorySnapshot) => "inbuxa:InventorySnapshot/get",
|
(MethodFunction::Get, MethodObject::InventorySnapshot) => "inbuxa:InventorySnapshot/get",
|
||||||
(MethodFunction::Set, MethodObject::LogSettings) => "inbuxa:LogSettings/set",
|
(MethodFunction::Set, MethodObject::LogSettings) => "inbuxa:LogSettings/set",
|
||||||
|
(MethodFunction::Set, MethodObject::DlpSettings) => "inbuxa:DlpSettings/set",
|
||||||
(MethodFunction::Set, MethodObject::Explanation) => "inbuxa:Explanation/set",
|
(MethodFunction::Set, MethodObject::Explanation) => "inbuxa:Explanation/set",
|
||||||
(MethodFunction::Get, MethodObject::AuditEvent) => "inbuxa:AuditEvent/get",
|
(MethodFunction::Get, MethodObject::AuditEvent) => "inbuxa:AuditEvent/get",
|
||||||
(MethodFunction::Query, MethodObject::AuditEvent) => "inbuxa:AuditEvent/query",
|
(MethodFunction::Query, MethodObject::AuditEvent) => "inbuxa:AuditEvent/query",
|
||||||
@@ -444,9 +448,11 @@ impl MethodName {
|
|||||||
"inbuxa:AiLimits/get" => (MethodObject::AiLimits, MethodFunction::Get),
|
"inbuxa:AiLimits/get" => (MethodObject::AiLimits, MethodFunction::Get),
|
||||||
"inbuxa:AiLimits/set" => (MethodObject::AiLimits, MethodFunction::Set),
|
"inbuxa:AiLimits/set" => (MethodObject::AiLimits, MethodFunction::Set),
|
||||||
"inbuxa:LogSettings/get" => (MethodObject::LogSettings, MethodFunction::Get),
|
"inbuxa:LogSettings/get" => (MethodObject::LogSettings, MethodFunction::Get),
|
||||||
|
"inbuxa:DlpSettings/get" => (MethodObject::DlpSettings, MethodFunction::Get),
|
||||||
"inbuxa:DataInventory/get" => (MethodObject::DataInventory, MethodFunction::Get),
|
"inbuxa:DataInventory/get" => (MethodObject::DataInventory, MethodFunction::Get),
|
||||||
"inbuxa:InventorySnapshot/get" => (MethodObject::InventorySnapshot, MethodFunction::Get),
|
"inbuxa:InventorySnapshot/get" => (MethodObject::InventorySnapshot, MethodFunction::Get),
|
||||||
"inbuxa:LogSettings/set" => (MethodObject::LogSettings, MethodFunction::Set),
|
"inbuxa:LogSettings/set" => (MethodObject::LogSettings, MethodFunction::Set),
|
||||||
|
"inbuxa:DlpSettings/set" => (MethodObject::DlpSettings, MethodFunction::Set),
|
||||||
"inbuxa:Explanation/set" => (MethodObject::Explanation, MethodFunction::Set),
|
"inbuxa:Explanation/set" => (MethodObject::Explanation, MethodFunction::Set),
|
||||||
"inbuxa:AuditEvent/get" => (MethodObject::AuditEvent, MethodFunction::Get),
|
"inbuxa:AuditEvent/get" => (MethodObject::AuditEvent, MethodFunction::Get),
|
||||||
"inbuxa:AuditEvent/query" => (MethodObject::AuditEvent, MethodFunction::Query),
|
"inbuxa:AuditEvent/query" => (MethodObject::AuditEvent, MethodFunction::Query),
|
||||||
@@ -522,6 +528,7 @@ impl Display for MethodObject {
|
|||||||
MethodObject::DeletedAccount => "inbuxa:DeletedAccount",
|
MethodObject::DeletedAccount => "inbuxa:DeletedAccount",
|
||||||
MethodObject::AiLimits => "inbuxa:AiLimits",
|
MethodObject::AiLimits => "inbuxa:AiLimits",
|
||||||
MethodObject::LogSettings => "inbuxa:LogSettings",
|
MethodObject::LogSettings => "inbuxa:LogSettings",
|
||||||
|
MethodObject::DlpSettings => "inbuxa:DlpSettings",
|
||||||
MethodObject::DataInventory => "inbuxa:DataInventory",
|
MethodObject::DataInventory => "inbuxa:DataInventory",
|
||||||
MethodObject::InventorySnapshot => "inbuxa:InventorySnapshot",
|
MethodObject::InventorySnapshot => "inbuxa:InventorySnapshot",
|
||||||
MethodObject::Explanation => "inbuxa:Explanation",
|
MethodObject::Explanation => "inbuxa:Explanation",
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ pub enum GetRequestMethod {
|
|||||||
DeletedAccount(Box<GetRequest<crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
DeletedAccount(Box<GetRequest<crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||||
AiLimits(Box<GetRequest<crate::object::inbuxa_ai_limits::AiLimits>>),
|
AiLimits(Box<GetRequest<crate::object::inbuxa_ai_limits::AiLimits>>),
|
||||||
LogSettings(Box<GetRequest<crate::object::inbuxa_log_settings::LogSettings>>),
|
LogSettings(Box<GetRequest<crate::object::inbuxa_log_settings::LogSettings>>),
|
||||||
|
DlpSettings(Box<GetRequest<crate::object::inbuxa_dlp_settings::DlpSettings>>),
|
||||||
DataInventory(Box<GetRequest<crate::object::inbuxa_data_inventory::DataInventory>>),
|
DataInventory(Box<GetRequest<crate::object::inbuxa_data_inventory::DataInventory>>),
|
||||||
InventorySnapshot(Box<GetRequest<crate::object::inbuxa_inventory_snapshot::InventorySnapshot>>),
|
InventorySnapshot(Box<GetRequest<crate::object::inbuxa_inventory_snapshot::InventorySnapshot>>),
|
||||||
AuditEvent(Box<GetRequest<crate::object::inbuxa_audit::AuditEvent>>),
|
AuditEvent(Box<GetRequest<crate::object::inbuxa_audit::AuditEvent>>),
|
||||||
@@ -154,6 +155,7 @@ pub enum SetRequestMethod<'x> {
|
|||||||
DeletedAccount(Box<SetRequest<'x, crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
DeletedAccount(Box<SetRequest<'x, crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||||
AiLimits(Box<SetRequest<'x, crate::object::inbuxa_ai_limits::AiLimits>>),
|
AiLimits(Box<SetRequest<'x, crate::object::inbuxa_ai_limits::AiLimits>>),
|
||||||
LogSettings(Box<SetRequest<'x, crate::object::inbuxa_log_settings::LogSettings>>),
|
LogSettings(Box<SetRequest<'x, crate::object::inbuxa_log_settings::LogSettings>>),
|
||||||
|
DlpSettings(Box<SetRequest<'x, crate::object::inbuxa_dlp_settings::DlpSettings>>),
|
||||||
Explanation(Box<SetRequest<'x, crate::object::inbuxa_explanation::Explanation>>),
|
Explanation(Box<SetRequest<'x, crate::object::inbuxa_explanation::Explanation>>),
|
||||||
AuditSettings(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditSettings>>),
|
AuditSettings(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditSettings>>),
|
||||||
AuditExport(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditExport>>),
|
AuditExport(Box<SetRequest<'x, crate::object::inbuxa_audit::AuditExport>>),
|
||||||
|
|||||||
@@ -176,6 +176,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
|||||||
return Err(de::Error::invalid_length(1, &self));
|
return Err(de::Error::invalid_length(1, &self));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
(MethodFunction::Get, MethodObject::DlpSettings) => match seq.next_element() {
|
||||||
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::DlpSettings(value)),
|
||||||
|
Err(err) => RequestMethod::invalid(err),
|
||||||
|
Ok(None) => {
|
||||||
|
return Err(de::Error::invalid_length(1, &self));
|
||||||
|
}
|
||||||
|
},
|
||||||
(MethodFunction::Get, MethodObject::DataInventory) => match seq.next_element() {
|
(MethodFunction::Get, MethodObject::DataInventory) => match seq.next_element() {
|
||||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::DataInventory(value)),
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::DataInventory(value)),
|
||||||
Err(err) => RequestMethod::invalid(err),
|
Err(err) => RequestMethod::invalid(err),
|
||||||
@@ -378,6 +385,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
|||||||
return Err(de::Error::invalid_length(1, &self));
|
return Err(de::Error::invalid_length(1, &self));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
(MethodFunction::Set, MethodObject::DlpSettings) => match seq.next_element() {
|
||||||
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::DlpSettings(value)),
|
||||||
|
Err(err) => RequestMethod::invalid(err),
|
||||||
|
Ok(None) => {
|
||||||
|
return Err(de::Error::invalid_length(1, &self));
|
||||||
|
}
|
||||||
|
},
|
||||||
(MethodFunction::Set, MethodObject::Explanation) => match seq.next_element() {
|
(MethodFunction::Set, MethodObject::Explanation) => match seq.next_element() {
|
||||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Explanation(value)),
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Explanation(value)),
|
||||||
Err(err) => RequestMethod::invalid(err),
|
Err(err) => RequestMethod::invalid(err),
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ pub enum GetResponseMethod {
|
|||||||
DeletedAccount(GetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>),
|
DeletedAccount(GetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>),
|
||||||
AiLimits(GetResponse<crate::object::inbuxa_ai_limits::AiLimits>),
|
AiLimits(GetResponse<crate::object::inbuxa_ai_limits::AiLimits>),
|
||||||
LogSettings(GetResponse<crate::object::inbuxa_log_settings::LogSettings>),
|
LogSettings(GetResponse<crate::object::inbuxa_log_settings::LogSettings>),
|
||||||
|
DlpSettings(GetResponse<crate::object::inbuxa_dlp_settings::DlpSettings>),
|
||||||
DataInventory(GetResponse<crate::object::inbuxa_data_inventory::DataInventory>),
|
DataInventory(GetResponse<crate::object::inbuxa_data_inventory::DataInventory>),
|
||||||
InventorySnapshot(GetResponse<crate::object::inbuxa_inventory_snapshot::InventorySnapshot>),
|
InventorySnapshot(GetResponse<crate::object::inbuxa_inventory_snapshot::InventorySnapshot>),
|
||||||
AuditEvent(GetResponse<crate::object::inbuxa_audit::AuditEvent>),
|
AuditEvent(GetResponse<crate::object::inbuxa_audit::AuditEvent>),
|
||||||
@@ -142,6 +143,7 @@ pub enum SetResponseMethod {
|
|||||||
DeletedAccount(Box<SetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
DeletedAccount(Box<SetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||||
AiLimits(Box<SetResponse<crate::object::inbuxa_ai_limits::AiLimits>>),
|
AiLimits(Box<SetResponse<crate::object::inbuxa_ai_limits::AiLimits>>),
|
||||||
LogSettings(Box<SetResponse<crate::object::inbuxa_log_settings::LogSettings>>),
|
LogSettings(Box<SetResponse<crate::object::inbuxa_log_settings::LogSettings>>),
|
||||||
|
DlpSettings(Box<SetResponse<crate::object::inbuxa_dlp_settings::DlpSettings>>),
|
||||||
AuditSettings(Box<SetResponse<crate::object::inbuxa_audit::AuditSettings>>),
|
AuditSettings(Box<SetResponse<crate::object::inbuxa_audit::AuditSettings>>),
|
||||||
AuditExport(Box<SetResponse<crate::object::inbuxa_audit::AuditExport>>),
|
AuditExport(Box<SetResponse<crate::object::inbuxa_audit::AuditExport>>),
|
||||||
AuditVerification(Box<SetResponse<crate::object::inbuxa_audit::AuditVerification>>),
|
AuditVerification(Box<SetResponse<crate::object::inbuxa_audit::AuditVerification>>),
|
||||||
@@ -363,6 +365,12 @@ impl<'x> From<GetResponse<crate::object::inbuxa_log_settings::LogSettings>> for
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'x> From<GetResponse<crate::object::inbuxa_dlp_settings::DlpSettings>> for ResponseMethod<'x> {
|
||||||
|
fn from(value: GetResponse<crate::object::inbuxa_dlp_settings::DlpSettings>) -> Self {
|
||||||
|
ResponseMethod::Get(GetResponseMethod::DlpSettings(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'x> From<GetResponse<crate::object::inbuxa_data_inventory::DataInventory>> for ResponseMethod<'x> {
|
impl<'x> From<GetResponse<crate::object::inbuxa_data_inventory::DataInventory>> for ResponseMethod<'x> {
|
||||||
fn from(value: GetResponse<crate::object::inbuxa_data_inventory::DataInventory>) -> Self {
|
fn from(value: GetResponse<crate::object::inbuxa_data_inventory::DataInventory>) -> Self {
|
||||||
ResponseMethod::Get(GetResponseMethod::DataInventory(value))
|
ResponseMethod::Get(GetResponseMethod::DataInventory(value))
|
||||||
@@ -387,6 +395,12 @@ impl<'x> From<SetResponse<crate::object::inbuxa_log_settings::LogSettings>> for
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'x> From<SetResponse<crate::object::inbuxa_dlp_settings::DlpSettings>> for ResponseMethod<'x> {
|
||||||
|
fn from(value: SetResponse<crate::object::inbuxa_dlp_settings::DlpSettings>) -> Self {
|
||||||
|
ResponseMethod::Set(SetResponseMethod::DlpSettings(Box::new(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'x> From<SetResponse<crate::object::inbuxa_explanation::Explanation>> for ResponseMethod<'x> {
|
impl<'x> From<SetResponse<crate::object::inbuxa_explanation::Explanation>> for ResponseMethod<'x> {
|
||||||
fn from(value: SetResponse<crate::object::inbuxa_explanation::Explanation>) -> Self {
|
fn from(value: SetResponse<crate::object::inbuxa_explanation::Explanation>) -> Self {
|
||||||
ResponseMethod::Set(SetResponseMethod::Explanation(Box::new(value)))
|
ResponseMethod::Set(SetResponseMethod::Explanation(Box::new(value)))
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ impl JmapAuthorization for AccessToken {
|
|||||||
GetRequestMethod::AiLimits(_) => Permission::SysSpamLlmGet,
|
GetRequestMethod::AiLimits(_) => Permission::SysSpamLlmGet,
|
||||||
// inbuxa: log file retention, with the tracers' permissions
|
// inbuxa: log file retention, with the tracers' permissions
|
||||||
GetRequestMethod::LogSettings(_) => Permission::SysTracerGet,
|
GetRequestMethod::LogSettings(_) => Permission::SysTracerGet,
|
||||||
|
GetRequestMethod::DlpSettings(_) => Permission::SysDlpPolicyGet,
|
||||||
// inbuxa: personal-data catalog, the inventory and its history
|
// inbuxa: personal-data catalog, the inventory and its history
|
||||||
GetRequestMethod::DataInventory(_) | GetRequestMethod::InventorySnapshot(_) => {
|
GetRequestMethod::DataInventory(_) | GetRequestMethod::InventorySnapshot(_) => {
|
||||||
Permission::SysComplianceGet
|
Permission::SysComplianceGet
|
||||||
@@ -227,6 +228,13 @@ impl JmapAuthorization for AccessToken {
|
|||||||
Permission::SysTracerUpdate,
|
Permission::SysTracerUpdate,
|
||||||
Permission::SysTracerUpdate,
|
Permission::SysTracerUpdate,
|
||||||
),
|
),
|
||||||
|
SetRequestMethod::DlpSettings(s) => validate_set(
|
||||||
|
s,
|
||||||
|
self,
|
||||||
|
Permission::SysDlpPolicyUpdate,
|
||||||
|
Permission::SysDlpPolicyUpdate,
|
||||||
|
Permission::SysDlpPolicyUpdate,
|
||||||
|
),
|
||||||
// inbuxa: the audit log (AU-7, AU-9, AU-11)
|
// inbuxa: the audit log (AU-7, AU-9, AU-11)
|
||||||
SetRequestMethod::AuditSettings(s) => validate_set(
|
SetRequestMethod::AuditSettings(s) => validate_set(
|
||||||
s,
|
s,
|
||||||
@@ -430,6 +438,7 @@ impl JmapAuthorization for AccessToken {
|
|||||||
| MethodObject::DeletedAccount
|
| MethodObject::DeletedAccount
|
||||||
| MethodObject::AiLimits
|
| MethodObject::AiLimits
|
||||||
| MethodObject::LogSettings
|
| MethodObject::LogSettings
|
||||||
|
| MethodObject::DlpSettings
|
||||||
| MethodObject::DataInventory
|
| MethodObject::DataInventory
|
||||||
| MethodObject::InventorySnapshot
|
| MethodObject::InventorySnapshot
|
||||||
| MethodObject::Explanation
|
| MethodObject::Explanation
|
||||||
|
|||||||
@@ -264,6 +264,9 @@ impl RequestHandler for Server {
|
|||||||
SetResponseMethod::LogSettings(set_response) => {
|
SetResponseMethod::LogSettings(set_response) => {
|
||||||
set_response.update_created_ids(&mut response);
|
set_response.update_created_ids(&mut response);
|
||||||
}
|
}
|
||||||
|
SetResponseMethod::DlpSettings(set_response) => {
|
||||||
|
set_response.update_created_ids(&mut response);
|
||||||
|
}
|
||||||
SetResponseMethod::AuditSettings(set_response) => {
|
SetResponseMethod::AuditSettings(set_response) => {
|
||||||
set_response.update_created_ids(&mut response);
|
set_response.update_created_ids(&mut response);
|
||||||
}
|
}
|
||||||
@@ -461,6 +464,13 @@ impl RequestHandler for Server {
|
|||||||
.await?
|
.await?
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
// inbuxa: inbuxa:DlpSettings/get
|
||||||
|
GetRequestMethod::DlpSettings(mut req) => {
|
||||||
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
|
crate::inbuxa::dlp_settings::get(self, access_token, *req)
|
||||||
|
.await?
|
||||||
|
.into()
|
||||||
|
}
|
||||||
// inbuxa: inbuxa:DataInventory/get
|
// inbuxa: inbuxa:DataInventory/get
|
||||||
GetRequestMethod::DataInventory(mut req) => {
|
GetRequestMethod::DataInventory(mut req) => {
|
||||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
@@ -826,6 +836,23 @@ impl RequestHandler for Server {
|
|||||||
.await?
|
.await?
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
// inbuxa: inbuxa:DlpSettings/set
|
||||||
|
SetRequestMethod::DlpSettings(mut req) => {
|
||||||
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
|
// inbuxa: AU-1.2, AU-3
|
||||||
|
crate::inbuxa::audit::recorded(
|
||||||
|
self,
|
||||||
|
access_token,
|
||||||
|
session,
|
||||||
|
&method_name.obj.to_string(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
*req,
|
||||||
|
|req| Box::pin(crate::inbuxa::dlp_settings::set(self, access_token, req)),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into()
|
||||||
|
}
|
||||||
// inbuxa: the audit log (AU-7, AU-11, AU-6)
|
// inbuxa: the audit log (AU-7, AU-11, AU-6)
|
||||||
SetRequestMethod::AuditSettings(mut req) => {
|
SetRequestMethod::AuditSettings(mut req) => {
|
||||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ impl IntermediateChangesResponse {
|
|||||||
| MethodObject::DeletedAccount
|
| MethodObject::DeletedAccount
|
||||||
| MethodObject::AiLimits
|
| MethodObject::AiLimits
|
||||||
| MethodObject::LogSettings
|
| MethodObject::LogSettings
|
||||||
|
| MethodObject::DlpSettings
|
||||||
| MethodObject::DataInventory
|
| MethodObject::DataInventory
|
||||||
| MethodObject::InventorySnapshot
|
| MethodObject::InventorySnapshot
|
||||||
| MethodObject::Explanation
|
| MethodObject::Explanation
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! `inbuxa:DlpSettings/get` and `/set`: how many days held mail waits for a
|
||||||
|
//! reviewer (dlp-and-mail-flow-rules spec, §2.6), 1 to 90, 7 by default.
|
||||||
|
//! Server-level, like the rules; applies to mail held from then on.
|
||||||
|
|
||||||
|
use common::{Server, auth::AccessToken};
|
||||||
|
use inbuxa_features::mailflow::held::{self, Settings};
|
||||||
|
use jmap_proto::{
|
||||||
|
error::set::SetError,
|
||||||
|
method::{
|
||||||
|
get::{GetRequest, GetResponse},
|
||||||
|
set::{SetRequest, SetResponse},
|
||||||
|
},
|
||||||
|
object::inbuxa_dlp_settings::{DlpSettings, DlpSettingsProperty as P, DlpSettingsValue},
|
||||||
|
request::IntoValid,
|
||||||
|
};
|
||||||
|
use jmap_tools::{Key, Map, Value};
|
||||||
|
use types::id::Id;
|
||||||
|
|
||||||
|
type LValue = Value<'static, P, DlpSettingsValue>;
|
||||||
|
|
||||||
|
const ALL: &[P] = &[P::Id, P::KeepHeldDays];
|
||||||
|
|
||||||
|
fn assert_server_level(access_token: &AccessToken) -> trc::Result<()> {
|
||||||
|
if access_token.tenant_id().is_some() {
|
||||||
|
Err(trc::JmapEvent::Forbidden
|
||||||
|
.into_err()
|
||||||
|
.details("DLP settings are server-level."))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_value(settings: &Settings, properties: &[P]) -> LValue {
|
||||||
|
let mut out = Map::with_capacity(properties.len());
|
||||||
|
for property in properties {
|
||||||
|
let value = match property {
|
||||||
|
P::Id => Value::Element(DlpSettingsValue::Id(Id::singleton())),
|
||||||
|
P::KeepHeldDays => Value::Number(settings.keep_held_days.into()),
|
||||||
|
};
|
||||||
|
out.insert_unchecked(Key::Property(property.clone()), value);
|
||||||
|
}
|
||||||
|
Value::Object(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `inbuxa:DlpSettings/get`.
|
||||||
|
pub async fn get(
|
||||||
|
server: &Server,
|
||||||
|
access_token: &AccessToken,
|
||||||
|
mut request: GetRequest<DlpSettings>,
|
||||||
|
) -> trc::Result<GetResponse<DlpSettings>> {
|
||||||
|
assert_server_level(access_token)?;
|
||||||
|
let properties = request.unwrap_properties(ALL);
|
||||||
|
let (ids, not_found) = request.unwrap_ids(1)?;
|
||||||
|
let mut response = GetResponse {
|
||||||
|
account_id: request.account_id.into(),
|
||||||
|
state: None,
|
||||||
|
list: Vec::new(),
|
||||||
|
not_found,
|
||||||
|
};
|
||||||
|
let settings = held::settings(server.store()).await?;
|
||||||
|
match ids {
|
||||||
|
None => response.list.push(to_value(&settings, &properties)),
|
||||||
|
Some(ids) => {
|
||||||
|
for id in ids {
|
||||||
|
if id.is_singleton() {
|
||||||
|
response.list.push(to_value(&settings, &properties));
|
||||||
|
} else {
|
||||||
|
response.push_not_found(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(
|
||||||
|
settings: &mut Settings,
|
||||||
|
property: &P,
|
||||||
|
value: &Value<'_, P, DlpSettingsValue>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
match property {
|
||||||
|
P::KeepHeldDays => {
|
||||||
|
settings.keep_held_days = value
|
||||||
|
.as_u64()
|
||||||
|
.ok_or_else(|| "must be a whole number of days".to_string())?
|
||||||
|
}
|
||||||
|
P::Id => return Err("is immutable".to_string()),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `inbuxa:DlpSettings/set`: updates the singleton.
|
||||||
|
pub async fn set(
|
||||||
|
server: &Server,
|
||||||
|
access_token: &AccessToken,
|
||||||
|
mut request: SetRequest<'_, DlpSettings>,
|
||||||
|
) -> trc::Result<SetResponse<DlpSettings>> {
|
||||||
|
assert_server_level(access_token)?;
|
||||||
|
let mut response = SetResponse::from_request(&request, server.core.jmap.set_max_objects)?;
|
||||||
|
for (client_id, _) in request.unwrap_create() {
|
||||||
|
response
|
||||||
|
.not_created
|
||||||
|
.append(client_id, SetError::singleton());
|
||||||
|
}
|
||||||
|
for id in request.unwrap_destroy().into_valid() {
|
||||||
|
response.not_destroyed.append(id, SetError::singleton());
|
||||||
|
}
|
||||||
|
let data = server.store();
|
||||||
|
for (id, value) in request.unwrap_update().into_valid() {
|
||||||
|
if !id.is_singleton() {
|
||||||
|
response.not_updated.append(id, SetError::not_found());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut settings = held::settings(data).await?;
|
||||||
|
let mut error = None;
|
||||||
|
for (key, value) in value.into_expanded_object() {
|
||||||
|
let Key::Property(property) = &key else {
|
||||||
|
error = Some(SetError::invalid_properties().with_property(key.into_owned()));
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if let Err(why) = apply(&mut settings, property, &value) {
|
||||||
|
error = Some(
|
||||||
|
SetError::invalid_properties()
|
||||||
|
.with_property(property.clone())
|
||||||
|
.with_description(why),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if error.is_none()
|
||||||
|
&& let Err((property, why)) = settings.check()
|
||||||
|
{
|
||||||
|
error = Some(
|
||||||
|
SetError::invalid_properties()
|
||||||
|
.with_property(property.parse::<P>().unwrap_or(P::Id))
|
||||||
|
.with_description(format!("{property} {why}.")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
match error {
|
||||||
|
Some(error) => response.not_updated.append(id, error),
|
||||||
|
None => {
|
||||||
|
held::set_settings(data, &settings).await?;
|
||||||
|
response.updated.append(id, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ pub mod account_lock;
|
|||||||
pub mod legal_hold;
|
pub mod legal_hold;
|
||||||
pub mod mail_rule;
|
pub mod mail_rule;
|
||||||
pub mod held_message;
|
pub mod held_message;
|
||||||
|
pub mod dlp_settings;
|
||||||
pub mod hold_export;
|
pub mod hold_export;
|
||||||
pub mod hold_export_api;
|
pub mod hold_export_api;
|
||||||
pub mod audit;
|
pub mod audit;
|
||||||
|
|||||||
@@ -478,6 +478,9 @@ impl<T: SessionStream> Session<T> {
|
|||||||
size: u64,
|
size: u64,
|
||||||
) {
|
) {
|
||||||
let at = store::write::now();
|
let at = store::write::now();
|
||||||
|
let keep_days = held::settings(self.server.store())
|
||||||
|
.await
|
||||||
|
.map_or(KEEP_DAYS, |s| s.keep_held_days);
|
||||||
let account = self.data.authenticated_as.as_ref();
|
let account = self.data.authenticated_as.as_ref();
|
||||||
let record = Held {
|
let record = Held {
|
||||||
queue_id,
|
queue_id,
|
||||||
@@ -490,7 +493,8 @@ impl<T: SessionStream> Session<T> {
|
|||||||
rules: draft.rules,
|
rules: draft.rules,
|
||||||
counts: draft.counts,
|
counts: draft.counts,
|
||||||
held_at: at,
|
held_at: at,
|
||||||
expires_at: at + KEEP_DAYS * 86_400,
|
expires_at: at + keep_days * 86_400,
|
||||||
|
keep_days,
|
||||||
};
|
};
|
||||||
if let Err(err) = held::create(self.server.store(), &record).await {
|
if let Err(err) = held::create(self.server.store(), &record).await {
|
||||||
trc::error!(
|
trc::error!(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use common::{
|
|||||||
};
|
};
|
||||||
use inbuxa_features::{
|
use inbuxa_features::{
|
||||||
audit::{Action, Actor, Outcome, Record, Target},
|
audit::{Action, Actor, Outcome, Record, Target},
|
||||||
mailflow::held::{self, HOLD_SECONDS, Held, KEEP_DAYS},
|
mailflow::held::{self, HOLD_SECONDS, Held},
|
||||||
};
|
};
|
||||||
use mail_builder::{
|
use mail_builder::{
|
||||||
MessageBuilder,
|
MessageBuilder,
|
||||||
@@ -83,7 +83,8 @@ pub async fn reject(server: &Server, record: &Held, note: Option<&str>) -> trc::
|
|||||||
match note {
|
match note {
|
||||||
Some(note) => text.push_str(&format!("\r\nThe reviewer's note: {note}\r\n")),
|
Some(note) => text.push_str(&format!("\r\nThe reviewer's note: {note}\r\n")),
|
||||||
None => text.push_str(&format!(
|
None => text.push_str(&format!(
|
||||||
"\r\nNobody reviewed it within {KEEP_DAYS} days, so it was returned.\r\n"
|
"\r\nNobody reviewed it within {} days, so it was returned.\r\n",
|
||||||
|
record.keep_days
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
notify(
|
notify(
|
||||||
@@ -107,9 +108,10 @@ pub async fn notify_held(server: &Server, record: &Held) {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
let text = format!(
|
let text = format!(
|
||||||
"Your message \"{}\" to {} is held for review under this server's rules: {notices}\r\n\r\n\
|
"Your message \"{}\" to {} is held for review under this server's rules: {notices}\r\n\r\n\
|
||||||
It will be sent if a reviewer releases it, and returned otherwise within {KEEP_DAYS} days.\r\n",
|
It will be sent if a reviewer releases it, and returned otherwise within {} days.\r\n",
|
||||||
record.subject,
|
record.subject,
|
||||||
record.recipients.join(", "),
|
record.recipients.join(", "),
|
||||||
|
record.keep_days,
|
||||||
);
|
);
|
||||||
notify(
|
notify(
|
||||||
server,
|
server,
|
||||||
@@ -166,7 +168,8 @@ pub async fn expire(server: &Server) -> trc::Result<usize> {
|
|||||||
},
|
},
|
||||||
changes: vec![],
|
changes: vec![],
|
||||||
details: Some(format!(
|
details: Some(format!(
|
||||||
"Rejected: nobody reviewed it within {KEEP_DAYS} days; the sender was told"
|
"Rejected: nobody reviewed it within {} days; the sender was told",
|
||||||
|
record.keep_days
|
||||||
)),
|
)),
|
||||||
reason: None,
|
reason: None,
|
||||||
outcome: Outcome::success(),
|
outcome: Outcome::success(),
|
||||||
|
|||||||
@@ -304,8 +304,9 @@ held mail, so what's released is what would have gone out. The daily
|
|||||||
clean-up rejects what's past its 7 days (recorded as the server's doing).
|
clean-up rejects what's past its 7 days (recorded as the server's doing).
|
||||||
`preview` returns the text (64 KB) only when asked for, and each read is
|
`preview` returns the text (64 KB) only when asked for, and each read is
|
||||||
recorded as `blobAccess`. Emails › Queue refuses to change or delete held
|
recorded as `blobAccess`. Emails › Queue refuses to change or delete held
|
||||||
mail, and the sender can't unsend it. The 7 days is a constant for now; a
|
mail, and the sender can't unsend it. How many days held mail waits is
|
||||||
setting comes with the console page.
|
`inbuxa:DlpSettings.keepHeldDays`, 1 to 90, 7 by default; each held message
|
||||||
|
keeps the days it was given.
|
||||||
|
|
||||||
### 2.7 What's recorded
|
### 2.7 What's recorded
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ lockedAt = ["metadata"]
|
|||||||
lockedBy = ["identifier"]
|
lockedBy = ["identifier"]
|
||||||
delegates = ["identifier"]
|
delegates = ["identifier"]
|
||||||
|
|
||||||
|
[object."inbuxa:DlpSettings"]
|
||||||
|
file = "inbuxa_dlp_settings.rs"
|
||||||
|
default = "none"
|
||||||
|
|
||||||
[object."inbuxa:HeldMessage"]
|
[object."inbuxa:HeldMessage"]
|
||||||
file = "inbuxa_held_message.rs"
|
file = "inbuxa_held_message.rs"
|
||||||
default = "none"
|
default = "none"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use registry::schema::{
|
|||||||
};
|
};
|
||||||
use registry::types::map::Map;
|
use registry::types::map::Map;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
const USING: &[&str] = &[
|
const USING: &[&str] = &[
|
||||||
"urn:ietf:params:jmap:core",
|
"urn:ietf:params:jmap:core",
|
||||||
@@ -817,6 +818,26 @@ pub async fn hold(test: &mut TestServer) {
|
|||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_else(|| panic!("{response}"))
|
.unwrap_or_else(|| panic!("{response}"))
|
||||||
.to_string();
|
.to_string();
|
||||||
|
// How long held mail waits: 7 days unless set, from 1 to 90
|
||||||
|
let (_, response) = call(&admin, "inbuxa:DlpSettings/get", json!({"ids": null})).await;
|
||||||
|
assert_eq!(response["list"][0]["keepHeldDays"], 7, "{response}");
|
||||||
|
let (_, response) = call(
|
||||||
|
&admin,
|
||||||
|
"inbuxa:DlpSettings/set",
|
||||||
|
json!({"update": {"singleton": {"keepHeldDays": 0}}}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
response["notUpdated"].get("singleton").is_some(),
|
||||||
|
"{response}"
|
||||||
|
);
|
||||||
|
let (_, response) = call(
|
||||||
|
&admin,
|
||||||
|
"inbuxa:DlpSettings/set",
|
||||||
|
json!({"update": {"singleton": {"keepHeldDays": 3}}}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(response["updated"].get("singleton").is_some(), "{response}");
|
||||||
let body = "hold-me: card 4242 4242 4242 4242";
|
let body = "hold-me: card 4242 4242 4242 4242";
|
||||||
|
|
||||||
// Accepted, held, listed
|
// Accepted, held, listed
|
||||||
@@ -844,6 +865,9 @@ pub async fn hold(test: &mut TestServer) {
|
|||||||
assert_eq!(list[0]["sender"], "[email protected]");
|
assert_eq!(list[0]["sender"], "[email protected]");
|
||||||
assert_eq!(list[0]["subject"], "Held one");
|
assert_eq!(list[0]["subject"], "Held one");
|
||||||
assert_eq!(list[0]["rules"][0]["name"], "Hold cards");
|
assert_eq!(list[0]["rules"][0]["name"], "Hold cards");
|
||||||
|
let span = chrono_seconds(list[0]["expiresAt"].as_str().unwrap())
|
||||||
|
- chrono_seconds(list[0]["heldAt"].as_str().unwrap());
|
||||||
|
assert_eq!(span, 3 * 86_400, "held for the days set");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list[0]["counts"],
|
list[0]["counts"],
|
||||||
json!([{"detector": "words", "count": 1}, {"detector": "payment-card", "count": 1}])
|
json!([{"detector": "words", "count": 1}, {"detector": "payment-card", "count": 1}])
|
||||||
@@ -1057,7 +1081,7 @@ pub async fn hold(test: &mut TestServer) {
|
|||||||
);
|
);
|
||||||
let notice = received(&sender, "Not sent: Held three", "X-Flow", Some(&mailbox)).await;
|
let notice = received(&sender, "Not sent: Held three", "X-Flow", Some(&mailbox)).await;
|
||||||
assert!(
|
assert!(
|
||||||
notice[0].1.contains("Nobody reviewed it within 7 days"),
|
notice[0].1.contains("Nobody reviewed it within 3 days"),
|
||||||
"{notice:?}"
|
"{notice:?}"
|
||||||
);
|
);
|
||||||
let (_, response) = call(
|
let (_, response) = call(
|
||||||
@@ -1075,6 +1099,13 @@ pub async fn hold(test: &mut TestServer) {
|
|||||||
call(&admin, "inbuxa:MailRule/set", json!({"destroy": [rule]})).await;
|
call(&admin, "inbuxa:MailRule/set", json!({"destroy": [rule]})).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Seconds since the epoch of a UTC date the server wrote.
|
||||||
|
fn chrono_seconds(date: &str) -> i64 {
|
||||||
|
jmap_proto::types::date::UTCDate::from_str(date)
|
||||||
|
.map(|d| d.timestamp())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Subjects in `account` matching `text` right now, not in `drafts`.
|
/// Subjects in `account` matching `text` right now, not in `drafts`.
|
||||||
async fn received_now(account: &Account, text: &str, drafts: &str) -> Vec<String> {
|
async fn received_now(account: &Account, text: &str, drafts: &str) -> Vec<String> {
|
||||||
let (_, response) = call(
|
let (_, response) = call(
|
||||||
|
|||||||
Reference in New Issue
Block a user