Skip to main content

agent_first_mail/config/
workspace.rs

1use super::defaults::{
2    default_case_group, default_contact_group, default_reason_mode, default_smtp_port,
3    default_timezone_utc_offset, default_timezone_utc_offset_option, default_true,
4};
5use super::validation::validate_language_bcp47;
6use crate::error::{AppError, Result};
7use agent_first_data::normalize_utc_offset;
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
11#[serde(deny_unknown_fields)]
12pub struct CaseSection {
13    #[serde(default = "default_case_group")]
14    pub default_group: String,
15}
16
17impl Default for CaseSection {
18    fn default() -> Self {
19        Self {
20            default_group: default_case_group(),
21        }
22    }
23}
24
25#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
26#[serde(deny_unknown_fields)]
27pub struct ContactSection {
28    #[serde(default = "default_contact_group")]
29    pub default_group: String,
30}
31
32impl Default for ContactSection {
33    fn default() -> Self {
34        Self {
35            default_group: default_contact_group(),
36        }
37    }
38}
39
40#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
41#[serde(deny_unknown_fields)]
42pub struct AuditSection {
43    #[serde(default = "default_reason_mode")]
44    pub reason_mode: ReasonMode,
45}
46
47impl Default for AuditSection {
48    fn default() -> Self {
49        Self {
50            reason_mode: default_reason_mode(),
51        }
52    }
53}
54
55#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum ReasonMode {
58    Required,
59    Optional,
60}
61
62impl ReasonMode {
63    pub fn as_str(self) -> &'static str {
64        match self {
65            ReasonMode::Required => "required",
66            ReasonMode::Optional => "optional",
67        }
68    }
69
70    pub(super) fn parse(value: &str) -> Result<Self> {
71        match value {
72            "required" => Ok(Self::Required),
73            "optional" => Ok(Self::Optional),
74            _ => Err(AppError::new(
75                "invalid_request",
76                "audit.reason_mode expects required or optional",
77            )),
78        }
79    }
80}
81
82#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
83#[serde(deny_unknown_fields)]
84pub struct SmtpSection {
85    pub host: Option<String>,
86    #[serde(default = "default_smtp_port")]
87    pub port: u16,
88    #[serde(default = "default_true")]
89    pub starttls: bool,
90    #[serde(default)]
91    pub tls_wrapper: bool,
92    pub username: Option<String>,
93    pub password_secret: Option<String>,
94    pub password_secret_env: Option<String>,
95}
96
97impl Default for SmtpSection {
98    fn default() -> Self {
99        Self {
100            host: None,
101            port: default_smtp_port(),
102            starttls: true,
103            tls_wrapper: false,
104            username: None,
105            password_secret: None,
106            password_secret_env: None,
107        }
108    }
109}
110
111#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
112#[serde(deny_unknown_fields)]
113pub struct WorkspaceSection {
114    #[serde(default)]
115    pub language_bcp47: Option<String>,
116    #[serde(default = "default_timezone_utc_offset_option")]
117    pub timezone_utc_offset: Option<String>,
118}
119
120impl Default for WorkspaceSection {
121    fn default() -> Self {
122        Self {
123            language_bcp47: None,
124            timezone_utc_offset: Some(default_timezone_utc_offset()),
125        }
126    }
127}
128
129impl WorkspaceSection {
130    pub(super) fn validate(&self) -> Result<()> {
131        if let Some(language) = self.language_bcp47.as_deref() {
132            validate_language_bcp47("workspace.language_bcp47", language, "config_invalid")?;
133        }
134        if let Some(offset) = self.timezone_utc_offset.as_deref() {
135            let normalized = normalize_utc_offset(offset).ok_or_else(|| {
136                AppError::new(
137                    "config_invalid",
138                    "workspace.timezone_utc_offset expects UTC or a fixed offset like +08:00",
139                )
140            })?;
141            if normalized != offset {
142                return Err(AppError::new(
143                    "config_invalid",
144                    "workspace.timezone_utc_offset must be canonical UTC or ±HH:MM",
145                ));
146            }
147        }
148        Ok(())
149    }
150}
151
152#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
153pub enum TemplateLanguage {
154    #[default]
155    EnUs,
156    ZhCn,
157}
158
159impl TemplateLanguage {
160    pub const ALL: [Self; 2] = [Self::EnUs, Self::ZhCn];
161
162    pub fn as_str(self) -> &'static str {
163        match self {
164            TemplateLanguage::EnUs => "en-US",
165            TemplateLanguage::ZhCn => "zh-CN",
166        }
167    }
168
169    pub fn from_bcp47(value: &str) -> Self {
170        let lower = value.trim().to_ascii_lowercase();
171        if lower == "zh" || lower.starts_with("zh-") {
172            Self::ZhCn
173        } else {
174            Self::EnUs
175        }
176    }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct SmtpConfig {
181    pub host: String,
182    pub port: u16,
183    pub starttls: bool,
184    pub tls_wrapper: bool,
185    pub username: Option<String>,
186    pub password_secret: Option<String>,
187}