Skip to main content

postcrate_core/db/
webhooks.rs

1//! Webhook CRUD.
2
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use sqlx::{Row, SqlitePool};
6use uuid::Uuid;
7
8use crate::error::{Error, Result};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[cfg_attr(feature = "specta", derive(specta::Type))]
12#[serde(rename_all = "camelCase")]
13pub struct Webhook {
14    pub id: String,
15    /// `None` means this is a global webhook (fires for every mailbox).
16    pub mailbox_id: Option<String>,
17    pub url: String,
18    /// Sent as the `Authorization` header verbatim. Use this for
19    /// `Bearer <token>` or basic-auth strings.
20    pub auth_header: Option<String>,
21    pub enabled: bool,
22    pub created_at: i64,
23}
24
25#[derive(Debug, Clone, Deserialize)]
26#[cfg_attr(feature = "specta", derive(specta::Type))]
27#[serde(rename_all = "camelCase")]
28pub struct CreateWebhook {
29    pub mailbox_id: Option<String>,
30    pub url: String,
31    pub auth_header: Option<String>,
32    pub enabled: Option<bool>,
33}
34
35pub(crate) async fn insert(pool: &SqlitePool, input: CreateWebhook) -> Result<Webhook> {
36    let id = Uuid::new_v4().to_string();
37    let now = Utc::now().timestamp_millis();
38    let enabled = input.enabled.unwrap_or(true);
39    sqlx::query(
40        r"INSERT INTO webhooks (id, mailbox_id, url, auth_header, enabled, created_at)
41          VALUES (?, ?, ?, ?, ?, ?)",
42    )
43    .bind(&id)
44    .bind(&input.mailbox_id)
45    .bind(&input.url)
46    .bind(&input.auth_header)
47    .bind(i64::from(enabled))
48    .bind(now)
49    .execute(pool)
50    .await?;
51    Ok(Webhook {
52        id,
53        mailbox_id: input.mailbox_id,
54        url: input.url,
55        auth_header: input.auth_header,
56        enabled,
57        created_at: now,
58    })
59}
60
61pub(crate) async fn list(pool: &SqlitePool) -> Result<Vec<Webhook>> {
62    let rows =
63        sqlx::query("SELECT id, mailbox_id, url, auth_header, enabled, created_at FROM webhooks")
64            .fetch_all(pool)
65            .await?;
66    Ok(rows.iter().map(row_to_webhook).collect())
67}
68
69pub(crate) async fn list_for_mailbox(pool: &SqlitePool, mailbox_id: &str) -> Result<Vec<Webhook>> {
70    let rows = sqlx::query(
71        r"SELECT id, mailbox_id, url, auth_header, enabled, created_at
72          FROM webhooks
73          WHERE enabled = 1 AND (mailbox_id IS NULL OR mailbox_id = ?)",
74    )
75    .bind(mailbox_id)
76    .fetch_all(pool)
77    .await?;
78    Ok(rows.iter().map(row_to_webhook).collect())
79}
80
81pub(crate) async fn delete(pool: &SqlitePool, id: &str) -> Result<()> {
82    let res = sqlx::query("DELETE FROM webhooks WHERE id = ?")
83        .bind(id)
84        .execute(pool)
85        .await?;
86    if res.rows_affected() == 0 {
87        return Err(Error::Invalid(format!("webhook {id} not found")));
88    }
89    Ok(())
90}
91
92fn row_to_webhook(row: &sqlx::sqlite::SqliteRow) -> Webhook {
93    Webhook {
94        id: row.try_get("id").unwrap_or_default(),
95        mailbox_id: row
96            .try_get::<Option<String>, _>("mailbox_id")
97            .ok()
98            .flatten(),
99        url: row.try_get("url").unwrap_or_default(),
100        auth_header: row
101            .try_get::<Option<String>, _>("auth_header")
102            .ok()
103            .flatten(),
104        enabled: row.try_get::<i64, _>("enabled").unwrap_or(0) != 0,
105        created_at: row.try_get("created_at").unwrap_or(0),
106    }
107}