Skip to main content

postcrate_core/db/
forwarding.rs

1//! Auto-forwarding rule CRUD.
2
3use 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    /// `None` means "forward emails from every mailbox".
17    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}