Scheduled reports and the weekly digest #160

Merged
jcoffey-dev merged 3 commits from feature/scheduled-reports into main 2026-10-06 22:41:05 +00:00
16 changed files with 494 additions and 3 deletions
Showing only changes of commit 09ccad4b4b - Show all commits

No files matched your search

@@ -0,0 +1,175 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! `inbuxa:ReportExport/set` under `urn:inbuxa:jmap`: a scheduled report
//! for a period as a ZIP of a summary and CSVs, without mailing anyone
//! (scheduled-reports spec, RP-19). Exports aren't kept; `/get` finds none.
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 ReportExport;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ReportExportProperty {
Id,
ReportId,
From,
To,
BlobId,
Size,
Sha256,
Files,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ReportExportValue {
Id(Id),
}
impl Property for ReportExportProperty {
fn try_parse(parent: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
// Keys inside the lists stay plain keys
match parent {
None => ReportExportProperty::parse(value),
Some(_) => None,
}
}
fn to_cow(&self) -> Cow<'static, str> {
match self {
ReportExportProperty::Id => "id",
ReportExportProperty::ReportId => "reportId",
ReportExportProperty::From => "from",
ReportExportProperty::To => "to",
ReportExportProperty::BlobId => "blobId",
ReportExportProperty::Size => "size",
ReportExportProperty::Sha256 => "sha256",
ReportExportProperty::Files => "files",
}
.into()
}
}
impl ReportExportProperty {
fn parse(value: &str) -> Option<Self> {
hashify::tiny_map!(value.as_bytes(),
b"id" => ReportExportProperty::Id,
b"reportId" => ReportExportProperty::ReportId,
b"from" => ReportExportProperty::From,
b"to" => ReportExportProperty::To,
b"blobId" => ReportExportProperty::BlobId,
b"size" => ReportExportProperty::Size,
b"sha256" => ReportExportProperty::Sha256,
b"files" => ReportExportProperty::Files,
)
}
}
impl FromStr for ReportExportProperty {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
ReportExportProperty::parse(s).ok_or(())
}
}
impl Element for ReportExportValue {
type Property = ReportExportProperty;
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
match key {
Key::Property(ReportExportProperty::Id) => {
Id::from_str(value).ok().map(ReportExportValue::Id)
}
_ => None,
}
}
fn to_cow(&self) -> Cow<'static, str> {
match self {
ReportExportValue::Id(id) => id.to_string().into(),
}
}
}
impl JmapObject for ReportExport {
type Property = ReportExportProperty;
type Element = ReportExportValue;
type Id = Id;
type Filter = ();
type Comparator = ();
type GetArguments = ();
type SetArguments<'de> = ();
type QueryArguments = ();
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = ReportExportProperty::Id;
}
impl From<Id> for ReportExportValue {
fn from(id: Id) -> Self {
ReportExportValue::Id(id)
}
}
impl JmapObjectId for ReportExportValue {
fn as_id(&self) -> Option<Id> {
match self {
ReportExportValue::Id(id) => Some(*id),
}
}
fn as_any_id(&self) -> Option<AnyId> {
match self {
ReportExportValue::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 = ReportExportValue::Id(id);
true
} else {
false
}
}
}
impl JmapObjectId for ReportExportProperty {
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
}
}
+1
View File
@@ -33,6 +33,7 @@ pub mod inbuxa_mail_rule; // inbuxa: DLP and mail flow rules
pub mod inbuxa_security_acceptance; // inbuxa: accepted security to-do items pub mod inbuxa_security_acceptance; // inbuxa: accepted security to-do items
pub mod inbuxa_deliverability_report; // inbuxa: the deliverability check pub mod inbuxa_deliverability_report; // inbuxa: the deliverability check
pub mod inbuxa_deliverability_settings; // inbuxa: the deliverability check pub mod inbuxa_deliverability_settings; // inbuxa: the deliverability check
pub mod inbuxa_report_export; // inbuxa: scheduled reports, RP-19
pub mod inbuxa_scheduled_report; // inbuxa: scheduled reports pub mod inbuxa_scheduled_report; // inbuxa: scheduled reports
pub mod inbuxa_scheduled_report_settings; // inbuxa: scheduled reports pub mod inbuxa_scheduled_report_settings; // inbuxa: scheduled reports
pub mod inbuxa_journal; // inbuxa: journaling pub mod inbuxa_journal; // inbuxa: journaling
+3
View File
@@ -97,6 +97,9 @@ impl Response<'_> {
GetResponseMethod::DeliverabilitySettings(response) => { GetResponseMethod::DeliverabilitySettings(response) => {
response.eval_jptr(path, &mut results) response.eval_jptr(path, &mut results)
} }
GetResponseMethod::ReportExport(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::ScheduledReport(response) => { GetResponseMethod::ScheduledReport(response) => {
response.eval_jptr(path, &mut results) response.eval_jptr(path, &mut results)
} }
@@ -58,6 +58,7 @@ impl Response<'_> {
GetRequestMethod::SecurityAcceptance(request) => request.resolve_references(self)?, GetRequestMethod::SecurityAcceptance(request) => request.resolve_references(self)?,
GetRequestMethod::DeliverabilityReport(request) => request.resolve_references(self)?, GetRequestMethod::DeliverabilityReport(request) => request.resolve_references(self)?,
GetRequestMethod::DeliverabilitySettings(request) => request.resolve_references(self)?, GetRequestMethod::DeliverabilitySettings(request) => request.resolve_references(self)?,
GetRequestMethod::ReportExport(request) => request.resolve_references(self)?,
GetRequestMethod::ScheduledReport(request) => request.resolve_references(self)?, GetRequestMethod::ScheduledReport(request) => request.resolve_references(self)?,
GetRequestMethod::ScheduledReportSettings(request) => request.resolve_references(self)?, GetRequestMethod::ScheduledReportSettings(request) => request.resolve_references(self)?,
GetRequestMethod::Journal(request) => request.resolve_references(self)?, GetRequestMethod::Journal(request) => request.resolve_references(self)?,
@@ -150,6 +151,9 @@ impl Response<'_> {
SetRequestMethod::DeliverabilitySettings(request) => { SetRequestMethod::DeliverabilitySettings(request) => {
request.resolve_references(self, 1, false)? request.resolve_references(self, 1, false)?
} }
SetRequestMethod::ReportExport(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::ScheduledReport(request) => { SetRequestMethod::ScheduledReport(request) => {
request.resolve_references(self, 1, false)? request.resolve_references(self, 1, false)?
} }
+7
View File
@@ -73,6 +73,7 @@ pub enum MethodObject {
// inbuxa: the deliverability check // inbuxa: the deliverability check
DeliverabilityReport, DeliverabilityReport,
DeliverabilitySettings, DeliverabilitySettings,
ReportExport,
ScheduledReport, ScheduledReport,
ScheduledReportSettings, ScheduledReportSettings,
HeldMessage, HeldMessage,
@@ -126,6 +127,7 @@ impl MethodObject {
| MethodObject::HeldMessage | MethodObject::HeldMessage
| MethodObject::DeliverabilityReport | MethodObject::DeliverabilityReport
| MethodObject::DeliverabilitySettings | MethodObject::DeliverabilitySettings
| MethodObject::ReportExport
| MethodObject::ScheduledReport | MethodObject::ScheduledReport
| MethodObject::ScheduledReportSettings | MethodObject::ScheduledReportSettings
| MethodObject::Journal | MethodObject::Journal
@@ -336,6 +338,8 @@ impl MethodName {
(MethodFunction::Set, MethodObject::DeliverabilityReport) => "inbuxa:DeliverabilityReport/set", (MethodFunction::Set, MethodObject::DeliverabilityReport) => "inbuxa:DeliverabilityReport/set",
(MethodFunction::Get, MethodObject::DeliverabilitySettings) => "inbuxa:DeliverabilitySettings/get", (MethodFunction::Get, MethodObject::DeliverabilitySettings) => "inbuxa:DeliverabilitySettings/get",
(MethodFunction::Set, MethodObject::DeliverabilitySettings) => "inbuxa:DeliverabilitySettings/set", (MethodFunction::Set, MethodObject::DeliverabilitySettings) => "inbuxa:DeliverabilitySettings/set",
(MethodFunction::Get, MethodObject::ReportExport) => "inbuxa:ReportExport/get",
(MethodFunction::Set, MethodObject::ReportExport) => "inbuxa:ReportExport/set",
(MethodFunction::Get, MethodObject::ScheduledReport) => "inbuxa:ScheduledReport/get", (MethodFunction::Get, MethodObject::ScheduledReport) => "inbuxa:ScheduledReport/get",
(MethodFunction::Set, MethodObject::ScheduledReport) => "inbuxa:ScheduledReport/set", (MethodFunction::Set, MethodObject::ScheduledReport) => "inbuxa:ScheduledReport/set",
(MethodFunction::Get, MethodObject::ScheduledReportSettings) => "inbuxa:ScheduledReportSettings/get", (MethodFunction::Get, MethodObject::ScheduledReportSettings) => "inbuxa:ScheduledReportSettings/get",
@@ -518,6 +522,8 @@ impl MethodName {
"inbuxa:DeliverabilityReport/set" => (MethodObject::DeliverabilityReport, MethodFunction::Set), "inbuxa:DeliverabilityReport/set" => (MethodObject::DeliverabilityReport, MethodFunction::Set),
"inbuxa:DeliverabilitySettings/get" => (MethodObject::DeliverabilitySettings, MethodFunction::Get), "inbuxa:DeliverabilitySettings/get" => (MethodObject::DeliverabilitySettings, MethodFunction::Get),
"inbuxa:DeliverabilitySettings/set" => (MethodObject::DeliverabilitySettings, MethodFunction::Set), "inbuxa:DeliverabilitySettings/set" => (MethodObject::DeliverabilitySettings, MethodFunction::Set),
"inbuxa:ReportExport/get" => (MethodObject::ReportExport, MethodFunction::Get),
"inbuxa:ReportExport/set" => (MethodObject::ReportExport, MethodFunction::Set),
"inbuxa:ScheduledReport/get" => (MethodObject::ScheduledReport, MethodFunction::Get), "inbuxa:ScheduledReport/get" => (MethodObject::ScheduledReport, MethodFunction::Get),
"inbuxa:ScheduledReport/set" => (MethodObject::ScheduledReport, MethodFunction::Set), "inbuxa:ScheduledReport/set" => (MethodObject::ScheduledReport, MethodFunction::Set),
"inbuxa:ScheduledReportSettings/get" => (MethodObject::ScheduledReportSettings, MethodFunction::Get), "inbuxa:ScheduledReportSettings/get" => (MethodObject::ScheduledReportSettings, MethodFunction::Get),
@@ -607,6 +613,7 @@ impl Display for MethodObject {
MethodObject::SecurityAcceptance => "inbuxa:SecurityAcceptance", MethodObject::SecurityAcceptance => "inbuxa:SecurityAcceptance",
MethodObject::DeliverabilityReport => "inbuxa:DeliverabilityReport", MethodObject::DeliverabilityReport => "inbuxa:DeliverabilityReport",
MethodObject::DeliverabilitySettings => "inbuxa:DeliverabilitySettings", MethodObject::DeliverabilitySettings => "inbuxa:DeliverabilitySettings",
MethodObject::ReportExport => "inbuxa:ReportExport",
MethodObject::ScheduledReport => "inbuxa:ScheduledReport", MethodObject::ScheduledReport => "inbuxa:ScheduledReport",
MethodObject::ScheduledReportSettings => "inbuxa:ScheduledReportSettings", MethodObject::ScheduledReportSettings => "inbuxa:ScheduledReportSettings",
MethodObject::Journal => "inbuxa:Journal", MethodObject::Journal => "inbuxa:Journal",
+2
View File
@@ -128,6 +128,7 @@ pub enum GetRequestMethod {
SecurityAcceptance(Box<GetRequest<crate::object::inbuxa_security_acceptance::SecurityAcceptance>>), SecurityAcceptance(Box<GetRequest<crate::object::inbuxa_security_acceptance::SecurityAcceptance>>),
DeliverabilityReport(Box<GetRequest<crate::object::inbuxa_deliverability_report::DeliverabilityReport>>), DeliverabilityReport(Box<GetRequest<crate::object::inbuxa_deliverability_report::DeliverabilityReport>>),
DeliverabilitySettings(Box<GetRequest<crate::object::inbuxa_deliverability_settings::DeliverabilitySettings>>), DeliverabilitySettings(Box<GetRequest<crate::object::inbuxa_deliverability_settings::DeliverabilitySettings>>),
ReportExport(Box<GetRequest<crate::object::inbuxa_report_export::ReportExport>>),
ScheduledReport(Box<GetRequest<crate::object::inbuxa_scheduled_report::ScheduledReport>>), ScheduledReport(Box<GetRequest<crate::object::inbuxa_scheduled_report::ScheduledReport>>),
ScheduledReportSettings(Box<GetRequest<crate::object::inbuxa_scheduled_report_settings::ScheduledReportSettings>>), ScheduledReportSettings(Box<GetRequest<crate::object::inbuxa_scheduled_report_settings::ScheduledReportSettings>>),
Journal(Box<GetRequest<crate::object::inbuxa_journal::Journal>>), Journal(Box<GetRequest<crate::object::inbuxa_journal::Journal>>),
@@ -178,6 +179,7 @@ pub enum SetRequestMethod<'x> {
), ),
DeliverabilityReport(Box<SetRequest<'x, crate::object::inbuxa_deliverability_report::DeliverabilityReport>>), DeliverabilityReport(Box<SetRequest<'x, crate::object::inbuxa_deliverability_report::DeliverabilityReport>>),
DeliverabilitySettings(Box<SetRequest<'x, crate::object::inbuxa_deliverability_settings::DeliverabilitySettings>>), DeliverabilitySettings(Box<SetRequest<'x, crate::object::inbuxa_deliverability_settings::DeliverabilitySettings>>),
ReportExport(Box<SetRequest<'x, crate::object::inbuxa_report_export::ReportExport>>),
ScheduledReport(Box<SetRequest<'x, crate::object::inbuxa_scheduled_report::ScheduledReport>>), ScheduledReport(Box<SetRequest<'x, crate::object::inbuxa_scheduled_report::ScheduledReport>>),
ScheduledReportSettings(Box<SetRequest<'x, crate::object::inbuxa_scheduled_report_settings::ScheduledReportSettings>>), ScheduledReportSettings(Box<SetRequest<'x, crate::object::inbuxa_scheduled_report_settings::ScheduledReportSettings>>),
Journal(Box<SetRequest<'x, crate::object::inbuxa_journal::Journal>>), Journal(Box<SetRequest<'x, crate::object::inbuxa_journal::Journal>>),
+14
View File
@@ -743,6 +743,20 @@ 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::ReportExport) => match seq.next_element() {
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ReportExport(value)),
Err(err) => RequestMethod::invalid(err),
Ok(None) => {
return Err(de::Error::invalid_length(1, &self));
}
},
(MethodFunction::Set, MethodObject::ReportExport) => match seq.next_element() {
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ReportExport(value)),
Err(err) => RequestMethod::invalid(err),
Ok(None) => {
return Err(de::Error::invalid_length(1, &self));
}
},
// inbuxa: journaling // inbuxa: journaling
(MethodFunction::Get, MethodObject::JournalEntry) => match seq.next_element() { (MethodFunction::Get, MethodObject::JournalEntry) => match seq.next_element() {
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::JournalEntry(value)), Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::JournalEntry(value)),
+13
View File
@@ -115,6 +115,7 @@ pub enum GetResponseMethod {
SecurityAcceptance(GetResponse<crate::object::inbuxa_security_acceptance::SecurityAcceptance>), SecurityAcceptance(GetResponse<crate::object::inbuxa_security_acceptance::SecurityAcceptance>),
DeliverabilityReport(GetResponse<crate::object::inbuxa_deliverability_report::DeliverabilityReport>), DeliverabilityReport(GetResponse<crate::object::inbuxa_deliverability_report::DeliverabilityReport>),
DeliverabilitySettings(GetResponse<crate::object::inbuxa_deliverability_settings::DeliverabilitySettings>), DeliverabilitySettings(GetResponse<crate::object::inbuxa_deliverability_settings::DeliverabilitySettings>),
ReportExport(GetResponse<crate::object::inbuxa_report_export::ReportExport>),
ScheduledReport(GetResponse<crate::object::inbuxa_scheduled_report::ScheduledReport>), ScheduledReport(GetResponse<crate::object::inbuxa_scheduled_report::ScheduledReport>),
ScheduledReportSettings(GetResponse<crate::object::inbuxa_scheduled_report_settings::ScheduledReportSettings>), ScheduledReportSettings(GetResponse<crate::object::inbuxa_scheduled_report_settings::ScheduledReportSettings>),
Journal(GetResponse<crate::object::inbuxa_journal::Journal>), Journal(GetResponse<crate::object::inbuxa_journal::Journal>),
@@ -165,6 +166,7 @@ pub enum SetResponseMethod {
), ),
DeliverabilityReport(Box<SetResponse<crate::object::inbuxa_deliverability_report::DeliverabilityReport>>), DeliverabilityReport(Box<SetResponse<crate::object::inbuxa_deliverability_report::DeliverabilityReport>>),
DeliverabilitySettings(Box<SetResponse<crate::object::inbuxa_deliverability_settings::DeliverabilitySettings>>), DeliverabilitySettings(Box<SetResponse<crate::object::inbuxa_deliverability_settings::DeliverabilitySettings>>),
ReportExport(Box<SetResponse<crate::object::inbuxa_report_export::ReportExport>>),
ScheduledReport(Box<SetResponse<crate::object::inbuxa_scheduled_report::ScheduledReport>>), ScheduledReport(Box<SetResponse<crate::object::inbuxa_scheduled_report::ScheduledReport>>),
ScheduledReportSettings(Box<SetResponse<crate::object::inbuxa_scheduled_report_settings::ScheduledReportSettings>>), ScheduledReportSettings(Box<SetResponse<crate::object::inbuxa_scheduled_report_settings::ScheduledReportSettings>>),
Journal(Box<SetResponse<crate::object::inbuxa_journal::Journal>>), Journal(Box<SetResponse<crate::object::inbuxa_journal::Journal>>),
@@ -896,6 +898,17 @@ impl<'x> From<SetResponse<crate::object::inbuxa_deliverability_settings::Deliver
ResponseMethod::Set(SetResponseMethod::DeliverabilitySettings(Box::new(value))) ResponseMethod::Set(SetResponseMethod::DeliverabilitySettings(Box::new(value)))
} }
} }
impl<'x> From<GetResponse<crate::object::inbuxa_report_export::ReportExport>> for ResponseMethod<'x> {
fn from(value: GetResponse<crate::object::inbuxa_report_export::ReportExport>) -> Self {
ResponseMethod::Get(GetResponseMethod::ReportExport(value))
}
}
impl<'x> From<SetResponse<crate::object::inbuxa_report_export::ReportExport>> for ResponseMethod<'x> {
fn from(value: SetResponse<crate::object::inbuxa_report_export::ReportExport>) -> Self {
ResponseMethod::Set(SetResponseMethod::ReportExport(Box::new(value)))
}
}
impl<'x> From<GetResponse<crate::object::inbuxa_scheduled_report::ScheduledReport>> for ResponseMethod<'x> { impl<'x> From<GetResponse<crate::object::inbuxa_scheduled_report::ScheduledReport>> for ResponseMethod<'x> {
fn from(value: GetResponse<crate::object::inbuxa_scheduled_report::ScheduledReport>) -> Self { fn from(value: GetResponse<crate::object::inbuxa_scheduled_report::ScheduledReport>) -> Self {
ResponseMethod::Get(GetResponseMethod::ScheduledReport(value)) ResponseMethod::Get(GetResponseMethod::ScheduledReport(value))
+11 -1
View File
@@ -130,7 +130,8 @@ impl JmapAuthorization for AccessToken {
// inbuxa: scheduled-reports spec; a tenant administrator sees // inbuxa: scheduled-reports spec; a tenant administrator sees
// their own tenant's reports (RP-22) // their own tenant's reports (RP-22)
GetRequestMethod::ScheduledReport(_) GetRequestMethod::ScheduledReport(_)
| GetRequestMethod::ScheduledReportSettings(_) => Permission::SysScheduledReportGet, | GetRequestMethod::ScheduledReportSettings(_)
| GetRequestMethod::ReportExport(_) => Permission::SysScheduledReportGet,
// inbuxa: legacy protocols off. It takes listeners away and // inbuxa: legacy protocols off. It takes listeners away and
// puts them back, so it takes the listener's permissions // puts them back, so it takes the listener's permissions
GetRequestMethod::ProtocolPolicy(_) => Permission::SysNetworkListenerGet, GetRequestMethod::ProtocolPolicy(_) => Permission::SysNetworkListenerGet,
@@ -375,6 +376,14 @@ impl JmapAuthorization for AccessToken {
Permission::SysScheduledReportUpdate, Permission::SysScheduledReportUpdate,
Permission::SysScheduledReportUpdate, Permission::SysScheduledReportUpdate,
), ),
// inbuxa: RP-19, a download reads what the report would send
SetRequestMethod::ReportExport(s) => validate_set(
s,
self,
Permission::SysScheduledReportGet,
Permission::SysScheduledReportGet,
Permission::SysScheduledReportGet,
),
// inbuxa: LH-12, exporting held data // inbuxa: LH-12, exporting held data
SetRequestMethod::HoldExport(s) => validate_set( SetRequestMethod::HoldExport(s) => validate_set(
s, s,
@@ -548,6 +557,7 @@ impl JmapAuthorization for AccessToken {
| MethodObject::SecurityAcceptance | MethodObject::SecurityAcceptance
| MethodObject::DeliverabilityReport | MethodObject::DeliverabilityReport
| MethodObject::DeliverabilitySettings | MethodObject::DeliverabilitySettings
| MethodObject::ReportExport
| MethodObject::ScheduledReport | MethodObject::ScheduledReport
| MethodObject::ScheduledReportSettings | MethodObject::ScheduledReportSettings
| MethodObject::HeldMessage | MethodObject::HeldMessage
+31
View File
@@ -299,6 +299,9 @@ impl RequestHandler for Server {
SetResponseMethod::DeliverabilitySettings(set_response) => { SetResponseMethod::DeliverabilitySettings(set_response) => {
set_response.update_created_ids(&mut response); set_response.update_created_ids(&mut response);
} }
SetResponseMethod::ReportExport(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::ScheduledReport(set_response) => { SetResponseMethod::ScheduledReport(set_response) => {
set_response.update_created_ids(&mut response); set_response.update_created_ids(&mut response);
} }
@@ -577,6 +580,12 @@ impl RequestHandler for Server {
.await? .await?
.into() .into()
} }
GetRequestMethod::ReportExport(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::scheduled_reports::get_exports(self, access_token, *req)
.await?
.into()
}
// inbuxa: journaling // inbuxa: journaling
GetRequestMethod::Journal(mut req) => { GetRequestMethod::Journal(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)?;
@@ -1127,6 +1136,28 @@ impl RequestHandler for Server {
.await? .await?
.into() .into()
} }
// inbuxa: RP-19; a download is in the audit log too
SetRequestMethod::ReportExport(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
None,
*req,
|req| {
Box::pin(crate::inbuxa::scheduled_reports::set_exports(
self,
access_token,
req,
))
},
)
.await?
.into()
}
SetRequestMethod::ScheduledReportSettings(mut req) => { SetRequestMethod::ScheduledReportSettings(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)?;
crate::inbuxa::audit::recorded( crate::inbuxa::audit::recorded(
+1
View File
@@ -434,6 +434,7 @@ impl IntermediateChangesResponse {
| MethodObject::SecurityAcceptance | MethodObject::SecurityAcceptance
| MethodObject::DeliverabilityReport | MethodObject::DeliverabilityReport
| MethodObject::DeliverabilitySettings | MethodObject::DeliverabilitySettings
| MethodObject::ReportExport
| MethodObject::ScheduledReport | MethodObject::ScheduledReport
| MethodObject::ScheduledReportSettings | MethodObject::ScheduledReportSettings
| MethodObject::Journal | MethodObject::Journal
+156 -1
View File
@@ -23,6 +23,7 @@ use jmap_proto::{
set::{SetRequest, SetResponse}, set::{SetRequest, SetResponse},
}, },
object::{ object::{
inbuxa_report_export::{ReportExport, ReportExportProperty as X, ReportExportValue},
inbuxa_scheduled_report::{ inbuxa_scheduled_report::{
ScheduledReport, ScheduledReportProperty as R, ScheduledReportValue, ScheduledReport, ScheduledReportProperty as R, ScheduledReportValue,
}, },
@@ -35,7 +36,7 @@ use jmap_proto::{
types::date::UTCDate, types::date::UTCDate,
}; };
use jmap_tools::{Element, Key, Map, Property, Value}; use jmap_tools::{Element, Key, Map, Property, Value};
use std::borrow::Cow; use std::{borrow::Cow, str::FromStr};
use store::write::now; use store::write::now;
use types::id::Id; use types::id::Id;
@@ -533,3 +534,157 @@ pub async fn set_settings(
} }
Ok(response) Ok(response)
} }
/// RP-19: the furthest back a download goes, as far as metrics are kept.
const EXPORT_MAX_AGE: u64 = 90 * 86_400;
fn parse_date(value: &serde_json::Value) -> Option<u64> {
value
.as_str()
.and_then(|s| UTCDate::from_str(s).ok())
.map(|d| d.timestamp().max(0) as u64)
}
/// `inbuxa:ReportExport/get`: exports aren't kept, so there's never one.
pub async fn get_exports(
_server: &Server,
_access_token: &AccessToken,
mut request: GetRequest<ReportExport>,
) -> trc::Result<GetResponse<ReportExport>> {
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,
};
for id in ids.unwrap_or_default() {
response.push_not_found(id);
}
Ok(response)
}
/// `inbuxa:ReportExport/set`: create `{reportId, from?, to?}` builds that
/// report for the period (by default its last full one) as a ZIP of a
/// summary and CSVs, stored as an upload, and mails nobody (RP-19).
pub async fn set_exports(
server: &Server,
access_token: &AccessToken,
mut request: SetRequest<'_, ReportExport>,
) -> trc::Result<SetResponse<ReportExport>> {
use sha2::{Digest, Sha256};
use std::io::{Cursor, Write};
use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions};
let mut response = SetResponse::from_request(&request, server.core.jmap.set_max_objects)?;
for (id, _) in request.unwrap_update().into_valid() {
response.not_updated.append(
id,
SetError::forbidden().with_description("Exports can't be changed."),
);
}
for id in request.unwrap_destroy().into_valid() {
response.not_destroyed.append(
id,
SetError::forbidden().with_description("Exports aren't kept to destroy."),
);
}
for (client_id, value) in request.unwrap_create() {
let json = serde_json::to_value(&value).unwrap_or_default();
let invalid = |property: X, why: &str| {
SetError::invalid_properties()
.with_property(property)
.with_description(why.to_string())
};
let report = match json
.get("reportId")
.and_then(|v| v.as_str())
.and_then(|s| Id::from_str(s).ok())
{
Some(id) => model::report(server.store(), id.id())
.await?
.filter(|r| visible(access_token, r)),
None => None,
};
let Some(report) = report else {
response
.not_created
.append(client_id, invalid(X::ReportId, "A report you can see."));
continue;
};
let now = now();
let (default_from, default_to) = report.schedule.period(
report
.schedule
.next_due(report.last_due.min(now))
.filter(|d| *d <= now)
.unwrap_or(report.last_due.min(now)),
);
let from = json
.get("from")
.and_then(parse_date)
.unwrap_or(default_from);
let to = json.get("to").and_then(parse_date).unwrap_or(default_to);
if from >= to || to > now + 60 || from + EXPORT_MAX_AGE < now {
response.not_created.append(
client_id,
invalid(
X::From,
"A period that has started, ends no later than now, and goes back at most 90 days.",
),
);
continue;
}
let files =
services::inbuxa_scheduled_reports::export_files(server, &report, from, to).await?;
let fail = |err: zip::result::ZipError| {
trc::StoreEvent::UnexpectedError
.into_err()
.details("Failed to write a report export")
.reason(err)
};
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
let names: Vec<String> = files.iter().map(|(name, _)| name.clone()).collect();
for (name, body) in &files {
zip.start_file(name.as_str(), options).map_err(fail)?;
zip.write_all(body).map_err(|err| {
trc::StoreEvent::UnexpectedError
.into_err()
.details("Failed to write a report export")
.reason(err)
})?;
}
let bytes = zip.finish().map_err(fail)?.into_inner();
let sha256 = Sha256::digest(&bytes)
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
let blob = server
.put_jmap_blob(access_token.account_id(), &bytes)
.await?;
let mut created = Map::with_capacity(7);
created.insert_unchecked(
Key::Property(X::Id),
Value::Element(ReportExportValue::Id(Id::from(now))),
);
created.insert_unchecked(
Key::Property(X::BlobId),
Value::Str(blob.to_string().into()),
);
created.insert_unchecked(
Key::Property(X::Size),
Value::Number((bytes.len() as u64).into()),
);
created.insert_unchecked(Key::Property(X::Sha256), Value::Str(sha256.into()));
created.insert_unchecked(Key::Property(X::From), date(from));
created.insert_unchecked(Key::Property(X::To), date(to));
created.insert_unchecked(
Key::Property(X::Files),
Value::Array(names.into_iter().map(|n| Value::Str(n.into())).collect()),
);
response.created.insert(client_id, Value::Object(created));
}
Ok(response)
}
@@ -1087,6 +1087,36 @@ async fn certificates(
})) }))
} }
/// RP-19: the report for a period as files, without mailing anyone: the
/// summary as text, and a CSV for each section that has rows. A download
/// doesn't move the deliverability baseline the next mail compares with.
pub async fn export_files(
server: &Server,
report: &Report,
from: u64,
to: u64,
) -> trc::Result<Vec<(String, Vec<u8>)>> {
let built = build(server, report, from, to).await?;
let mut summary = format!("{}\n{}\n\n", report.name, model::period_label(from, to));
if built.parts.is_empty() {
summary.push_str("Nothing to report for this period.\n");
}
for part in &built.parts {
summary.push_str(&format!("{}\n", part.title));
for line in &part.lines {
summary.push_str(&format!(" {line}\n"));
}
summary.push('\n');
}
let mut files = vec![("summary.txt".to_string(), summary.into_bytes())];
for part in built.parts {
if let Some((name, body)) = part.csv {
files.push((name, body.into_bytes()));
}
}
Ok(files)
}
// --- The mail ------------------------------------------------------------- // --- The mail -------------------------------------------------------------
fn escape(s: &str) -> String { fn escape(s: &str) -> String {
+1 -1
View File
@@ -377,7 +377,7 @@ Not a rebuild: the **security to-do list** is INBUXA's own design (inbuxa-drafts
Not a rebuild: the **deliverability check** is INBUXA's own design (inbuxa-drafts `specs/deliverability.md`). Each sending node checks what other servers see of it (blocklists, reverse DNS, SPF, DKIM, DMARC, MTA-STS, certificates) and keeps a report: `inbuxa:DeliverabilityReport` and `inbuxa:DeliverabilitySettings` (`crates/jmap/src/inbuxa/deliverability.rs`, `crates/services/src/inbuxa_deliverability.rs`), and the `sysDeliverabilityGet`, `sysDeliverabilityUpdate` and `sysDeliverabilityCheck` permissions. Not a rebuild: the **deliverability check** is INBUXA's own design (inbuxa-drafts `specs/deliverability.md`). Each sending node checks what other servers see of it (blocklists, reverse DNS, SPF, DKIM, DMARC, MTA-STS, certificates) and keeps a report: `inbuxa:DeliverabilityReport` and `inbuxa:DeliverabilitySettings` (`crates/jmap/src/inbuxa/deliverability.rs`, `crates/services/src/inbuxa_deliverability.rs`), and the `sysDeliverabilityGet`, `sysDeliverabilityUpdate` and `sysDeliverabilityCheck` permissions.
Not a rebuild: **scheduled reports and the weekly digest** are INBUXA's own design (inbuxa-drafts `specs/scheduled-reports.md`). An administrator picks sections, a schedule in a time zone and recipients on this server; every node looks for due reports once a minute, one claims each run with the task lock, and the report is built from data the server already keeps and mailed DKIM-signed: `inbuxa:ScheduledReport` and `inbuxa:ScheduledReportSettings` (`crates/jmap/src/inbuxa/scheduled_reports.rs`, `crates/features/src/scheduled_reports/`, `crates/services/src/inbuxa_scheduled_reports.rs`, `crates/smtp/src/reporting/inbuxa_send.rs`), and the `sysScheduledReportGet` and `sysScheduledReportUpdate` permissions. The weekly digest is a built-in report, on by default. Not a rebuild: **scheduled reports and the weekly digest** are INBUXA's own design (inbuxa-drafts `specs/scheduled-reports.md`). An administrator picks sections, a schedule in a time zone and recipients on this server; every node looks for due reports once a minute, one claims each run with the task lock, and the report is built from data the server already keeps and mailed DKIM-signed: `inbuxa:ScheduledReport`, `inbuxa:ScheduledReportSettings` and `inbuxa:ReportExport` (a download: a ZIP of a summary and CSVs) (`crates/jmap/src/inbuxa/scheduled_reports.rs`, `crates/features/src/scheduled_reports/`, `crates/services/src/inbuxa_scheduled_reports.rs`, `crates/smtp/src/reporting/inbuxa_send.rs`), and the `sysScheduledReportGet` and `sysScheduledReportUpdate` permissions. The weekly digest is a built-in report, on by default.
## 5. The web front ends ## 5. The web front ends
+10
View File
@@ -188,6 +188,16 @@ retention = "object-life"
[object."inbuxa:ScheduledReport".properties] [object."inbuxa:ScheduledReport".properties]
recipients = ["contact"] recipients = ["contact"]
[object."inbuxa:ReportExport"]
file = "inbuxa_report_export.rs"
default = "none"
whose = ["administrator", "holder", "correspondent"]
where = ["blob-store"]
scope = "tenant"
retention = { setting = "x:Jmap.uploadTtl" }
[object."inbuxa:ReportExport".properties]
blobId = ["contact", "network", "metadata"]
[object."inbuxa:ScheduledReportSettings"] [object."inbuxa:ScheduledReportSettings"]
file = "inbuxa_scheduled_report_settings.rs" file = "inbuxa_scheduled_report_settings.rs"
default = "none" default = "none"
+35
View File
@@ -325,6 +325,41 @@ pub async fn test(test: &mut TestServer) {
.await; .await;
assert_eq!(name, "error", "a tenant changed the sender: {response}"); assert_eq!(name, "error", "a tenant changed the sender: {response}");
// --- Download (RP-19) ----------------------------------------------------
let (_, response) = call(
admin,
"inbuxa:ReportExport/set",
json!({"create": {
"last": {"reportId": &id},
"old": {"reportId": &id, "from": "2020-01-01T00:00:00Z", "to": "2020-01-02T00:00:00Z"}
}}),
)
.await;
let export = &response["created"]["last"];
assert!(export["blobId"].is_string(), "{response}");
assert!(
export["size"].as_u64().unwrap_or_default() > 0,
"{response}"
);
assert_eq!(
export["sha256"].as_str().map(|s| s.len()),
Some(64),
"{response}"
);
assert_eq!(export["files"][0], "summary.txt", "{response}");
assert!(
response["notCreated"]["old"].is_object(),
"a 2020 period was exported: {response}"
);
// A tenant administrator can't download a report that isn't theirs
let (_, response) = call(
&t_admin,
"inbuxa:ReportExport/set",
json!({"create": {"theirs": {"reportId": &id}}}),
)
.await;
assert!(response["notCreated"]["theirs"].is_object(), "{response}");
// --- Deleting ------------------------------------------------------------ // --- Deleting ------------------------------------------------------------
let (_, response) = call( let (_, response) = call(
admin, admin,