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}