1use crate::error::{AppError, Result};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
6pub enum MessageStatus {
7 Triage,
8 Case,
9 Archived,
10 Spam,
11 Trashed,
12 DeletedRemote,
13 Sent,
14 Draft,
15 Flagged,
16 PushQueued,
17}
18
19impl MessageStatus {
20 pub fn parse(value: &str) -> Result<Self> {
21 match value.trim() {
22 "" | "triage" => Ok(Self::Triage),
23 "case" => Ok(Self::Case),
24 "archived" => Ok(Self::Archived),
25 "spam" => Ok(Self::Spam),
26 "trashed" => Ok(Self::Trashed),
27 "deleted_remote" => Ok(Self::DeletedRemote),
28 "sent" => Ok(Self::Sent),
29 "draft" => Ok(Self::Draft),
30 "flagged" => Ok(Self::Flagged),
31 "push_queued" => Ok(Self::PushQueued),
32 other => Err(AppError::new(
33 "message_status_invalid",
34 format!("unsupported message workspace status: {other}"),
35 )),
36 }
37 }
38
39 pub fn as_str(self) -> &'static str {
40 match self {
41 Self::Triage => "triage",
42 Self::Case => "case",
43 Self::Archived => "archived",
44 Self::Spam => "spam",
45 Self::Trashed => "trashed",
46 Self::DeletedRemote => "deleted_remote",
47 Self::Sent => "sent",
48 Self::Draft => "draft",
49 Self::Flagged => "flagged",
50 Self::PushQueued => "push_queued",
51 }
52 }
53
54 pub fn is_terminal_local(self) -> bool {
55 matches!(
56 self,
57 Self::Spam
58 | Self::Trashed
59 | Self::DeletedRemote
60 | Self::Sent
61 | Self::Draft
62 | Self::Flagged
63 | Self::PushQueued
64 )
65 }
66}
67
68impl fmt::Display for MessageStatus {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.write_str(self.as_str())
71 }
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
75pub enum MailDirection {
76 Inbound,
77 Outbound,
78}
79
80impl MailDirection {
81 pub fn parse(value: &str) -> Result<Self> {
82 match value.trim().to_ascii_lowercase().as_str() {
83 "inbound" | "received" => Ok(Self::Inbound),
84 "outbound" | "sent" => Ok(Self::Outbound),
85 other => Err(AppError::new(
86 "mail_direction_invalid",
87 format!("unsupported mail direction: {other}"),
88 )),
89 }
90 }
91
92 pub fn as_str(self) -> &'static str {
93 match self {
94 Self::Inbound => "inbound",
95 Self::Outbound => "outbound",
96 }
97 }
98}
99
100#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
101#[serde(deny_unknown_fields)]
102pub struct MessageFile {
103 pub schema_name: String,
104 pub schema_version: u64,
105 pub message_id: String,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub rfc822_message_id: Option<String>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub in_reply_to: Option<String>,
110 #[serde(default, skip_serializing_if = "Vec::is_empty")]
111 pub references: Vec<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub remote: Option<RemoteState>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub direction: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub subject: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub from: Option<String>,
120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
121 pub to: Vec<String>,
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
123 pub cc: Vec<String>,
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
125 pub bcc: Vec<String>,
126 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 pub reply_to: Vec<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub sender: Option<String>,
130 #[serde(default, skip_serializing_if = "Vec::is_empty")]
131 pub delivered_to: Vec<String>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub x_original_to: Vec<String>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
135 pub envelope_to: Vec<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub list_id: Option<String>,
138 #[serde(default, skip_serializing_if = "Vec::is_empty")]
139 pub mailing_list_headers: Vec<String>,
140 #[serde(default)]
141 pub authentication: MessageAuthentication,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub received_rfc3339: Option<String>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub sent_rfc3339: Option<String>,
146 #[serde(default)]
147 pub body_text: String,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub eml_path: Option<String>,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
151 pub attachments: Vec<AttachmentRef>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub contact: Option<MessageContact>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub identity: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub identity_email: Option<String>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub identity_match: Option<String>,
163 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub identity_candidates: Vec<String>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub observed_recipient_emails: Vec<String>,
167 pub workspace: WorkspaceState,
168}
169
170#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
172#[serde(deny_unknown_fields)]
173pub struct MessageContact {
174 pub contact_uid: String,
175 pub display_name: String,
176}
177
178#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
184#[serde(rename_all = "snake_case")]
185pub enum AuthVerdict {
186 Pass,
187 Fail,
188 SoftFail,
189 Neutral,
190 None,
191 TempError,
192 PermError,
193 #[default]
194 Missing,
195}
196
197impl AuthVerdict {
198 pub fn as_str(self) -> &'static str {
199 match self {
200 AuthVerdict::Pass => "pass",
201 AuthVerdict::Fail => "fail",
202 AuthVerdict::SoftFail => "softfail",
203 AuthVerdict::Neutral => "neutral",
204 AuthVerdict::None => "none",
205 AuthVerdict::TempError => "temperror",
206 AuthVerdict::PermError => "permerror",
207 AuthVerdict::Missing => "missing",
208 }
209 }
210}
211
212#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
215#[serde(rename_all = "snake_case")]
216pub enum AuthAlignment {
217 Aligned,
218 Mismatch,
219 #[default]
220 Unknown,
221}
222
223impl AuthAlignment {
224 pub fn as_str(self) -> &'static str {
225 match self {
226 AuthAlignment::Aligned => "aligned",
227 AuthAlignment::Mismatch => "mismatch",
228 AuthAlignment::Unknown => "unknown",
229 }
230 }
231}
232
233#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
239#[serde(deny_unknown_fields)]
240pub struct MessageAuthentication {
241 #[serde(default)]
242 pub spf: AuthVerdict,
243 #[serde(default)]
244 pub dkim: AuthVerdict,
245 #[serde(default)]
246 pub dmarc: AuthVerdict,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub dmarc_policy: Option<String>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub authenticated_domain: Option<String>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub from_domain: Option<String>,
253 #[serde(default)]
254 pub alignment: AuthAlignment,
255 #[serde(default, skip_serializing_if = "Vec::is_empty")]
256 pub raw: Vec<String>,
257}
258
259impl MessageAuthentication {
260 pub fn has_results(&self) -> bool {
262 !self.raw.is_empty()
263 }
264
265 pub fn is_warning(&self) -> bool {
269 let hard = [self.spf, self.dkim, self.dmarc]
270 .into_iter()
271 .any(|v| matches!(v, AuthVerdict::Fail | AuthVerdict::PermError));
272 let aligned_failure = self.alignment == AuthAlignment::Mismatch
273 && [self.spf, self.dkim, self.dmarc]
274 .into_iter()
275 .any(|v| v == AuthVerdict::Pass);
276 hard || aligned_failure
277 }
278}
279
280#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
281#[serde(deny_unknown_fields)]
282pub struct ImapRef {
283 pub mailbox_name: String,
284 pub uid_validity: u64,
285 pub uid: u64,
286}
287
288#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
289#[serde(deny_unknown_fields)]
290pub struct RemoteState {
291 #[serde(default, skip_serializing_if = "Vec::is_empty")]
292 pub locations: Vec<RemoteLocation>,
293}
294
295#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
296#[serde(deny_unknown_fields)]
297pub struct RemoteLocation {
298 pub mailbox_name: String,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub mailbox_id: Option<String>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub uid_validity: Option<u64>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub uid: Option<u64>,
305 #[serde(default, skip_serializing_if = "Vec::is_empty")]
306 pub flags: Vec<String>,
307 pub observed_rfc3339: String,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub missing_rfc3339: Option<String>,
310}
311
312#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
313#[serde(deny_unknown_fields)]
314pub struct AttachmentRef {
315 pub part_id: String,
316 pub filename: String,
317 pub content_type: String,
318 pub size_bytes: u64,
319 #[serde(default)]
320 pub fetched: bool,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub file_path: Option<String>,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub source_path: Option<String>,
325}
326
327#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
328#[serde(deny_unknown_fields)]
329pub struct WorkspaceState {
330 pub status: String,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub archive_uid: Option<String>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub archived_rfc3339: Option<String>,
335 #[serde(skip_serializing_if = "Option::is_none")]
338 pub origin: Option<String>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub remote_sync: Option<RemoteSyncState>,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub push: Option<WorkspacePushState>,
343}
344
345#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
346#[serde(deny_unknown_fields)]
347pub struct RemoteSyncState {
348 pub archive_eligible: bool,
349 pub checked_rfc3339: String,
350}
351
352#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
353#[serde(deny_unknown_fields)]
354pub struct WorkspacePushState {
355 #[serde(default, skip_serializing_if = "Vec::is_empty")]
356 pub pending: Vec<WorkspacePendingPush>,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub last_completed_rfc3339: Option<String>,
359}
360
361#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
362#[serde(deny_unknown_fields)]
363pub struct WorkspacePendingPush {
364 pub push_id: String,
365 pub kind: String,
366 pub queued_rfc3339: String,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub last_error: Option<String>,
369}