Skip to main content

swarmhive_api_types/
mail.rs

1//! Mail HTTP DTOs —— SMTP provider / 模板 / 日志 / 状态。
2//!
3//! 从 `swarmhive-server::routes::mail` 内联定义提升到此(`add-cli-storage-mail-admin`),
4//! 让 CLI(不依赖 entity / sea-orm)也能消费。entity 承担 `From<&Model>` 转换。
5//!
6//! 三个枚举统一 `#[serde(rename_all = "lowercase")]` + `ToSchema`,wire 为 `smtp` /
7//! `starttls` / `tls` / `none` / `sent` / `failed`,与 entity `string_value` 一致
8//! (`MailLogStatus` 历史上漏了 rename,本次一并统一成小写)。DTO 字段**直接引用枚举**
9//! (不再 `value_type=String`),OpenAPI 因此呈现为精确的字面量枚举——admin 类型更紧、
10//! 更优雅(schema.gen.ts 随之收紧,属可接受的破坏性变更)。
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use utoipa::ToSchema;
15use uuid::Uuid;
16
17/// 邮件 provider 类型(目前仅 SMTP)。
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
19#[serde(rename_all = "lowercase")]
20pub enum ProviderKind {
21    Smtp,
22}
23
24/// SMTP 加密方式。
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
26#[serde(rename_all = "lowercase")]
27pub enum SmtpEncryption {
28    StartTls,
29    Tls,
30    None,
31}
32
33/// 一封邮件日志的投递状态。
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
35#[serde(rename_all = "lowercase")]
36pub enum MailLogStatus {
37    Sent,
38    Failed,
39}
40
41/// provider 的列表 / 详情表示。secret 永不返回——`password_set` 表示是否已存密码。
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
43pub struct MailProviderView {
44    pub id: Uuid,
45    pub name: String,
46    pub kind: ProviderKind,
47    pub active: bool,
48    pub host: String,
49    pub port: i32,
50    pub username: Option<String>,
51    /// `true` 表示已配置(加密的)密码;密文永不出 wire。
52    pub password_set: bool,
53    pub encryption: SmtpEncryption,
54    pub from_email: String,
55    pub from_name: Option<String>,
56    pub reply_to: Option<String>,
57    pub created_at: DateTime<Utc>,
58    pub updated_at: DateTime<Utc>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
62pub struct CreateProviderReq {
63    pub name: String,
64    pub host: String,
65    pub port: i32,
66    pub encryption: SmtpEncryption,
67    pub from_email: String,
68    #[serde(default)]
69    pub from_name: Option<String>,
70    #[serde(default)]
71    pub reply_to: Option<String>,
72    #[serde(default)]
73    pub username: Option<String>,
74    /// 明文 SMTP 密码。server 落库前加密;明文永不记录 / 返回。
75    #[serde(default)]
76    pub password: Option<String>,
77}
78
79/// 全可选 patch。`from_name` / `reply_to` / `username` 用双层 `Option` 区分「缺省=保留」
80/// 与「null=清空」。`password` `Some(明文)` 设置 / 轮换,缺省 = 不变。
81#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
82pub struct UpdateProviderReq {
83    #[serde(default)]
84    pub name: Option<String>,
85    #[serde(default)]
86    pub host: Option<String>,
87    #[serde(default)]
88    pub port: Option<i32>,
89    #[serde(default)]
90    pub encryption: Option<SmtpEncryption>,
91    #[serde(default)]
92    pub from_email: Option<String>,
93    #[serde(default)]
94    pub from_name: Option<Option<String>>,
95    #[serde(default)]
96    pub reply_to: Option<Option<String>>,
97    #[serde(default)]
98    pub username: Option<Option<String>>,
99    #[serde(default)]
100    pub password: Option<String>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
104pub struct MailTemplateView {
105    pub id: Uuid,
106    pub event_name: String,
107    pub locale: String,
108    pub subject: String,
109    pub html_body: String,
110    pub text_body: String,
111    pub updated_at: DateTime<Utc>,
112}
113
114#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
115pub struct UpdateTemplateReq {
116    #[serde(default)]
117    pub subject: Option<String>,
118    #[serde(default)]
119    pub html_body: Option<String>,
120    #[serde(default)]
121    pub text_body: Option<String>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
125pub struct PreviewReq {
126    /// 传进 minijinja 渲染的任意 key/value 上下文。
127    pub sample: serde_json::Value,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
131pub struct PreviewResp {
132    pub subject: String,
133    pub html_body: String,
134    pub text_body: String,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
138pub struct MailLogView {
139    pub id: Uuid,
140    pub to: String,
141    pub template_id: Option<Uuid>,
142    pub provider_id: Option<Uuid>,
143    pub status: MailLogStatus,
144    pub error: Option<String>,
145    pub sent_at: DateTime<Utc>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
149pub struct MailStatusResp {
150    /// `"smtp"` 表示有活跃 provider,`"console"` 是 dev / fallback 传输。
151    pub transport: String,
152    pub fallback_mode: bool,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
156pub struct TouchedResp {
157    pub touched: usize,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
161pub struct TestSentResp {
162    /// 自检邮件发往的地址(当前登录 Principal 的邮箱)。
163    pub to: String,
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn enum_wire_strings_match_entity_string_values() {
172        assert_eq!(serde_json::to_value(ProviderKind::Smtp).unwrap(), "smtp");
173        assert_eq!(
174            serde_json::to_value(SmtpEncryption::StartTls).unwrap(),
175            "starttls"
176        );
177        assert_eq!(serde_json::to_value(SmtpEncryption::Tls).unwrap(), "tls");
178        assert_eq!(serde_json::to_value(SmtpEncryption::None).unwrap(), "none");
179        assert_eq!(serde_json::to_value(MailLogStatus::Sent).unwrap(), "sent");
180        assert_eq!(
181            serde_json::to_value(MailLogStatus::Failed).unwrap(),
182            "failed"
183        );
184        // round-trip
185        let e: SmtpEncryption = serde_json::from_value(serde_json::json!("starttls")).unwrap();
186        assert_eq!(e, SmtpEncryption::StartTls);
187    }
188}