Skip to main content

acme_proxy/config/types/
notify.rs

1//! `[notify]` — the operator-notification backends and their event filters.
2//!
3//! Re-exported flat from [`super`], so nothing outside this directory names
4//! the submodule.
5
6use std::collections::BTreeMap;
7
8use serde::Deserialize;
9
10use super::empty_string_is_no_values;
11
12/// Notification-subsystem configuration: which backends are active, and each
13/// backend's own settings. See [`crate::notify`].
14#[derive(Debug, Clone, Default, Deserialize)]
15#[serde(default)]
16pub struct NotifyConfig {
17    /// Which backends are active: `"email"`, `"webhook"`, `"custom"`.
18    /// Empty (default) means no notifications are sent at all.
19    #[serde(deserialize_with = "empty_string_is_no_values")]
20    pub enabled: Vec<String>,
21    pub email: EmailNotifyConfig,
22    /// Which of `webhook`'s entries to POST to, and in what order, when
23    /// `webhook` is listed in `enabled` — the same shape as `custom_enabled`
24    /// below, and resolved by the same [`resolve_named_entries`].
25    ///
26    /// [`resolve_named_entries`]: crate::config::resolve_named_entries
27    #[serde(deserialize_with = "empty_string_is_no_values")]
28    pub webhook_enabled: Vec<String>,
29    /// Named HTTP webhook targets, selected and ordered by `webhook_enabled`.
30    /// Each name must match `^[a-z0-9-]+$`, same as `custom`'s entries and for
31    /// the same reason.
32    pub webhook: BTreeMap<String, WebhookNotifyConfig>,
33    /// Which of `custom`'s entries to run, and in what order, when `custom`
34    /// is listed in `enabled` — the same shape as `webhook_enabled`.
35    #[serde(deserialize_with = "empty_string_is_no_values")]
36    pub custom_enabled: Vec<String>,
37    /// Named external script/webhook configs, selected and ordered by
38    /// `custom_enabled`. Each name must match `^[a-z0-9-]+$`, same as
39    /// `filter.custom`'s entries and for the same reason (see
40    /// [`valid_config_key_name`](crate::config::valid_config_key_name)).
41    pub custom: BTreeMap<String, CustomNotifyConfig>,
42    /// Filesystem directory to look for template overrides in, checked
43    /// per-template-file before falling back to the compiled-in defaults.
44    /// Empty (default) means compiled-in defaults only.
45    pub template_dir: String,
46}
47/// Configuration for the `email` notify backend (SMTP via `lettre`).
48#[derive(Debug, Clone, Deserialize)]
49#[serde(default)]
50pub struct EmailNotifyConfig {
51    pub smtp_host: String,
52    pub smtp_port: u16,
53    pub smtp_username: String,
54    pub smtp_password: String,
55    /// `"starttls"` (default) | `"tls"` | `"none"`.
56    pub smtp_security: String,
57    pub from: String,
58    #[serde(deserialize_with = "empty_string_is_no_values")]
59    pub to: Vec<String>,
60    /// Which lifecycle events this backend reacts to. Defaults to all six,
61    /// listed explicitly rather than relying on "empty means all": every
62    /// other list field in this codebase treats empty as *off*, so reusing
63    /// that convention here would silently mean "no events" the moment an
64    /// operator writes `events = []` expecting "all".
65    #[serde(deserialize_with = "empty_string_is_no_values")]
66    pub events: Vec<String>,
67    pub timeout_ms: u64,
68}
69
70impl Default for EmailNotifyConfig {
71    fn default() -> Self {
72        Self {
73            smtp_host: String::new(),
74            smtp_port: 587,
75            smtp_username: String::new(),
76            smtp_password: String::new(),
77            smtp_security: "starttls".to_string(),
78            from: String::new(),
79            to: Vec::new(),
80            events: all_notify_events(),
81            timeout_ms: 5000,
82        }
83    }
84}
85/// Configuration for one named `webhook` notify target: an HTTP request whose
86/// URL, method, headers and body are all the operator's to state.
87///
88/// This is what makes a chat provider configuration rather than a backend —
89/// Slack, Mattermost, Teams, Telegram and Matrix differ only in these four
90/// values. See [`crate::notify::webhook`].
91#[derive(Debug, Clone, Deserialize)]
92#[serde(default)]
93pub struct WebhookNotifyConfig {
94    /// The endpoint to call. Required once the entry is selected; it routinely
95    /// carries the credential in its path (a Slack hook id, a Telegram bot
96    /// token), which is why nothing ever logs more of it than the host.
97    pub url: String,
98    /// `"POST"` (default), `"PUT"` or `"PATCH"`. Matrix's send-message API is
99    /// the reason this is configurable at all.
100    pub method: String,
101    /// Extra request headers, e.g. `Authorization`. Applied after the
102    /// defaults, so an entry may override `content-type`.
103    pub headers: BTreeMap<String, String>,
104    /// The request body, as a MiniJinja template. `message` (the rendered
105    /// `webhook/<event>.j2`), `hook` and every field of the event's own
106    /// payload are in scope.
107    ///
108    /// The default is the shape Slack, Mattermost and Teams all accept. Note
109    /// the `tojson` filter: `.j2` templates have auto-escaping off, so a
110    /// message holding a quote or a newline needs it to stay valid JSON.
111    pub body: String,
112    #[serde(deserialize_with = "empty_string_is_no_values")]
113    pub events: Vec<String>,
114    pub timeout_ms: u64,
115}
116
117/// The default `body`: the `{"text": …}` payload Slack, Mattermost and
118/// Microsoft Teams incoming webhooks all accept, so those three need a `url`
119/// and nothing else.
120pub const DEFAULT_WEBHOOK_BODY: &str = r#"{"text": {{ message | tojson }}}"#;
121
122impl Default for WebhookNotifyConfig {
123    fn default() -> Self {
124        Self {
125            url: String::new(),
126            method: "POST".to_string(),
127            headers: BTreeMap::new(),
128            body: DEFAULT_WEBHOOK_BODY.to_string(),
129            events: all_notify_events(),
130            timeout_ms: 5000,
131        }
132    }
133}
134/// Configuration for one named `custom` notify script/webhook — the
135/// notify-side counterpart of `CustomFilterConfig`/`CustomSignerConfig`.
136#[derive(Debug, Clone, Deserialize)]
137#[serde(default)]
138pub struct CustomNotifyConfig {
139    pub script_path: String,
140    pub timeout_ms: u64,
141    #[serde(deserialize_with = "empty_string_is_no_values")]
142    pub args: Vec<String>,
143    #[serde(deserialize_with = "empty_string_is_no_values")]
144    pub events: Vec<String>,
145}
146
147impl Default for CustomNotifyConfig {
148    fn default() -> Self {
149        Self {
150            script_path: String::new(),
151            timeout_ms: 5000,
152            args: Vec::new(),
153            events: all_notify_events(),
154        }
155    }
156}
157
158/// The six lifecycle events the `notify` subsystem can react to. Kept here
159/// rather than in `src/notify` so each backend's `events` default (below) can
160/// list them without creating a dependency from `config` on `notify`.
161pub const ALL_NOTIFY_EVENTS: [&str; 6] = [
162    "profile_mounted",
163    "account_created",
164    "account_deactivated",
165    "certificate_issued",
166    "certificate_revoked",
167    "challenge_failed",
168];
169
170fn all_notify_events() -> Vec<String> {
171    ALL_NOTIFY_EVENTS.iter().map(|s| s.to_string()).collect()
172}