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 /// The periodic expiry digest — off until `lead_days` is non-zero.
23 pub expiry: ExpiryNotifyConfig,
24 /// Which of `webhook`'s entries to POST to, and in what order, when
25 /// `webhook` is listed in `enabled` — the same shape as `custom_enabled`
26 /// below, and resolved by the same [`resolve_named_entries`].
27 ///
28 /// [`resolve_named_entries`]: crate::config::resolve_named_entries
29 #[serde(deserialize_with = "empty_string_is_no_values")]
30 pub webhook_enabled: Vec<String>,
31 /// Named HTTP webhook targets, selected and ordered by `webhook_enabled`.
32 /// Each name must match `^[a-z0-9-]+$`, same as `custom`'s entries and for
33 /// the same reason.
34 pub webhook: BTreeMap<String, WebhookNotifyConfig>,
35 /// Which of `custom`'s entries to run, and in what order, when `custom`
36 /// is listed in `enabled` — the same shape as `webhook_enabled`.
37 #[serde(deserialize_with = "empty_string_is_no_values")]
38 pub custom_enabled: Vec<String>,
39 /// Named external script/webhook configs, selected and ordered by
40 /// `custom_enabled`. Each name must match `^[a-z0-9-]+$`, same as
41 /// `filter.custom`'s entries and for the same reason (see
42 /// [`valid_config_key_name`](crate::config::valid_config_key_name)).
43 pub custom: BTreeMap<String, CustomNotifyConfig>,
44 /// Filesystem directory to look for template overrides in, checked
45 /// per-template-file before falling back to the compiled-in defaults.
46 /// Empty (default) means compiled-in defaults only.
47 pub template_dir: String,
48}
49/// Configuration for the `email` notify backend (SMTP via `lettre`).
50#[derive(Debug, Clone, Deserialize)]
51#[serde(default)]
52pub struct EmailNotifyConfig {
53 pub smtp_host: String,
54 pub smtp_port: u16,
55 pub smtp_username: String,
56 pub smtp_password: String,
57 /// `"starttls"` (default) | `"tls"` | `"none"`.
58 pub smtp_security: String,
59 pub from: String,
60 #[serde(deserialize_with = "empty_string_is_no_values")]
61 pub to: Vec<String>,
62 /// Which lifecycle events this backend reacts to. Defaults to all of
63 /// them, listed explicitly rather than relying on "empty means all": every
64 /// other list field in this codebase treats empty as *off*, so reusing
65 /// that convention here would silently mean "no events" the moment an
66 /// operator writes `events = []` expecting "all".
67 #[serde(deserialize_with = "empty_string_is_no_values")]
68 pub events: Vec<String>,
69 pub timeout_ms: u64,
70}
71
72impl Default for EmailNotifyConfig {
73 fn default() -> Self {
74 Self {
75 smtp_host: String::new(),
76 smtp_port: 587,
77 smtp_username: String::new(),
78 smtp_password: String::new(),
79 smtp_security: "starttls".to_string(),
80 from: String::new(),
81 to: Vec::new(),
82 events: all_notify_events(),
83 timeout_ms: 5000,
84 }
85 }
86}
87/// `[notify.expiry]` — the periodic digest of certificates approaching expiry
88/// (see [`crate::notify::expiry`]).
89///
90/// One message per profile per `interval_days`, listing what lapses inside
91/// `lead_days`, and **not** one message per certificate: a renewal is a new
92/// order, so the certificate it replaced still expires on schedule and a
93/// per-certificate reminder would tell an operator about every certificate the
94/// CA has ever issued. The digest carries the superseded ones annotated
95/// instead, so the un-renewed rows are what stands out.
96#[derive(Debug, Clone, Deserialize)]
97#[serde(default)]
98pub struct ExpiryNotifyConfig {
99 /// How far ahead to look, in days. **`0` (the default) is off** — the job
100 /// is never registered at all, the shape `audit.retention_days` and
101 /// `jobs.retention_days` already use for "do not schedule this sweep".
102 pub lead_days: u64,
103 /// How often the digest is sent, in days. There is deliberately no
104 /// per-certificate rate limit beside it: the digest *is* the rate limit,
105 /// which is what the per-certificate shape needed a stored "last reminded"
106 /// timestamp for.
107 pub interval_days: u64,
108 /// The most certificates one message lists. The count of matches is
109 /// carried whole regardless, so a truncated digest still says how many it
110 /// did not name — a per-certificate message was self-limiting and this is
111 /// not, and ten thousand expiring at once is an unreadable mail and a
112 /// webhook body a provider refuses.
113 pub max_entries: usize,
114}
115
116impl Default for ExpiryNotifyConfig {
117 fn default() -> Self {
118 Self {
119 lead_days: 0,
120 interval_days: 7,
121 max_entries: 50,
122 }
123 }
124}
125/// Configuration for one named `webhook` notify target: an HTTP request whose
126/// URL, method, headers and body are all the operator's to state.
127///
128/// This is what makes a chat provider configuration rather than a backend —
129/// Slack, Mattermost, Teams, Telegram and Matrix differ only in these four
130/// values. See [`crate::notify::webhook`].
131#[derive(Debug, Clone, Deserialize)]
132#[serde(default)]
133pub struct WebhookNotifyConfig {
134 /// The endpoint to call. Required once the entry is selected; it routinely
135 /// carries the credential in its path (a Slack hook id, a Telegram bot
136 /// token), which is why nothing ever logs more of it than the host.
137 pub url: String,
138 /// `"POST"` (default), `"PUT"` or `"PATCH"`. Matrix's send-message API is
139 /// the reason this is configurable at all.
140 pub method: String,
141 /// Extra request headers, e.g. `Authorization`. Applied after the
142 /// defaults, so an entry may override `content-type`.
143 pub headers: BTreeMap<String, String>,
144 /// The request body, as a MiniJinja template. `message` (the rendered
145 /// `webhook/<event>.j2`), `hook` and every field of the event's own
146 /// payload are in scope.
147 ///
148 /// The default is the shape Slack, Mattermost and Teams all accept. Note
149 /// the `tojson` filter: `.j2` templates have auto-escaping off, so a
150 /// message holding a quote or a newline needs it to stay valid JSON.
151 pub body: String,
152 #[serde(deserialize_with = "empty_string_is_no_values")]
153 pub events: Vec<String>,
154 pub timeout_ms: u64,
155}
156
157/// The default `body`: the `{"text": …}` payload Slack, Mattermost and
158/// Microsoft Teams incoming webhooks all accept, so those three need a `url`
159/// and nothing else.
160pub const DEFAULT_WEBHOOK_BODY: &str = r#"{"text": {{ message | tojson }}}"#;
161
162impl Default for WebhookNotifyConfig {
163 fn default() -> Self {
164 Self {
165 url: String::new(),
166 method: "POST".to_string(),
167 headers: BTreeMap::new(),
168 body: DEFAULT_WEBHOOK_BODY.to_string(),
169 events: all_notify_events(),
170 timeout_ms: 5000,
171 }
172 }
173}
174/// Configuration for one named `custom` notify script/webhook — the
175/// notify-side counterpart of `CustomFilterConfig`/`CustomSignerConfig`.
176#[derive(Debug, Clone, Deserialize)]
177#[serde(default)]
178pub struct CustomNotifyConfig {
179 pub script_path: String,
180 pub timeout_ms: u64,
181 #[serde(deserialize_with = "empty_string_is_no_values")]
182 pub args: Vec<String>,
183 #[serde(deserialize_with = "empty_string_is_no_values")]
184 pub events: Vec<String>,
185}
186
187impl Default for CustomNotifyConfig {
188 fn default() -> Self {
189 Self {
190 script_path: String::new(),
191 timeout_ms: 5000,
192 args: Vec::new(),
193 events: all_notify_events(),
194 }
195 }
196}
197
198/// The lifecycle events the `notify` subsystem can react to. Kept here rather
199/// than in `src/notify` so each backend's `events` default (below) can list
200/// them without creating a dependency from `config` on `notify`.
201///
202/// `certificates_expiring` is the odd one and worth recognising as such: the
203/// other six are things that just happened to one account, order or
204/// certificate, where it is a periodic digest about however many certificates
205/// are approaching expiry. It reaches a backend only once
206/// `notify.expiry.lead_days` is non-zero, so listing it here does not start
207/// sending anything on its own.
208pub const ALL_NOTIFY_EVENTS: [&str; 7] = [
209 "profile_mounted",
210 "account_created",
211 "account_deactivated",
212 "certificate_issued",
213 "certificate_revoked",
214 "challenge_failed",
215 "certificates_expiring",
216];
217
218fn all_notify_events() -> Vec<String> {
219 ALL_NOTIFY_EVENTS.iter().map(|s| s.to_string()).collect()
220}