Skip to main content

agent_first_mail/config/
mailbox.rs

1use super::defaults::{default_imap_port, default_true};
2use crate::error::{AppError, Result};
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
6#[serde(deny_unknown_fields)]
7pub struct ImapSection {
8    pub host: Option<String>,
9    #[serde(default = "default_imap_port")]
10    pub port: u16,
11    #[serde(default = "default_true")]
12    pub tls: bool,
13    pub username: Option<String>,
14    pub password_secret: Option<String>,
15    pub password_secret_env: Option<String>,
16}
17
18impl Default for ImapSection {
19    fn default() -> Self {
20        Self {
21            host: None,
22            port: default_imap_port(),
23            tls: true,
24            username: None,
25            password_secret: None,
26            password_secret_env: None,
27        }
28    }
29}
30
31#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
32#[serde(deny_unknown_fields)]
33pub struct ImapMailboxConfig {
34    #[serde(default)]
35    pub mailbox_name: Option<String>,
36    #[serde(default)]
37    pub special_use: Option<String>,
38}
39
40impl ImapMailboxConfig {
41    pub(super) fn empty() -> Self {
42        Self {
43            mailbox_name: None,
44            special_use: None,
45        }
46    }
47
48    pub(super) fn validate(&self, id: &str) -> Result<()> {
49        match (&self.mailbox_name, &self.special_use) {
50            (Some(mailbox), None) if !mailbox.trim().is_empty() => {}
51            (None, Some(special_use)) if !special_use.trim().is_empty() => {}
52            (Some(_), Some(_)) => {
53                return Err(AppError::new(
54                    "config_invalid",
55                    format!("mailboxes.{id}.mailbox_name and special_use are mutually exclusive"),
56                ));
57            }
58            _ => {
59                return Err(AppError::new(
60                    "config_invalid",
61                    format!("mailboxes.{id} must set exactly one of mailbox_name or special_use"),
62                ));
63            }
64        }
65        Ok(())
66    }
67
68    pub fn offline_mailbox_name(&self) -> Option<String> {
69        if let Some(mailbox) = &self.mailbox_name {
70            return Some(mailbox.clone());
71        }
72        self.special_use
73            .as_deref()
74            .and_then(SpecialUseKind::from_attribute)
75            .map(|kind| kind.fallback_names()[0].to_string())
76    }
77
78    pub fn matches_mailbox_offline(&self, mailbox_name: &str) -> bool {
79        if self.mailbox_name.as_deref() == Some(mailbox_name) {
80            return true;
81        }
82        self.special_use
83            .as_deref()
84            .and_then(SpecialUseKind::from_attribute)
85            .is_some_and(|kind| {
86                kind.fallback_names()
87                    .iter()
88                    .any(|name| mailbox_name.eq_ignore_ascii_case(name))
89            })
90    }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum SpecialUseKind {
95    Archive,
96    Junk,
97    Trash,
98    Sent,
99    Drafts,
100    All,
101    Flagged,
102}
103
104impl SpecialUseKind {
105    pub fn as_str(self) -> &'static str {
106        match self {
107            SpecialUseKind::Archive => "archive",
108            SpecialUseKind::Junk => "junk",
109            SpecialUseKind::Trash => "trash",
110            SpecialUseKind::Sent => "sent",
111            SpecialUseKind::Drafts => "drafts",
112            SpecialUseKind::All => "all",
113            SpecialUseKind::Flagged => "flagged",
114        }
115    }
116
117    #[allow(clippy::should_implement_trait)]
118    pub fn from_str(value: &str) -> Option<Self> {
119        match value.to_ascii_lowercase().as_str() {
120            "archive" => Some(SpecialUseKind::Archive),
121            "junk" | "spam" => Some(SpecialUseKind::Junk),
122            "trash" => Some(SpecialUseKind::Trash),
123            "sent" => Some(SpecialUseKind::Sent),
124            "drafts" => Some(SpecialUseKind::Drafts),
125            "all" => Some(SpecialUseKind::All),
126            "flagged" | "flag" => Some(SpecialUseKind::Flagged),
127            _ => None,
128        }
129    }
130
131    pub fn from_attribute(value: &str) -> Option<Self> {
132        special_use_kinds()
133            .iter()
134            .copied()
135            .find(|kind| value.eq_ignore_ascii_case(kind.attribute()))
136    }
137
138    pub fn attribute(self) -> &'static str {
139        match self {
140            SpecialUseKind::Archive => "\\Archive",
141            SpecialUseKind::Junk => "\\Junk",
142            SpecialUseKind::Trash => "\\Trash",
143            SpecialUseKind::Sent => "\\Sent",
144            SpecialUseKind::Drafts => "\\Drafts",
145            SpecialUseKind::All => "\\All",
146            SpecialUseKind::Flagged => "\\Flagged",
147        }
148    }
149
150    pub fn fallback_names(self) -> &'static [&'static str] {
151        match self {
152            SpecialUseKind::Archive => &["Archive", "Archives"],
153            SpecialUseKind::Junk => &["Junk", "Spam", "Junk Email", "Junk E-mail"],
154            SpecialUseKind::Trash => &["Trash", "Deleted Items", "Deleted Messages", "Bin"],
155            SpecialUseKind::Sent => &["Sent", "Sent Mail", "Sent Messages"],
156            SpecialUseKind::Drafts => &["Drafts", "Draft"],
157            SpecialUseKind::All => &["All Mail", "All"],
158            SpecialUseKind::Flagged => &["Flagged", "Starred"],
159        }
160    }
161
162    pub fn can_move_to(self) -> bool {
163        !matches!(self, SpecialUseKind::All | SpecialUseKind::Flagged)
164    }
165}
166
167pub fn special_use_kinds() -> &'static [SpecialUseKind] {
168    &[
169        SpecialUseKind::All,
170        SpecialUseKind::Archive,
171        SpecialUseKind::Drafts,
172        SpecialUseKind::Flagged,
173        SpecialUseKind::Junk,
174        SpecialUseKind::Sent,
175        SpecialUseKind::Trash,
176    ]
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct SpecialUseTarget {
181    pub kind: SpecialUseKind,
182    pub mailbox_name: String,
183    pub source: SpecialUseSource,
184    pub attribute: &'static str,
185    pub flag: Option<String>,
186    pub can_move_to: bool,
187}
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190pub enum SpecialUseSource {
191    Mailboxes,
192    Rfc6154Attribute,
193    FallbackName,
194}
195
196impl SpecialUseSource {
197    pub fn as_str(self) -> &'static str {
198        match self {
199            SpecialUseSource::Mailboxes => "mailboxes",
200            SpecialUseSource::Rfc6154Attribute => "rfc6154_attribute",
201            SpecialUseSource::FallbackName => "fallback_name",
202        }
203    }
204}
205
206#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct ImapConfig {
208    pub host: String,
209    pub port: u16,
210    pub tls: bool,
211    pub username: String,
212    pub password_secret: String,
213    pub mailboxes: Vec<String>,
214}