1use super::validation::{validate_config_id, validate_steps};
2use super::MailConfig;
3use crate::error::{AppError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9pub enum PullImportAs {
10 #[default]
11 Triage,
12 Spam,
13 Trashed,
14}
15
16impl PullImportAs {
17 pub fn as_str(self) -> &'static str {
18 match self {
19 PullImportAs::Triage => "triage",
20 PullImportAs::Spam => "spam",
21 PullImportAs::Trashed => "trashed",
22 }
23 }
24
25 pub(super) fn parse(value: &str) -> Result<Self> {
26 match value {
27 "triage" => Ok(Self::Triage),
28 "spam" => Ok(Self::Spam),
29 "trashed" => Ok(Self::Trashed),
30 _ => Err(AppError::new(
31 "invalid_request",
32 "actions.pull.by_mailbox_id.<id>.import_as expects one of: triage, spam, trashed",
33 )),
34 }
35 }
36}
37
38#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum MailDirection {
41 #[default]
42 Inbound,
43 Outbound,
44}
45
46impl MailDirection {
47 pub fn as_str(self) -> &'static str {
48 match self {
49 MailDirection::Inbound => "inbound",
50 MailDirection::Outbound => "outbound",
51 }
52 }
53
54 pub(super) fn parse(value: &str) -> Result<Self> {
55 match value {
56 "inbound" => Ok(Self::Inbound),
57 "outbound" => Ok(Self::Outbound),
58 _ => Err(AppError::new(
59 "invalid_request",
60 "direction expects inbound or outbound",
61 )),
62 }
63 }
64}
65
66#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
67#[serde(deny_unknown_fields)]
68pub struct ActionsSection {
69 pub pull: PullActionSection,
70 #[serde(rename = "case.add")]
71 pub case_add: ActionRule,
72 #[serde(rename = "draft.save")]
73 pub draft_save: ActionRule,
74 #[serde(rename = "draft.send")]
75 pub draft_send: ActionRule,
76 #[serde(rename = "message.spam")]
77 pub message_spam: ActionRule,
78 #[serde(rename = "message.trash")]
79 pub message_trash: ActionRule,
80 #[serde(rename = "message.archive")]
81 pub message_archive: MessageArchiveAction,
82}
83
84impl Default for ActionsSection {
85 fn default() -> Self {
86 Self {
87 pull: PullActionSection::default(),
88 case_add: ActionRule { steps: Vec::new() },
89 draft_save: ActionRule {
90 steps: vec![ActionStep::append_to_mailbox_id("drafts")],
91 },
92 draft_send: ActionRule {
93 steps: vec![
94 ActionStep::smtp_send(),
95 ActionStep::append_to_mailbox_id("sent"),
96 ActionStep::add_flags_on(
97 vec!["\\Seen".to_string(), "\\Answered".to_string()],
98 ActionStepOn::ReplyToMessage,
99 ),
100 ],
101 },
102 message_spam: ActionRule {
103 steps: vec![
104 ActionStep::add_flags(vec!["\\Seen".to_string(), "$Junk".to_string()]),
105 ActionStep::move_to_mailbox_id("junk"),
106 ],
107 },
108 message_trash: ActionRule {
109 steps: vec![ActionStep::move_to_mailbox_id("trash")],
110 },
111 message_archive: MessageArchiveAction::default(),
112 }
113 }
114}
115
116impl ActionsSection {
117 pub(super) fn validate(&self, config: &MailConfig) -> Result<()> {
118 self.pull.validate(config)?;
119 validate_steps(config, "actions.case.add.steps", &self.case_add.steps)?;
120 validate_steps(config, "actions.draft.save.steps", &self.draft_save.steps)?;
121 validate_steps(config, "actions.draft.send.steps", &self.draft_send.steps)?;
122 validate_steps(
123 config,
124 "actions.message.spam.steps",
125 &self.message_spam.steps,
126 )?;
127 validate_steps(
128 config,
129 "actions.message.trash.steps",
130 &self.message_trash.steps,
131 )?;
132 for (id, rule) in &self.message_archive.by_source_mailbox_id {
133 validate_config_id("actions.message.archive source id", id)?;
134 if !config.mailboxes.contains_key(id) {
135 return Err(AppError::new(
136 "config_invalid",
137 format!("actions.message.archive.by_source_mailbox_id.{id} references unknown mailbox id"),
138 ));
139 }
140 validate_steps(
141 config,
142 &format!("actions.message.archive.by_source_mailbox_id.{id}.steps"),
143 &rule.steps,
144 )?;
145 }
146 Ok(())
147 }
148}
149
150#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
151#[serde(deny_unknown_fields)]
152pub struct PullActionSection {
153 pub default_mailbox_ids: Vec<String>,
154 pub by_mailbox_id: BTreeMap<String, PullMailboxAction>,
155}
156
157impl Default for PullActionSection {
158 fn default() -> Self {
159 let mut by_mailbox_id = BTreeMap::new();
160 by_mailbox_id.insert(
161 "inbox".to_string(),
162 PullMailboxAction {
163 import_as: PullImportAs::Triage,
164 direction: MailDirection::Inbound,
165 },
166 );
167 by_mailbox_id.insert(
168 "sent".to_string(),
169 PullMailboxAction {
170 import_as: PullImportAs::Triage,
171 direction: MailDirection::Outbound,
172 },
173 );
174 by_mailbox_id.insert(
175 "archive".to_string(),
176 PullMailboxAction {
177 import_as: PullImportAs::Triage,
178 direction: MailDirection::Inbound,
179 },
180 );
181 by_mailbox_id.insert(
182 "junk".to_string(),
183 PullMailboxAction {
184 import_as: PullImportAs::Spam,
185 direction: MailDirection::Inbound,
186 },
187 );
188 by_mailbox_id.insert(
189 "trash".to_string(),
190 PullMailboxAction {
191 import_as: PullImportAs::Trashed,
192 direction: MailDirection::Inbound,
193 },
194 );
195 by_mailbox_id.insert(
196 "drafts".to_string(),
197 PullMailboxAction {
198 import_as: PullImportAs::Triage,
199 direction: MailDirection::Outbound,
200 },
201 );
202 Self {
203 default_mailbox_ids: vec![
206 "inbox".to_string(),
207 "sent".to_string(),
208 "archive".to_string(),
209 "junk".to_string(),
210 "trash".to_string(),
211 ],
212 by_mailbox_id,
213 }
214 }
215}
216
217impl PullActionSection {
218 pub(super) fn validate(&self, config: &MailConfig) -> Result<()> {
219 for id in &self.default_mailbox_ids {
220 validate_config_id("actions.pull.default_mailbox_ids id", id)?;
221 if !config.mailboxes.contains_key(id) {
222 return Err(AppError::new(
223 "config_invalid",
224 format!("actions.pull.default_mailbox_ids references unknown mailbox id: {id}"),
225 ));
226 }
227 if !self.by_mailbox_id.contains_key(id) {
228 return Err(AppError::new(
229 "config_invalid",
230 format!("actions.pull.by_mailbox_id.{id} is required by default_mailbox_ids"),
231 ));
232 }
233 }
234 for id in self.by_mailbox_id.keys() {
235 validate_config_id("actions.pull.by_mailbox_id id", id)?;
236 if !config.mailboxes.contains_key(id) {
237 return Err(AppError::new(
238 "config_invalid",
239 format!("actions.pull.by_mailbox_id.{id} references unknown mailbox id"),
240 ));
241 }
242 }
243 Ok(())
244 }
245}
246
247#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
248#[serde(deny_unknown_fields)]
249pub struct PullMailboxAction {
250 pub import_as: PullImportAs,
251 pub direction: MailDirection,
252}
253
254impl Default for PullMailboxAction {
255 fn default() -> Self {
256 Self {
257 import_as: PullImportAs::Triage,
258 direction: MailDirection::Inbound,
259 }
260 }
261}
262
263#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
264#[serde(deny_unknown_fields)]
265pub struct ActionRule {
266 #[serde(default)]
267 pub steps: Vec<ActionStep>,
268}
269
270#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
271#[serde(deny_unknown_fields)]
272pub struct MessageArchiveAction {
273 #[serde(default)]
274 pub by_source_mailbox_id: BTreeMap<String, ActionRule>,
275}
276
277impl Default for MessageArchiveAction {
278 fn default() -> Self {
279 let mut by_source_mailbox_id = BTreeMap::new();
280 by_source_mailbox_id.insert(
281 "inbox".to_string(),
282 ActionRule {
283 steps: vec![ActionStep::move_to_mailbox_id("archive")],
284 },
285 );
286 for id in ["sent", "archive", "junk", "trash", "drafts"] {
287 by_source_mailbox_id.insert(id.to_string(), ActionRule::default());
288 }
289 Self {
290 by_source_mailbox_id,
291 }
292 }
293}
294
295#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
296#[serde(deny_unknown_fields)]
297pub struct ActionStep {
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 pub add_flags: Vec<String>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub move_to_mailbox_id: Option<String>,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub append_to_mailbox_id: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub smtp_send: Option<BTreeMap<String, String>>,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub on: Option<ActionStepOn>,
308}
309
310impl ActionStep {
311 pub fn add_flags(flags: Vec<String>) -> Self {
312 Self {
313 add_flags: flags,
314 ..Self::default()
315 }
316 }
317
318 pub fn add_flags_on(flags: Vec<String>, on: ActionStepOn) -> Self {
319 Self {
320 add_flags: flags,
321 on: Some(on),
322 ..Self::default()
323 }
324 }
325
326 pub fn move_to_mailbox_id(id: impl Into<String>) -> Self {
327 Self {
328 move_to_mailbox_id: Some(id.into()),
329 ..Self::default()
330 }
331 }
332
333 pub fn append_to_mailbox_id(id: impl Into<String>) -> Self {
334 Self {
335 append_to_mailbox_id: Some(id.into()),
336 ..Self::default()
337 }
338 }
339
340 pub fn smtp_send() -> Self {
341 Self {
342 smtp_send: Some(BTreeMap::new()),
343 ..Self::default()
344 }
345 }
346}
347
348#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
349#[serde(rename_all = "snake_case")]
350pub enum ActionStepOn {
351 ReplyToMessage,
352}