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}