Skip to main content

agent_first_mail/config/
workspace.rs

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