Skip to main content

agent_first_mail/store/
purge.rs

1use super::*;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4enum PurgeDisposition {
5    Spam,
6    Trash,
7    Deleted,
8}
9
10impl PurgeDisposition {
11    fn status(self) -> MessageStatus {
12        match self {
13            Self::Spam => MessageStatus::Spam,
14            Self::Trash => MessageStatus::Trashed,
15            Self::Deleted => MessageStatus::DeletedRemote,
16        }
17    }
18
19    fn target(self) -> &'static str {
20        match self {
21            Self::Spam => "spam",
22            Self::Trash => "trash",
23            Self::Deleted => "deleted",
24        }
25    }
26}
27
28#[derive(Default)]
29struct PurgeTotals {
30    purged_message_ids: Vec<String>,
31    purged_spam_count: usize,
32    purged_trash_count: usize,
33    purged_deleted_count: usize,
34    skipped_referenced_message_ids: Vec<String>,
35    skipped_recent_message_ids: Vec<String>,
36}
37
38impl PurgeTotals {
39    fn record_purged(&mut self, target: PurgeDisposition, message_id: String) {
40        match target {
41            PurgeDisposition::Spam => self.purged_spam_count += 1,
42            PurgeDisposition::Trash => self.purged_trash_count += 1,
43            PurgeDisposition::Deleted => self.purged_deleted_count += 1,
44        }
45        self.purged_message_ids.push(message_id);
46    }
47}
48
49impl Workspace {
50    pub fn purge_spam(&self, older_than_days: u64) -> Result<Value> {
51        self.purge_dispositions(&[PurgeDisposition::Spam], older_than_days)
52    }
53
54    pub fn purge_trash(&self, older_than_days: u64) -> Result<Value> {
55        self.purge_dispositions(&[PurgeDisposition::Trash], older_than_days)
56    }
57
58    pub fn purge_deleted(&self, older_than_days: u64) -> Result<Value> {
59        self.purge_dispositions(&[PurgeDisposition::Deleted], older_than_days)
60    }
61
62    pub fn purge_discards(&self, older_than_days: u64) -> Result<Value> {
63        self.purge_dispositions(
64            &[
65                PurgeDisposition::Spam,
66                PurgeDisposition::Trash,
67                PurgeDisposition::Deleted,
68            ],
69            older_than_days,
70        )
71    }
72
73    fn purge_dispositions(
74        &self,
75        targets: &[PurgeDisposition],
76        older_than_days: u64,
77    ) -> Result<Value> {
78        self.require_workspace()?;
79        let cutoff = Utc::now() - Duration::days(older_than_days as i64);
80        let targets_by_status = targets
81            .iter()
82            .copied()
83            .map(|target| (target.status(), target))
84            .collect::<BTreeMap<_, _>>();
85        let mut totals = PurgeTotals::default();
86
87        for path in message_json_paths(&self.root)? {
88            let message = read_message(&path)?;
89            let status = MessageStatus::parse(&message.workspace.status)?;
90            let Some(target) = targets_by_status.get(&status).copied() else {
91                continue;
92            };
93            let age_time = self.message_purge_age_time(&message)?;
94            if age_time > cutoff {
95                totals
96                    .skipped_recent_message_ids
97                    .push(message.message_id.clone());
98                continue;
99            }
100            if self.message_id_is_referenced(&message.message_id)? {
101                totals
102                    .skipped_referenced_message_ids
103                    .push(message.message_id.clone());
104                continue;
105            }
106            let message_id = message.message_id.clone();
107            purge_message_artifacts(&self.root, &message_id)?;
108            totals.record_purged(target, message_id);
109        }
110
111        let dispositions = self.refresh_disposition_views()?;
112        let mut result = json!({
113            "code": "purged",
114            "target": purge_target_name(targets),
115            "targets": targets.iter().map(|target| target.target()).collect::<Vec<_>>(),
116            "older_than_days": older_than_days,
117            "purged_count": totals.purged_message_ids.len(),
118            "purged_message_ids": totals.purged_message_ids,
119            "purged_spam_count": totals.purged_spam_count,
120            "purged_trash_count": totals.purged_trash_count,
121            "purged_deleted_count": totals.purged_deleted_count,
122            "skipped_referenced_count": totals.skipped_referenced_message_ids.len(),
123            "skipped_referenced_message_ids": totals.skipped_referenced_message_ids,
124            "skipped_recent_count": totals.skipped_recent_message_ids.len(),
125            "skipped_recent_message_ids": totals.skipped_recent_message_ids,
126        });
127        merge_disposition_refresh_into_purge(&mut result, &dispositions);
128        Ok(result)
129    }
130
131    fn message_purge_age_time(&self, message: &MessageFile) -> Result<DateTime<Utc>> {
132        let value = message_state_updated_rfc3339(&self.root, &message.message_id)?
133            .or_else(|| message.received_rfc3339.clone())
134            .or_else(|| message.sent_rfc3339.clone())
135            .unwrap_or_else(now_rfc3339);
136        Ok(DateTime::parse_from_rfc3339(&value)
137            .map(|time| time.with_timezone(&Utc))
138            .unwrap_or_else(|_| Utc::now()))
139    }
140}
141
142fn purge_target_name(targets: &[PurgeDisposition]) -> &'static str {
143    if targets == [PurgeDisposition::Spam] {
144        "spam"
145    } else if targets == [PurgeDisposition::Trash] {
146        "trash"
147    } else if targets == [PurgeDisposition::Deleted] {
148        "deleted"
149    } else {
150        "discards"
151    }
152}
153
154fn merge_disposition_refresh_into_purge(purge: &mut Value, dispositions: &Value) {
155    let Some(purge_obj) = purge.as_object_mut() else {
156        return;
157    };
158    let Some(disposition_obj) = dispositions.as_object() else {
159        return;
160    };
161    for key in [
162        "spam_count",
163        "spam_written_count",
164        "stale_spam_removed_count",
165        "trash_count",
166        "trash_written_count",
167        "stale_trash_removed_count",
168        "deleted_count",
169        "deleted_written_count",
170        "stale_deleted_removed_count",
171    ] {
172        if let Some(value) = disposition_obj.get(key) {
173            purge_obj.insert(key.to_string(), value.clone());
174        }
175    }
176}