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}