postcrate_core/db/
bounce_rules.rs1use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use sqlx::{Row, SqlitePool};
6use uuid::Uuid;
7
8use crate::error::{Error, Result};
9use crate::events::BounceKind;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[cfg_attr(feature = "specta", derive(specta::Type))]
13#[serde(rename_all = "camelCase")]
14pub struct BounceRule {
15 #[serde(default)]
16 pub id: String,
17 pub mailbox_id: String,
18 pub address_pattern: String,
19 pub bounce_kind: BounceKind,
20 pub smtp_code: u16,
21 pub smtp_message: String,
22 #[serde(default = "default_enabled")]
23 pub enabled: bool,
24 #[serde(default)]
25 pub created_at: i64,
26}
27
28fn default_enabled() -> bool {
29 true
30}
31
32pub(crate) async fn list(pool: &SqlitePool, mailbox_id: &str) -> Result<Vec<BounceRule>> {
33 let rows = sqlx::query(
34 r"SELECT id, mailbox_id, address_pattern, bounce_kind, smtp_code,
35 smtp_message, enabled, created_at
36 FROM bounce_rules
37 WHERE mailbox_id = ?
38 ORDER BY created_at ASC",
39 )
40 .bind(mailbox_id)
41 .fetch_all(pool)
42 .await?;
43 Ok(rows.iter().map(row_to_rule).collect())
44}
45
46pub(crate) async fn list_enabled(pool: &SqlitePool, mailbox_id: &str) -> Result<Vec<BounceRule>> {
47 let rows = sqlx::query(
48 r"SELECT id, mailbox_id, address_pattern, bounce_kind, smtp_code,
49 smtp_message, enabled, created_at
50 FROM bounce_rules
51 WHERE mailbox_id = ? AND enabled = 1",
52 )
53 .bind(mailbox_id)
54 .fetch_all(pool)
55 .await?;
56 Ok(rows.iter().map(row_to_rule).collect())
57}
58
59pub(crate) async fn upsert(pool: &SqlitePool, mut rule: BounceRule) -> Result<BounceRule> {
60 if rule.id.is_empty() {
61 rule.id = Uuid::new_v4().to_string();
62 }
63 if rule.created_at == 0 {
64 rule.created_at = Utc::now().timestamp_millis();
65 }
66 if !(400..600).contains(&rule.smtp_code) {
67 return Err(Error::Invalid(format!(
68 "smtp_code {} must be 4xx or 5xx",
69 rule.smtp_code
70 )));
71 }
72
73 sqlx::query(
74 r"INSERT INTO bounce_rules
75 (id, mailbox_id, address_pattern, bounce_kind, smtp_code,
76 smtp_message, enabled, created_at)
77 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
78 ON CONFLICT(id) DO UPDATE SET
79 address_pattern = excluded.address_pattern,
80 bounce_kind = excluded.bounce_kind,
81 smtp_code = excluded.smtp_code,
82 smtp_message = excluded.smtp_message,
83 enabled = excluded.enabled",
84 )
85 .bind(&rule.id)
86 .bind(&rule.mailbox_id)
87 .bind(&rule.address_pattern)
88 .bind(rule.bounce_kind.as_str())
89 .bind(i64::from(rule.smtp_code))
90 .bind(&rule.smtp_message)
91 .bind(i64::from(rule.enabled))
92 .bind(rule.created_at)
93 .execute(pool)
94 .await?;
95
96 Ok(rule)
97}
98
99pub(crate) async fn delete(pool: &SqlitePool, id: &str) -> Result<()> {
100 let res = sqlx::query("DELETE FROM bounce_rules WHERE id = ?")
101 .bind(id)
102 .execute(pool)
103 .await?;
104 if res.rows_affected() == 0 {
105 return Err(Error::BounceRuleNotFound(id.to_string()));
106 }
107 Ok(())
108}
109
110fn row_to_rule(row: &sqlx::sqlite::SqliteRow) -> BounceRule {
111 let kind_str: String = row.try_get("bounce_kind").unwrap_or_else(|_| "hard".into());
112 BounceRule {
113 id: row.try_get("id").unwrap_or_default(),
114 mailbox_id: row.try_get("mailbox_id").unwrap_or_default(),
115 address_pattern: row.try_get("address_pattern").unwrap_or_default(),
116 bounce_kind: BounceKind::from_str(&kind_str),
117 smtp_code: (row.try_get::<i64, _>("smtp_code").unwrap_or(550)) as u16,
118 smtp_message: row.try_get("smtp_message").unwrap_or_default(),
119 enabled: row.try_get::<i64, _>("enabled").unwrap_or(1) != 0,
120 created_at: row.try_get("created_at").unwrap_or(0),
121 }
122}