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 pub workspace: WorkspaceState,
153}
154
155#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
161#[serde(rename_all = "snake_case")]
162pub enum AuthVerdict {
163 Pass,
164 Fail,
165 SoftFail,
166 Neutral,
167 None,
168 TempError,
169 PermError,
170 #[default]
171 Missing,
172}
173
174impl AuthVerdict {
175 pub fn as_str(self) -> &'static str {
176 match self {
177 AuthVerdict::Pass => "pass",
178 AuthVerdict::Fail => "fail",
179 AuthVerdict::SoftFail => "softfail",
180 AuthVerdict::Neutral => "neutral",
181 AuthVerdict::None => "none",
182 AuthVerdict::TempError => "temperror",
183 AuthVerdict::PermError => "permerror",
184 AuthVerdict::Missing => "missing",
185 }
186 }
187}
188
189#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
192#[serde(rename_all = "snake_case")]
193pub enum AuthAlignment {
194 Aligned,
195 Mismatch,
196 #[default]
197 Unknown,
198}
199
200impl AuthAlignment {
201 pub fn as_str(self) -> &'static str {
202 match self {
203 AuthAlignment::Aligned => "aligned",
204 AuthAlignment::Mismatch => "mismatch",
205 AuthAlignment::Unknown => "unknown",
206 }
207 }
208}
209
210#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
216#[serde(deny_unknown_fields)]
217pub struct MessageAuthentication {
218 #[serde(default)]
219 pub spf: AuthVerdict,
220 #[serde(default)]
221 pub dkim: AuthVerdict,
222 #[serde(default)]
223 pub dmarc: AuthVerdict,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub dmarc_policy: Option<String>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub authenticated_domain: Option<String>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub from_domain: Option<String>,
230 #[serde(default)]
231 pub alignment: AuthAlignment,
232 #[serde(default, skip_serializing_if = "Vec::is_empty")]
233 pub raw: Vec<String>,
234}
235
236impl MessageAuthentication {
237 pub fn has_results(&self) -> bool {
239 !self.raw.is_empty()
240 }
241
242 pub fn is_warning(&self) -> bool {
246 let hard = [self.spf, self.dkim, self.dmarc]
247 .into_iter()
248 .any(|v| matches!(v, AuthVerdict::Fail | AuthVerdict::PermError));
249 let aligned_failure = self.alignment == AuthAlignment::Mismatch
250 && [self.spf, self.dkim, self.dmarc]
251 .into_iter()
252 .any(|v| v == AuthVerdict::Pass);
253 hard || aligned_failure
254 }
255}
256
257#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
258#[serde(deny_unknown_fields)]
259pub struct ImapRef {
260 pub mailbox_name: String,
261 pub uid_validity: u64,
262 pub uid: u64,
263}
264
265#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
266#[serde(deny_unknown_fields)]
267pub struct RemoteState {
268 #[serde(default, skip_serializing_if = "Vec::is_empty")]
269 pub locations: Vec<RemoteLocation>,
270}
271
272#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
273#[serde(deny_unknown_fields)]
274pub struct RemoteLocation {
275 pub mailbox_name: String,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub mailbox_id: Option<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub uid_validity: Option<u64>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub uid: Option<u64>,
282 #[serde(default, skip_serializing_if = "Vec::is_empty")]
283 pub flags: Vec<String>,
284 pub observed_rfc3339: String,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub missing_rfc3339: Option<String>,
287}
288
289#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
290#[serde(deny_unknown_fields)]
291pub struct AttachmentRef {
292 pub part_id: String,
293 pub filename: String,
294 pub content_type: String,
295 pub size_bytes: u64,
296 #[serde(default)]
297 pub fetched: bool,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub file_path: Option<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub source_path: Option<String>,
302}
303
304#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
305#[serde(deny_unknown_fields)]
306pub struct WorkspaceState {
307 pub status: String,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub archive_uid: Option<String>,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub archived_rfc3339: Option<String>,
312 #[serde(skip_serializing_if = "Option::is_none")]
315 pub origin: Option<String>,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub remote_sync: Option<RemoteSyncState>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub push: Option<WorkspacePushState>,
320}
321
322#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
323#[serde(deny_unknown_fields)]
324pub struct RemoteSyncState {
325 pub archive_eligible: bool,
326 pub checked_rfc3339: String,
327}
328
329#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
330#[serde(deny_unknown_fields)]
331pub struct WorkspacePushState {
332 #[serde(default, skip_serializing_if = "Vec::is_empty")]
333 pub pending: Vec<WorkspacePendingPush>,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub last_completed_rfc3339: Option<String>,
336}
337
338#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
339#[serde(deny_unknown_fields)]
340pub struct WorkspacePendingPush {
341 pub push_id: String,
342 pub kind: String,
343 pub queued_rfc3339: String,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub last_error: Option<String>,
346}