]> git.proxmox.com Git - proxmox-perl-rs.git/blob - pve-rs/src/notify.rs
notify: sendmail: support the `mailto-user` parameter
[proxmox-perl-rs.git] / pve-rs / src / notify.rs
1 use std::path::Path;
2
3 use log;
4
5 use proxmox_notify::context::Context;
6
7 // Some helpers borrowed and slightly adapted from `proxmox-mail-forward`
8
9 fn normalize_for_return(s: Option<&str>) -> Option<String> {
10 match s?.trim() {
11 "" => None,
12 s => Some(s.to_string()),
13 }
14 }
15
16 fn attempt_file_read<P: AsRef<Path>>(path: P) -> Option<String> {
17 match proxmox_sys::fs::file_read_optional_string(path) {
18 Ok(contents) => contents,
19 Err(err) => {
20 log::error!("{err}");
21 None
22 }
23 }
24 }
25
26 fn lookup_mail_address(content: &str, user: &str) -> Option<String> {
27 normalize_for_return(content.lines().find_map(|line| {
28 let fields: Vec<&str> = line.split(':').collect();
29 #[allow(clippy::get_first)] // to keep expression style consistent
30 match fields.get(0)?.trim() == "user" && fields.get(1)?.trim() == user {
31 true => fields.get(6).copied(),
32 false => None,
33 }
34 }))
35 }
36
37 #[derive(Debug)]
38 struct PVEContext;
39
40 impl Context for PVEContext {
41 fn lookup_email_for_user(&self, user: &str) -> Option<String> {
42 let content = attempt_file_read("/etc/pve/user.cfg");
43 content.and_then(|content| lookup_mail_address(&content, user))
44 }
45 }
46
47 #[cfg(test)]
48 mod tests {
49 use crate::notify::lookup_mail_address;
50
51 const USER_CONFIG: &str = "
52 user:root@pam:1:0:::root@example.com:::
53 user:test@pve:1:0:::test@example.com:::
54 user:no-mail@pve:1:0::::::
55 ";
56
57 #[test]
58 fn test_parse_mail() {
59 assert_eq!(
60 lookup_mail_address(USER_CONFIG, "root@pam"),
61 Some("root@example.com".to_string())
62 );
63 assert_eq!(
64 lookup_mail_address(USER_CONFIG, "test@pve"),
65 Some("test@example.com".to_string())
66 );
67 assert_eq!(lookup_mail_address(USER_CONFIG, "no-mail@pve"), None);
68 }
69 }
70
71 static CONTEXT: PVEContext = PVEContext;
72
73 pub fn init() {
74 proxmox_notify::context::set_context(&CONTEXT)
75 }
76
77 #[perlmod::package(name = "PVE::RS::Notify")]
78 mod export {
79 use anyhow::{bail, Error};
80 use perlmod::Value;
81 use serde_json::Value as JSONValue;
82 use std::sync::Mutex;
83
84 use proxmox_notify::endpoints::gotify::{
85 DeleteableGotifyProperty, GotifyConfig, GotifyConfigUpdater, GotifyPrivateConfig,
86 GotifyPrivateConfigUpdater,
87 };
88 use proxmox_notify::endpoints::sendmail::{
89 DeleteableSendmailProperty, SendmailConfig, SendmailConfigUpdater,
90 };
91 use proxmox_notify::filter::{
92 DeleteableFilterProperty, FilterConfig, FilterConfigUpdater, FilterModeOperator,
93 };
94 use proxmox_notify::group::{DeleteableGroupProperty, GroupConfig, GroupConfigUpdater};
95 use proxmox_notify::{api, api::ApiError, Config, Notification, Severity};
96
97 pub struct NotificationConfig {
98 config: Mutex<Config>,
99 }
100
101 perlmod::declare_magic!(Box<NotificationConfig> : &NotificationConfig as "PVE::RS::Notify");
102
103 /// Support `dclone` so this can be put into the `ccache` of `PVE::Cluster`.
104 #[export(name = "STORABLE_freeze", raw_return)]
105 fn storable_freeze(
106 #[try_from_ref] this: &NotificationConfig,
107 cloning: bool,
108 ) -> Result<Value, Error> {
109 if !cloning {
110 bail!("freezing Notification config not supported!");
111 }
112
113 let mut cloned = Box::new(NotificationConfig {
114 config: Mutex::new(this.config.lock().unwrap().clone()),
115 });
116 let value = Value::new_pointer::<NotificationConfig>(&mut *cloned);
117 let _perl = Box::leak(cloned);
118 Ok(value)
119 }
120
121 /// Instead of `thaw` we implement `attach` for `dclone`.
122 #[export(name = "STORABLE_attach", raw_return)]
123 fn storable_attach(
124 #[raw] class: Value,
125 cloning: bool,
126 #[raw] serialized: Value,
127 ) -> Result<Value, Error> {
128 if !cloning {
129 bail!("STORABLE_attach called with cloning=false");
130 }
131 let data = unsafe { Box::from_raw(serialized.pv_raw::<NotificationConfig>()?) };
132 Ok(perlmod::instantiate_magic!(&class, MAGIC => data))
133 }
134
135 #[export(raw_return)]
136 fn parse_config(
137 #[raw] class: Value,
138 raw_config: &[u8],
139 raw_private_config: &[u8],
140 ) -> Result<Value, Error> {
141 let raw_config = std::str::from_utf8(raw_config)?;
142 let raw_private_config = std::str::from_utf8(raw_private_config)?;
143
144 Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
145 NotificationConfig {
146 config: Mutex::new(Config::new(raw_config, raw_private_config)?)
147 }
148 )))
149 }
150
151 #[export]
152 fn write_config(#[try_from_ref] this: &NotificationConfig) -> Result<(String, String), Error> {
153 Ok(this.config.lock().unwrap().write()?)
154 }
155
156 #[export]
157 fn digest(#[try_from_ref] this: &NotificationConfig) -> String {
158 let config = this.config.lock().unwrap();
159 hex::encode(config.digest())
160 }
161
162 #[export(serialize_error)]
163 fn send(
164 #[try_from_ref] this: &NotificationConfig,
165 channel: &str,
166 severity: Severity,
167 title: String,
168 body: String,
169 properties: Option<JSONValue>,
170 ) -> Result<(), ApiError> {
171 let config = this.config.lock().unwrap();
172
173 let notification = Notification {
174 severity,
175 title,
176 body,
177 properties,
178 };
179
180 api::common::send(&config, channel, &notification)
181 }
182
183 #[export(serialize_error)]
184 fn test_target(
185 #[try_from_ref] this: &NotificationConfig,
186 target: &str,
187 ) -> Result<(), ApiError> {
188 let config = this.config.lock().unwrap();
189 api::common::test_target(&config, target)
190 }
191
192 #[export(serialize_error)]
193 fn get_groups(#[try_from_ref] this: &NotificationConfig) -> Result<Vec<GroupConfig>, ApiError> {
194 let config = this.config.lock().unwrap();
195 api::group::get_groups(&config)
196 }
197
198 #[export(serialize_error)]
199 fn get_group(
200 #[try_from_ref] this: &NotificationConfig,
201 id: &str,
202 ) -> Result<GroupConfig, ApiError> {
203 let config = this.config.lock().unwrap();
204 api::group::get_group(&config, id)
205 }
206
207 #[export(serialize_error)]
208 fn add_group(
209 #[try_from_ref] this: &NotificationConfig,
210 name: String,
211 endpoints: Vec<String>,
212 comment: Option<String>,
213 filter: Option<String>,
214 ) -> Result<(), ApiError> {
215 let mut config = this.config.lock().unwrap();
216 api::group::add_group(
217 &mut config,
218 &GroupConfig {
219 name,
220 endpoint: endpoints,
221 comment,
222 filter,
223 },
224 )
225 }
226
227 #[export(serialize_error)]
228 fn update_group(
229 #[try_from_ref] this: &NotificationConfig,
230 name: &str,
231 endpoints: Option<Vec<String>>,
232 comment: Option<String>,
233 filter: Option<String>,
234 delete: Option<Vec<DeleteableGroupProperty>>,
235 digest: Option<&str>,
236 ) -> Result<(), ApiError> {
237 let mut config = this.config.lock().unwrap();
238 let digest = digest.map(hex::decode).transpose().map_err(|e| {
239 ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
240 })?;
241
242 api::group::update_group(
243 &mut config,
244 name,
245 &GroupConfigUpdater {
246 endpoint: endpoints,
247 comment,
248 filter,
249 },
250 delete.as_deref(),
251 digest.as_deref(),
252 )
253 }
254
255 #[export(serialize_error)]
256 fn delete_group(#[try_from_ref] this: &NotificationConfig, name: &str) -> Result<(), ApiError> {
257 let mut config = this.config.lock().unwrap();
258 api::group::delete_group(&mut config, name)
259 }
260
261 #[export(serialize_error)]
262 fn get_sendmail_endpoints(
263 #[try_from_ref] this: &NotificationConfig,
264 ) -> Result<Vec<SendmailConfig>, ApiError> {
265 let config = this.config.lock().unwrap();
266 api::sendmail::get_endpoints(&config)
267 }
268
269 #[export(serialize_error)]
270 fn get_sendmail_endpoint(
271 #[try_from_ref] this: &NotificationConfig,
272 id: &str,
273 ) -> Result<SendmailConfig, ApiError> {
274 let config = this.config.lock().unwrap();
275 api::sendmail::get_endpoint(&config, id)
276 }
277
278 #[export(serialize_error)]
279 #[allow(clippy::too_many_arguments)]
280 fn add_sendmail_endpoint(
281 #[try_from_ref] this: &NotificationConfig,
282 name: String,
283 mailto: Option<Vec<String>>,
284 mailto_user: Option<Vec<String>>,
285 from_address: Option<String>,
286 author: Option<String>,
287 comment: Option<String>,
288 filter: Option<String>,
289 ) -> Result<(), ApiError> {
290 let mut config = this.config.lock().unwrap();
291
292 api::sendmail::add_endpoint(
293 &mut config,
294 &SendmailConfig {
295 name,
296 mailto,
297 mailto_user,
298 from_address,
299 author,
300 comment,
301 filter,
302 },
303 )
304 }
305
306 #[export(serialize_error)]
307 #[allow(clippy::too_many_arguments)]
308 fn update_sendmail_endpoint(
309 #[try_from_ref] this: &NotificationConfig,
310 name: &str,
311 mailto: Option<Vec<String>>,
312 mailto_user: Option<Vec<String>>,
313 from_address: Option<String>,
314 author: Option<String>,
315 comment: Option<String>,
316 filter: Option<String>,
317 delete: Option<Vec<DeleteableSendmailProperty>>,
318 digest: Option<&str>,
319 ) -> Result<(), ApiError> {
320 let mut config = this.config.lock().unwrap();
321 let digest = digest.map(hex::decode).transpose().map_err(|e| {
322 ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
323 })?;
324
325 api::sendmail::update_endpoint(
326 &mut config,
327 name,
328 &SendmailConfigUpdater {
329 mailto,
330 mailto_user,
331 from_address,
332 author,
333 comment,
334 filter,
335 },
336 delete.as_deref(),
337 digest.as_deref(),
338 )
339 }
340
341 #[export(serialize_error)]
342 fn delete_sendmail_endpoint(
343 #[try_from_ref] this: &NotificationConfig,
344 name: &str,
345 ) -> Result<(), ApiError> {
346 let mut config = this.config.lock().unwrap();
347 api::sendmail::delete_endpoint(&mut config, name)
348 }
349
350 #[export(serialize_error)]
351 fn get_gotify_endpoints(
352 #[try_from_ref] this: &NotificationConfig,
353 ) -> Result<Vec<GotifyConfig>, ApiError> {
354 let config = this.config.lock().unwrap();
355 api::gotify::get_endpoints(&config)
356 }
357
358 #[export(serialize_error)]
359 fn get_gotify_endpoint(
360 #[try_from_ref] this: &NotificationConfig,
361 id: &str,
362 ) -> Result<GotifyConfig, ApiError> {
363 let config = this.config.lock().unwrap();
364 api::gotify::get_endpoint(&config, id)
365 }
366
367 #[export(serialize_error)]
368 fn add_gotify_endpoint(
369 #[try_from_ref] this: &NotificationConfig,
370 name: String,
371 server: String,
372 token: String,
373 comment: Option<String>,
374 filter: Option<String>,
375 ) -> Result<(), ApiError> {
376 let mut config = this.config.lock().unwrap();
377 api::gotify::add_endpoint(
378 &mut config,
379 &GotifyConfig {
380 name: name.clone(),
381 server,
382 comment,
383 filter,
384 },
385 &GotifyPrivateConfig { name, token },
386 )
387 }
388
389 #[export(serialize_error)]
390 #[allow(clippy::too_many_arguments)]
391 fn update_gotify_endpoint(
392 #[try_from_ref] this: &NotificationConfig,
393 name: &str,
394 server: Option<String>,
395 token: Option<String>,
396 comment: Option<String>,
397 filter: Option<String>,
398 delete: Option<Vec<DeleteableGotifyProperty>>,
399 digest: Option<&str>,
400 ) -> Result<(), ApiError> {
401 let mut config = this.config.lock().unwrap();
402 let digest = digest.map(hex::decode).transpose().map_err(|e| {
403 ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
404 })?;
405
406 api::gotify::update_endpoint(
407 &mut config,
408 name,
409 &GotifyConfigUpdater {
410 server,
411 comment,
412 filter,
413 },
414 &GotifyPrivateConfigUpdater { token },
415 delete.as_deref(),
416 digest.as_deref(),
417 )
418 }
419
420 #[export(serialize_error)]
421 fn delete_gotify_endpoint(
422 #[try_from_ref] this: &NotificationConfig,
423 name: &str,
424 ) -> Result<(), ApiError> {
425 let mut config = this.config.lock().unwrap();
426 api::gotify::delete_gotify_endpoint(&mut config, name)
427 }
428
429 #[export(serialize_error)]
430 fn get_filters(
431 #[try_from_ref] this: &NotificationConfig,
432 ) -> Result<Vec<FilterConfig>, ApiError> {
433 let config = this.config.lock().unwrap();
434 api::filter::get_filters(&config)
435 }
436
437 #[export(serialize_error)]
438 fn get_filter(
439 #[try_from_ref] this: &NotificationConfig,
440 id: &str,
441 ) -> Result<FilterConfig, ApiError> {
442 let config = this.config.lock().unwrap();
443 api::filter::get_filter(&config, id)
444 }
445
446 #[export(serialize_error)]
447 #[allow(clippy::too_many_arguments)]
448 fn add_filter(
449 #[try_from_ref] this: &NotificationConfig,
450 name: String,
451 min_severity: Option<Severity>,
452 mode: Option<FilterModeOperator>,
453 invert_match: Option<bool>,
454 comment: Option<String>,
455 ) -> Result<(), ApiError> {
456 let mut config = this.config.lock().unwrap();
457 api::filter::add_filter(
458 &mut config,
459 &FilterConfig {
460 name,
461 min_severity,
462 mode,
463 invert_match,
464 comment,
465 },
466 )
467 }
468
469 #[export(serialize_error)]
470 #[allow(clippy::too_many_arguments)]
471 fn update_filter(
472 #[try_from_ref] this: &NotificationConfig,
473 name: &str,
474 min_severity: Option<Severity>,
475 mode: Option<FilterModeOperator>,
476 invert_match: Option<bool>,
477 comment: Option<String>,
478 delete: Option<Vec<DeleteableFilterProperty>>,
479 digest: Option<&str>,
480 ) -> Result<(), ApiError> {
481 let mut config = this.config.lock().unwrap();
482 let digest = digest.map(hex::decode).transpose().map_err(|e| {
483 ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
484 })?;
485
486 api::filter::update_filter(
487 &mut config,
488 name,
489 &FilterConfigUpdater {
490 min_severity,
491 mode,
492 invert_match,
493 comment,
494 },
495 delete.as_deref(),
496 digest.as_deref(),
497 )
498 }
499
500 #[export(serialize_error)]
501 fn delete_filter(
502 #[try_from_ref] this: &NotificationConfig,
503 name: &str,
504 ) -> Result<(), ApiError> {
505 let mut config = this.config.lock().unwrap();
506 api::filter::delete_filter(&mut config, name)
507 }
508 }