postcrate_core/db/
forwarding.rs1use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use sqlx::{Row, SqlitePool};
6use uuid::Uuid;
7
8use crate::error::{Error, Result};
9use crate::smtp::relay::RelayConfig;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[cfg_attr(feature = "specta", derive(specta::Type))]
13#[serde(rename_all = "camelCase")]
14pub struct ForwardingRule {
15 pub id: String,
16 pub mailbox_id: Option<String>,
18 pub target_addresses: Vec<String>,
19 pub relay: RelayConfig,
20 pub enabled: bool,
21 pub created_at: i64,
22}
23
24#[derive(Debug, Clone, Deserialize)]
25#[cfg_attr(feature = "specta", derive(specta::Type))]
26#[serde(rename_all = "camelCase")]
27pub struct CreateForwardingRule {
28 pub mailbox_id: Option<String>,
29 pub target_addresses: Vec<String>,
30 pub relay: RelayConfig,
31 pub enabled: Option<bool>,
32}
33
34pub(crate) async fn insert(pool: &SqlitePool, input: CreateForwardingRule) -> Result<ForwardingRule> {
35 if input.target_addresses.is_empty() {
36 return Err(Error::Invalid("forwarding rule needs at least one target".into()));
37 }
38 let id = Uuid::new_v4().to_string();
39 let now = Utc::now().timestamp_millis();
40 let enabled = input.enabled.unwrap_or(true);
41 let targets = serde_json::to_string(&input.target_addresses)?;
42 let relay = serde_json::to_string(&input.relay)?;
43 sqlx::query(
44 r"INSERT INTO forwarding_rules
45 (id, mailbox_id, target_addresses, relay_json, enabled, created_at)
46 VALUES (?, ?, ?, ?, ?, ?)",
47 )
48 .bind(&id)
49 .bind(&input.mailbox_id)
50 .bind(&targets)
51 .bind(&relay)
52 .bind(i64::from(enabled))
53 .bind(now)
54 .execute(pool)
55 .await?;
56 Ok(ForwardingRule {
57 id,
58 mailbox_id: input.mailbox_id,
59 target_addresses: input.target_addresses,
60 relay: input.relay,
61 enabled,
62 created_at: now,
63 })
64}
65
66pub(crate) async fn list(pool: &SqlitePool) -> Result<Vec<ForwardingRule>> {
67 let rows = sqlx::query(
68 r"SELECT id, mailbox_id, target_addresses, relay_json, enabled, created_at
69 FROM forwarding_rules",
70 )
71 .fetch_all(pool)
72 .await?;
73 let mut out = Vec::with_capacity(rows.len());
74 for r in rows {
75 out.push(row_to_rule(&r)?);
76 }
77 Ok(out)
78}
79
80pub(crate) async fn list_for_mailbox(
81 pool: &SqlitePool,
82 mailbox_id: &str,
83) -> Result<Vec<ForwardingRule>> {
84 let rows = sqlx::query(
85 r"SELECT id, mailbox_id, target_addresses, relay_json, enabled, created_at
86 FROM forwarding_rules
87 WHERE enabled = 1 AND (mailbox_id IS NULL OR mailbox_id = ?)",
88 )
89 .bind(mailbox_id)
90 .fetch_all(pool)
91 .await?;
92 let mut out = Vec::with_capacity(rows.len());
93 for r in rows {
94 out.push(row_to_rule(&r)?);
95 }
96 Ok(out)
97}
98
99pub(crate) async fn delete(pool: &SqlitePool, id: &str) -> Result<()> {
100 let res = sqlx::query("DELETE FROM forwarding_rules WHERE id = ?")
101 .bind(id)
102 .execute(pool)
103 .await?;
104 if res.rows_affected() == 0 {
105 return Err(Error::Invalid(format!("forwarding rule {id} not found")));
106 }
107 Ok(())
108}
109
110fn row_to_rule(row: &sqlx::sqlite::SqliteRow) -> Result<ForwardingRule> {
111 let targets_json: String = row.try_get("target_addresses").unwrap_or_default();
112 let relay_json: String = row.try_get("relay_json").unwrap_or_default();
113 let target_addresses: Vec<String> = serde_json::from_str(&targets_json).unwrap_or_default();
114 let relay: RelayConfig = serde_json::from_str(&relay_json)?;
115 Ok(ForwardingRule {
116 id: row.try_get("id").unwrap_or_default(),
117 mailbox_id: row.try_get::<Option<String>, _>("mailbox_id").ok().flatten(),
118 target_addresses,
119 relay,
120 enabled: row.try_get::<i64, _>("enabled").unwrap_or(0) != 0,
121 created_at: row.try_get("created_at").unwrap_or(0),
122 })
123}