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