Skip to main content

agent_first_mail/config/
access.rs

1use super::*;
2use agent_first_data::normalize_utc_offset;
3
4impl MailConfig {
5    pub fn get_key(&self, key: &str) -> Result<Value> {
6        match key {
7            "schema_name" => Ok(json!(self.schema_name)),
8            "schema_version" => Ok(json!(self.schema_version)),
9            "imap.host" => Ok(json!(self.imap.host)),
10            "imap.port" => Ok(json!(self.imap.port)),
11            "imap.tls" => Ok(json!(self.imap.tls)),
12            "imap.username" => Ok(json!(self.imap.username)),
13            "imap.password_secret" => Ok(json!(self.imap.password_secret)),
14            "imap.password_secret_env" => Ok(json!(self.imap.password_secret_env)),
15            "mailboxes" => Ok(json!(self.mailboxes)),
16            key if key.starts_with("mailboxes.") => self.get_mailbox_key(key),
17            "actions" => Ok(json!(self.actions)),
18            "actions.pull.default_mailbox_ids" => Ok(json!(self.actions.pull.default_mailbox_ids)),
19            key if key.starts_with("actions.pull.by_mailbox_id.") => {
20                self.get_pull_mailbox_action_key(key)
21            }
22            key if key.starts_with("actions.message.archive.by_source_mailbox_id.") => {
23                self.get_archive_action_key(key)
24            }
25            "actions.case.add.steps" => Ok(json!(self.actions.case_add.steps)),
26            "actions.draft.save.steps" => Ok(json!(self.actions.draft_save.steps)),
27            "actions.draft.send.steps" => Ok(json!(self.actions.draft_send.steps)),
28            "actions.message.spam.steps" => Ok(json!(self.actions.message_spam.steps)),
29            "actions.message.trash.steps" => Ok(json!(self.actions.message_trash.steps)),
30            "case.default_group" => Ok(json!(self.case.default_group)),
31            "archive" => Ok(json!(self.archive)),
32            "archive.message_index.item_fields" => {
33                Ok(json!(self.archive.message_index.item_fields))
34            }
35            "archive.message_index.sort" => Ok(json!(self.archive.message_index.sort.as_str())),
36            "audit.reason_mode" => Ok(json!(self.audit.reason_mode.as_str())),
37            "smtp.host" => Ok(json!(self.smtp.host)),
38            "smtp.port" => Ok(json!(self.smtp.port)),
39            "smtp.starttls" => Ok(json!(self.smtp.starttls)),
40            "smtp.tls_wrapper" => Ok(json!(self.smtp.tls_wrapper)),
41            "smtp.username" => Ok(json!(self.smtp.username)),
42            "smtp.password_secret" => Ok(json!(self.smtp.password_secret)),
43            "smtp.password_secret_env" => Ok(json!(self.smtp.password_secret_env)),
44            "smtp.from" => Ok(json!(self.smtp.from)),
45            "workspace.language_bcp47" => Ok(json!(self.workspace.language_bcp47)),
46            "workspace.timezone_utc_offset" => Ok(json!(self.workspace.timezone_utc_offset)),
47            _ => Err(AppError::new(
48                "invalid_request",
49                format!("unknown config key: {key}"),
50            )),
51        }
52    }
53
54    fn get_mailbox_key(&self, key: &str) -> Result<Value> {
55        let rest = &key["mailboxes.".len()..];
56        let (id, field) = rest.split_once('.').ok_or_else(|| {
57            AppError::new(
58                "invalid_request",
59                "mailboxes key expects mailboxes.<id>.<field>",
60            )
61        })?;
62        let mailbox = self.mailbox(id)?;
63        match field {
64            "mailbox_name" => Ok(json!(mailbox.mailbox_name)),
65            "special_use" => Ok(json!(mailbox.special_use)),
66            _ => Err(AppError::new(
67                "invalid_request",
68                format!("unknown config key: {key}"),
69            )),
70        }
71    }
72
73    pub fn set_key(&mut self, key: &str, values: &[String]) -> Result<()> {
74        if values.is_empty() {
75            return Err(AppError::new(
76                "invalid_request",
77                "config set requires at least one value",
78            ));
79        }
80        match key {
81            "imap.host" => self.imap.host = Some(single(values, key)?),
82            "imap.port" => self.imap.port = parse_u16(&single(values, key)?, key)?,
83            "imap.tls" => self.imap.tls = parse_bool(&single(values, key)?, key)?,
84            "imap.username" => self.imap.username = Some(single(values, key)?),
85            "imap.password_secret" => {
86                self.imap.password_secret = Some(single(values, key)?);
87                self.imap.password_secret_env = None;
88            }
89            "imap.password_secret_env" => {
90                self.imap.password_secret_env = Some(single(values, key)?);
91                self.imap.password_secret = None;
92            }
93            "actions.pull.default_mailbox_ids" => {
94                let mut ids = Vec::new();
95                for value in values {
96                    validate_config_id("actions.pull.default_mailbox_ids id", value)?;
97                    if !ids.iter().any(|existing| existing == value) {
98                        ids.push(value.clone());
99                    }
100                }
101                self.actions.pull.default_mailbox_ids = ids;
102            }
103            "case.default_group" => self.case.default_group = single(values, key)?,
104            "audit.reason_mode" => {
105                self.audit.reason_mode = ReasonMode::parse(&single(values, key)?)?;
106            }
107            "smtp.host" => self.smtp.host = Some(single(values, key)?),
108            "smtp.port" => self.smtp.port = parse_u16(&single(values, key)?, key)?,
109            "smtp.starttls" => self.smtp.starttls = parse_bool(&single(values, key)?, key)?,
110            "smtp.tls_wrapper" => self.smtp.tls_wrapper = parse_bool(&single(values, key)?, key)?,
111            "smtp.username" => self.smtp.username = Some(single(values, key)?),
112            "smtp.password_secret" => {
113                self.smtp.password_secret = Some(single(values, key)?);
114                self.smtp.password_secret_env = None;
115            }
116            "smtp.password_secret_env" => {
117                self.smtp.password_secret_env = Some(single(values, key)?);
118                self.smtp.password_secret = None;
119            }
120            "smtp.from" => self.smtp.from = Some(single(values, key)?),
121            "workspace.language_bcp47" => {
122                let value = single(values, key)?;
123                self.workspace.language_bcp47 = parse_optional_language_bcp47(&value)?;
124            }
125            "workspace.timezone_utc_offset" => {
126                let value = single(values, key)?;
127                self.workspace.timezone_utc_offset = parse_optional_timezone_utc_offset(&value)?;
128            }
129            key if key.starts_with("mailboxes.") => self.set_mailbox_key(key, values)?,
130            key if key.starts_with("actions.pull.by_mailbox_id.") => {
131                self.set_pull_mailbox_action_key(key, values)?
132            }
133            key if key.starts_with("actions.message.archive.by_source_mailbox_id.") => {
134                self.set_archive_action_key(key, values)?
135            }
136            "archive.message_index.item_fields" => {
137                self.archive.message_index.item_fields = values
138                    .iter()
139                    .map(|value| ArchiveMessageIndexField::parse(value))
140                    .collect::<Result<Vec<_>>>()?;
141            }
142            "archive.message_index.sort" => {
143                self.archive.message_index.sort =
144                    ArchiveMessageIndexSort::parse(&single(values, key)?)?;
145            }
146            _ => {
147                return Err(AppError::new(
148                    "invalid_request",
149                    format!("unknown config key: {key}"),
150                ))
151            }
152        }
153        self.validate()?;
154        Ok(())
155    }
156
157    fn set_mailbox_key(&mut self, key: &str, values: &[String]) -> Result<()> {
158        let rest = &key["mailboxes.".len()..];
159        let (id, field) = rest.split_once('.').ok_or_else(|| {
160            AppError::new(
161                "invalid_request",
162                "mailboxes key expects mailboxes.<id>.<field>",
163            )
164        })?;
165        validate_config_id("mailboxes id", id)?;
166        let mailbox = self
167            .mailboxes
168            .entry(id.to_string())
169            .or_insert_with(ImapMailboxConfig::empty);
170        match field {
171            "mailbox_name" => {
172                mailbox.mailbox_name = Some(single(values, key)?);
173                mailbox.special_use = None;
174            }
175            "special_use" => {
176                mailbox.special_use = Some(single(values, key)?);
177                mailbox.mailbox_name = None;
178            }
179            _ => {
180                return Err(AppError::new(
181                    "invalid_request",
182                    format!("unknown config key: {key}"),
183                ));
184            }
185        }
186        Ok(())
187    }
188
189    fn get_pull_mailbox_action_key(&self, key: &str) -> Result<Value> {
190        let rest = &key["actions.pull.by_mailbox_id.".len()..];
191        let (id, field) = rest.split_once('.').ok_or_else(|| {
192            AppError::new(
193                "invalid_request",
194                "actions pull key expects actions.pull.by_mailbox_id.<id>.<field>",
195            )
196        })?;
197        let action = self.pull_action(id)?;
198        match field {
199            "import_as" => Ok(json!(action.import_as.as_str())),
200            "direction" => Ok(json!(action.direction.as_str())),
201            _ => Err(AppError::new(
202                "invalid_request",
203                format!("unknown config key: {key}"),
204            )),
205        }
206    }
207
208    fn set_pull_mailbox_action_key(&mut self, key: &str, values: &[String]) -> Result<()> {
209        let rest = &key["actions.pull.by_mailbox_id.".len()..];
210        let (id, field) = rest.split_once('.').ok_or_else(|| {
211            AppError::new(
212                "invalid_request",
213                "actions pull key expects actions.pull.by_mailbox_id.<id>.<field>",
214            )
215        })?;
216        validate_config_id("actions.pull.by_mailbox_id id", id)?;
217        let action = self
218            .actions
219            .pull
220            .by_mailbox_id
221            .entry(id.to_string())
222            .or_default();
223        match field {
224            "import_as" => action.import_as = PullImportAs::parse(&single(values, key)?)?,
225            "direction" => action.direction = MailDirection::parse(&single(values, key)?)?,
226            _ => {
227                return Err(AppError::new(
228                    "invalid_request",
229                    format!("unknown config key: {key}"),
230                ));
231            }
232        }
233        Ok(())
234    }
235
236    fn get_archive_action_key(&self, key: &str) -> Result<Value> {
237        let rest = &key["actions.message.archive.by_source_mailbox_id.".len()..];
238        let (id, field) = rest.split_once('.').ok_or_else(|| {
239            AppError::new(
240                "invalid_request",
241                "archive action key expects actions.message.archive.by_source_mailbox_id.<id>.<field>",
242            )
243        })?;
244        let rule = self
245            .actions
246            .message_archive
247            .by_source_mailbox_id
248            .get(id)
249            .ok_or_else(|| {
250                AppError::new(
251                    "unknown_mailbox_id",
252                    format!("unknown archive source mailbox id: {id}"),
253                )
254            })?;
255        match field {
256            "steps" => Ok(json!(rule.steps)),
257            "move_to_mailbox_id" => Ok(json!(first_move_to_mailbox_id(&rule.steps))),
258            _ => Err(AppError::new(
259                "invalid_request",
260                format!("unknown config key: {key}"),
261            )),
262        }
263    }
264
265    fn set_archive_action_key(&mut self, key: &str, values: &[String]) -> Result<()> {
266        let rest = &key["actions.message.archive.by_source_mailbox_id.".len()..];
267        let (id, field) = rest.split_once('.').ok_or_else(|| {
268            AppError::new(
269                "invalid_request",
270                "archive action key expects actions.message.archive.by_source_mailbox_id.<id>.<field>",
271            )
272        })?;
273        validate_config_id("actions.message.archive source id", id)?;
274        let rule = self
275            .actions
276            .message_archive
277            .by_source_mailbox_id
278            .entry(id.to_string())
279            .or_default();
280        match field {
281            "move_to_mailbox_id" | "steps.move_to_mailbox_id" => {
282                rule.steps = vec![ActionStep::move_to_mailbox_id(single(values, key)?)];
283            }
284            "steps" if values == ["none"] || values == ["[]"] => {
285                rule.steps.clear();
286            }
287            _ => {
288                return Err(AppError::new(
289                    "invalid_request",
290                    format!("unknown config key: {key}"),
291                ));
292            }
293        }
294        Ok(())
295    }
296}
297
298fn parse_optional_language_bcp47(value: &str) -> Result<Option<String>> {
299    let trimmed = value.trim();
300    if trimmed.eq_ignore_ascii_case("null") {
301        return Ok(None);
302    }
303    validate_language_bcp47("workspace.language_bcp47", trimmed, "invalid_request")?;
304    Ok(Some(trimmed.to_string()))
305}
306
307fn parse_optional_timezone_utc_offset(value: &str) -> Result<Option<String>> {
308    let trimmed = value.trim();
309    if trimmed.eq_ignore_ascii_case("null") {
310        return Ok(None);
311    }
312    normalize_utc_offset(trimmed).map(Some).ok_or_else(|| {
313        AppError::new(
314            "invalid_request",
315            "workspace.timezone_utc_offset expects UTC or a fixed offset like +08:00",
316        )
317    })
318}