Legal holds, step 3: deleted items in a held account are kept
Every way of deleting mail (JMAP, IMAP EXPUNGE, POP3, mailbox removal, Trash emptying) and Sieve scripts, events, contacts and files now asks how the account's deletions are kept: a hold keeps them with no expiry (archivedUntil 9999-12-31), even with undelete off; otherwise undelete's period applies as before (LH-4). A hold's date range decides by the item's own date (LH-3). Mail is noted as held at deletion and settled when it's archived, once its received date is known; outside the range it gets undelete's deadline or isn't kept. Events go by their start, with a day's slack for time zones; recurring events, contacts, files and scripts are held whole. A groupware item's note now stays until its archive succeeds, and a failure retries the task instead of being logged and lost (LH-5).
This commit is contained in:
@@ -123,6 +123,14 @@ pub struct EmailNote {
|
||||
pub size: u64,
|
||||
pub mailboxes: Vec<u32>,
|
||||
pub keywords: Vec<String>,
|
||||
/// LH-3: the ranges of the holds on the account when it was deleted.
|
||||
/// Its received date is only known when it's archived, which decides
|
||||
/// whether a hold keeps it after all.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub held_ranges: Vec<(Option<u64>, Option<u64>)>,
|
||||
/// The undelete deadline for when no range covers it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub otherwise_until: Option<u64>,
|
||||
}
|
||||
|
||||
/// What restore needs beyond the kept copy (UD-4, UD-8).
|
||||
|
||||
@@ -12,9 +12,12 @@
|
||||
//! is made if archiving is on, fixing the deadline then. When the data is
|
||||
//! finally removed, a noted message becomes an archived item.
|
||||
|
||||
use crate::undelete::{
|
||||
data::{self, EmailNote, Extra},
|
||||
records,
|
||||
use crate::{
|
||||
hold::Keeping,
|
||||
undelete::{
|
||||
data::{self, EmailNote, Extra},
|
||||
records,
|
||||
},
|
||||
};
|
||||
use registry::{
|
||||
schema::structs::{ArchivedEmail, ArchivedItem},
|
||||
@@ -26,10 +29,12 @@ use store::{
|
||||
};
|
||||
use types::{blob::BlobId, blob_hash::BlobHash};
|
||||
|
||||
/// Notes a deleted message, when archiving is on (`retention` seconds).
|
||||
/// Notes a deleted message, when anything keeps it: undelete, or a legal
|
||||
/// hold on the account (LH-4). A held note keeps it until it's archived,
|
||||
/// when its received date says whether the hold's range covers it.
|
||||
pub fn note(
|
||||
batch: &mut BatchBuilder,
|
||||
retention: u64,
|
||||
keeping: &Keeping,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
size: u64,
|
||||
@@ -37,16 +42,23 @@ pub fn note(
|
||||
keywords: Vec<String>,
|
||||
) -> trc::Result<()> {
|
||||
let archived_at = now();
|
||||
// Held until the date is known; the undelete deadline otherwise
|
||||
let otherwise_until = keeping.until(archived_at, false);
|
||||
let Some(archived_until) = keeping.until(archived_at, keeping.is_held()) else {
|
||||
return Ok(());
|
||||
};
|
||||
data::note_email(
|
||||
batch,
|
||||
account_id,
|
||||
document_id,
|
||||
&EmailNote {
|
||||
archived_at,
|
||||
archived_until: archived_at + retention,
|
||||
archived_until,
|
||||
size,
|
||||
mailboxes,
|
||||
keywords,
|
||||
held_ranges: keeping.ranges.clone(),
|
||||
otherwise_until: if keeping.is_held() { otherwise_until } else { None },
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -78,9 +90,27 @@ pub async fn archive(
|
||||
document_id: u32,
|
||||
summary: Summary<'_>,
|
||||
) -> trc::Result<bool> {
|
||||
let Some(note) = data::email_note(data, account_id, document_id).await? else {
|
||||
let Some(mut note) = data::email_note(data, account_id, document_id).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
// LH-3: a held note's range decides now that the date is known; outside
|
||||
// it, undelete's deadline, or nothing kept at all
|
||||
if !note.held_ranges.is_empty() {
|
||||
let keeping = Keeping {
|
||||
retention: None,
|
||||
ranges: std::mem::take(&mut note.held_ranges),
|
||||
};
|
||||
if !keeping.covers(Some(summary.received_at)) {
|
||||
match note.otherwise_until {
|
||||
Some(until) => note.archived_until = until,
|
||||
None => {
|
||||
let mut batch = BatchBuilder::new();
|
||||
data::clear_email_note(&mut batch, account_id, document_id);
|
||||
return data.write(batch.build_all()).await.map(|_| false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let item = ArchivedItem::Email(ArchivedEmail {
|
||||
from: summary.from.unwrap_or_default().to_string(),
|
||||
subject: summary.subject.unwrap_or_default().to_string(),
|
||||
|
||||
@@ -97,6 +97,39 @@ pub async fn take(
|
||||
Ok(Some(note))
|
||||
}
|
||||
|
||||
/// A note, left in place: for a held account it's cleared only once its item
|
||||
/// is archived, so a failure leaves it for the retry (LH-5).
|
||||
pub async fn peek(
|
||||
data: &Store,
|
||||
kind: Kind,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
) -> trc::Result<Option<Note>> {
|
||||
Ok(data
|
||||
.get_value::<Json<Note>>(ValueKey::from(note_class(kind, account_id, document_id)))
|
||||
.await?
|
||||
.map(|Json(note)| note))
|
||||
}
|
||||
|
||||
/// Removes a note once its item is archived or needn't be.
|
||||
pub async fn clear(data: &Store, kind: Kind, account_id: u32, document_id: u32) -> trc::Result<()> {
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.clear(note_class(kind, account_id, document_id));
|
||||
data.write(batch.build_all()).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// An event's start, for a hold's range (LH-3). None for a recurring event,
|
||||
/// which may have an occurrence anywhere, so a hold keeps it whole.
|
||||
pub fn event_start(note: &Note) -> Option<u64> {
|
||||
let text = note.content.as_deref()?;
|
||||
if property(text, "RRULE").is_some() || property(text, "RDATE").is_some() {
|
||||
return None;
|
||||
}
|
||||
property(text, "DTSTART")
|
||||
.and_then(|v| ical_time(&v))
|
||||
.map(|t| t.max(0) as u64)
|
||||
}
|
||||
|
||||
/// The value of the first line starting with `name` (as `NAME:` or
|
||||
/// `NAME;params:`) in iCalendar or vCard text, unfolded.
|
||||
fn property(text: &str, name: &str) -> Option<String> {
|
||||
|
||||
Reference in New Issue
Block a user