5 use proxmox_notify
::context
::Context
;
7 // Some helpers borrowed and slightly adapted from `proxmox-mail-forward`
9 fn normalize_for_return(s
: Option
<&str>) -> Option
<String
> {
12 s
=> Some(s
.to_string()),
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
,
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(),
37 fn lookup_datacenter_config_key(content
: &str, key
: &str) -> Option
<String
> {
38 let key_prefix
= format
!("{key}:");
42 .find_map(|line
| line
.strip_prefix(&key_prefix
)),
49 impl Context
for PVEContext
{
50 fn lookup_email_for_user(&self, user
: &str) -> Option
<String
> {
51 let content
= attempt_file_read("/etc/pve/user.cfg");
52 content
.and_then(|content
| lookup_mail_address(&content
, user
))
55 fn default_sendmail_author(&self) -> String
{
59 fn default_sendmail_from(&self) -> String
{
60 let content
= attempt_file_read("/etc/pve/datacenter.cfg");
62 .and_then(|content
| lookup_datacenter_config_key(&content
, "mail_from"))
63 .unwrap_or_else(|| String
::from("root"))
69 use crate::notify
::{lookup_datacenter_config_key, lookup_mail_address}
;
71 const USER_CONFIG
: &str = "
72 user:root@pam:1:0:::root@example.com:::
73 user:test@pve:1:0:::test@example.com:::
74 user:no-mail@pve:1:0::::::
78 fn test_parse_mail() {
80 lookup_mail_address(USER_CONFIG
, "root@pam"),
81 Some("root@example.com".to_string())
84 lookup_mail_address(USER_CONFIG
, "test@pve"),
85 Some("test@example.com".to_string())
87 assert_eq
!(lookup_mail_address(USER_CONFIG
, "no-mail@pve"), None
);
90 const DC_CONFIG
: &str = "
91 email_from: user@example.com
95 fn test_parse_dc_config() {
97 lookup_datacenter_config_key(DC_CONFIG
, "email_from"),
98 Some("user@example.com".to_string())
103 static CONTEXT
: PVEContext
= PVEContext
;
106 proxmox_notify
::context
::set_context(&CONTEXT
)
109 #[perlmod::package(name = "PVE::RS::Notify")]
111 use anyhow
::{bail, Error}
;
113 use serde_json
::Value
as JSONValue
;
114 use std
::sync
::Mutex
;
116 use proxmox_notify
::endpoints
::gotify
::{
117 DeleteableGotifyProperty
, GotifyConfig
, GotifyConfigUpdater
, GotifyPrivateConfig
,
118 GotifyPrivateConfigUpdater
,
120 use proxmox_notify
::endpoints
::sendmail
::{
121 DeleteableSendmailProperty
, SendmailConfig
, SendmailConfigUpdater
,
123 use proxmox_notify
::filter
::{
124 DeleteableFilterProperty
, FilterConfig
, FilterConfigUpdater
, FilterModeOperator
,
126 use proxmox_notify
::group
::{DeleteableGroupProperty, GroupConfig, GroupConfigUpdater}
;
127 use proxmox_notify
::{api, api::ApiError, Config, Notification, Severity}
;
129 pub struct NotificationConfig
{
130 config
: Mutex
<Config
>,
133 perlmod
::declare_magic
!(Box
<NotificationConfig
> : &NotificationConfig
as "PVE::RS::Notify");
135 /// Support `dclone` so this can be put into the `ccache` of `PVE::Cluster`.
136 #[export(name = "STORABLE_freeze", raw_return)]
138 #[try_from_ref] this: &NotificationConfig,
140 ) -> Result
<Value
, Error
> {
142 bail
!("freezing Notification config not supported!");
145 let mut cloned
= Box
::new(NotificationConfig
{
146 config
: Mutex
::new(this
.config
.lock().unwrap().clone()),
148 let value
= Value
::new_pointer
::<NotificationConfig
>(&mut *cloned
);
149 let _perl
= Box
::leak(cloned
);
153 /// Instead of `thaw` we implement `attach` for `dclone`.
154 #[export(name = "STORABLE_attach", raw_return)]
158 #[raw] serialized: Value,
159 ) -> Result
<Value
, Error
> {
161 bail
!("STORABLE_attach called with cloning=false");
163 let data
= unsafe { Box::from_raw(serialized.pv_raw::<NotificationConfig>()?) }
;
164 Ok(perlmod
::instantiate_magic
!(&class
, MAGIC
=> data
))
167 #[export(raw_return)]
171 raw_private_config
: &[u8],
172 ) -> Result
<Value
, Error
> {
173 let raw_config
= std
::str::from_utf8(raw_config
)?
;
174 let raw_private_config
= std
::str::from_utf8(raw_private_config
)?
;
176 Ok(perlmod
::instantiate_magic
!(&class
, MAGIC
=> Box
::new(
178 config
: Mutex
::new(Config
::new(raw_config
, raw_private_config
)?
)
184 fn write_config(#[try_from_ref] this: &NotificationConfig) -> Result<(String, String), Error> {
185 Ok(this
.config
.lock().unwrap().write()?
)
189 fn digest(#[try_from_ref] this: &NotificationConfig) -> String {
190 let config
= this
.config
.lock().unwrap();
191 hex
::encode(config
.digest())
194 #[export(serialize_error)]
196 #[try_from_ref] this: &NotificationConfig,
201 properties
: Option
<JSONValue
>,
202 ) -> Result
<(), ApiError
> {
203 let config
= this
.config
.lock().unwrap();
205 let notification
= Notification
{
212 api
::common
::send(&config
, channel
, ¬ification
)
215 #[export(serialize_error)]
217 #[try_from_ref] this: &NotificationConfig,
219 ) -> Result
<(), ApiError
> {
220 let config
= this
.config
.lock().unwrap();
221 api
::common
::test_target(&config
, target
)
224 #[export(serialize_error)]
225 fn get_groups(#[try_from_ref] this: &NotificationConfig) -> Result<Vec<GroupConfig>, ApiError> {
226 let config
= this
.config
.lock().unwrap();
227 api
::group
::get_groups(&config
)
230 #[export(serialize_error)]
232 #[try_from_ref] this: &NotificationConfig,
234 ) -> Result
<GroupConfig
, ApiError
> {
235 let config
= this
.config
.lock().unwrap();
236 api
::group
::get_group(&config
, id
)
239 #[export(serialize_error)]
241 #[try_from_ref] this: &NotificationConfig,
243 endpoints
: Vec
<String
>,
244 comment
: Option
<String
>,
245 filter
: Option
<String
>,
246 ) -> Result
<(), ApiError
> {
247 let mut config
= this
.config
.lock().unwrap();
248 api
::group
::add_group(
259 #[export(serialize_error)]
261 #[try_from_ref] this: &NotificationConfig,
263 endpoints
: Option
<Vec
<String
>>,
264 comment
: Option
<String
>,
265 filter
: Option
<String
>,
266 delete
: Option
<Vec
<DeleteableGroupProperty
>>,
267 digest
: Option
<&str>,
268 ) -> Result
<(), ApiError
> {
269 let mut config
= this
.config
.lock().unwrap();
270 let digest
= digest
.map(hex
::decode
).transpose().map_err(|e
| {
271 ApiError
::internal_server_error(format
!("invalid digest: {e}"), Some(Box
::new(e
)))
274 api
::group
::update_group(
277 &GroupConfigUpdater
{
287 #[export(serialize_error)]
288 fn delete_group(#[try_from_ref] this: &NotificationConfig, name: &str) -> Result<(), ApiError> {
289 let mut config
= this
.config
.lock().unwrap();
290 api
::group
::delete_group(&mut config
, name
)
293 #[export(serialize_error)]
294 fn get_sendmail_endpoints(
295 #[try_from_ref] this: &NotificationConfig,
296 ) -> Result
<Vec
<SendmailConfig
>, ApiError
> {
297 let config
= this
.config
.lock().unwrap();
298 api
::sendmail
::get_endpoints(&config
)
301 #[export(serialize_error)]
302 fn get_sendmail_endpoint(
303 #[try_from_ref] this: &NotificationConfig,
305 ) -> Result
<SendmailConfig
, ApiError
> {
306 let config
= this
.config
.lock().unwrap();
307 api
::sendmail
::get_endpoint(&config
, id
)
310 #[export(serialize_error)]
311 #[allow(clippy::too_many_arguments)]
312 fn add_sendmail_endpoint(
313 #[try_from_ref] this: &NotificationConfig,
315 mailto
: Option
<Vec
<String
>>,
316 mailto_user
: Option
<Vec
<String
>>,
317 from_address
: Option
<String
>,
318 author
: Option
<String
>,
319 comment
: Option
<String
>,
320 filter
: Option
<String
>,
321 ) -> Result
<(), ApiError
> {
322 let mut config
= this
.config
.lock().unwrap();
324 api
::sendmail
::add_endpoint(
338 #[export(serialize_error)]
339 #[allow(clippy::too_many_arguments)]
340 fn update_sendmail_endpoint(
341 #[try_from_ref] this: &NotificationConfig,
343 mailto
: Option
<Vec
<String
>>,
344 mailto_user
: Option
<Vec
<String
>>,
345 from_address
: Option
<String
>,
346 author
: Option
<String
>,
347 comment
: Option
<String
>,
348 filter
: Option
<String
>,
349 delete
: Option
<Vec
<DeleteableSendmailProperty
>>,
350 digest
: Option
<&str>,
351 ) -> Result
<(), ApiError
> {
352 let mut config
= this
.config
.lock().unwrap();
353 let digest
= digest
.map(hex
::decode
).transpose().map_err(|e
| {
354 ApiError
::internal_server_error(format
!("invalid digest: {e}"), Some(Box
::new(e
)))
357 api
::sendmail
::update_endpoint(
360 &SendmailConfigUpdater
{
373 #[export(serialize_error)]
374 fn delete_sendmail_endpoint(
375 #[try_from_ref] this: &NotificationConfig,
377 ) -> Result
<(), ApiError
> {
378 let mut config
= this
.config
.lock().unwrap();
379 api
::sendmail
::delete_endpoint(&mut config
, name
)
382 #[export(serialize_error)]
383 fn get_gotify_endpoints(
384 #[try_from_ref] this: &NotificationConfig,
385 ) -> Result
<Vec
<GotifyConfig
>, ApiError
> {
386 let config
= this
.config
.lock().unwrap();
387 api
::gotify
::get_endpoints(&config
)
390 #[export(serialize_error)]
391 fn get_gotify_endpoint(
392 #[try_from_ref] this: &NotificationConfig,
394 ) -> Result
<GotifyConfig
, ApiError
> {
395 let config
= this
.config
.lock().unwrap();
396 api
::gotify
::get_endpoint(&config
, id
)
399 #[export(serialize_error)]
400 fn add_gotify_endpoint(
401 #[try_from_ref] this: &NotificationConfig,
405 comment
: Option
<String
>,
406 filter
: Option
<String
>,
407 ) -> Result
<(), ApiError
> {
408 let mut config
= this
.config
.lock().unwrap();
409 api
::gotify
::add_endpoint(
417 &GotifyPrivateConfig { name, token }
,
421 #[export(serialize_error)]
422 #[allow(clippy::too_many_arguments)]
423 fn update_gotify_endpoint(
424 #[try_from_ref] this: &NotificationConfig,
426 server
: Option
<String
>,
427 token
: Option
<String
>,
428 comment
: Option
<String
>,
429 filter
: Option
<String
>,
430 delete
: Option
<Vec
<DeleteableGotifyProperty
>>,
431 digest
: Option
<&str>,
432 ) -> Result
<(), ApiError
> {
433 let mut config
= this
.config
.lock().unwrap();
434 let digest
= digest
.map(hex
::decode
).transpose().map_err(|e
| {
435 ApiError
::internal_server_error(format
!("invalid digest: {e}"), Some(Box
::new(e
)))
438 api
::gotify
::update_endpoint(
441 &GotifyConfigUpdater
{
446 &GotifyPrivateConfigUpdater { token }
,
452 #[export(serialize_error)]
453 fn delete_gotify_endpoint(
454 #[try_from_ref] this: &NotificationConfig,
456 ) -> Result
<(), ApiError
> {
457 let mut config
= this
.config
.lock().unwrap();
458 api
::gotify
::delete_gotify_endpoint(&mut config
, name
)
461 #[export(serialize_error)]
463 #[try_from_ref] this: &NotificationConfig,
464 ) -> Result
<Vec
<FilterConfig
>, ApiError
> {
465 let config
= this
.config
.lock().unwrap();
466 api
::filter
::get_filters(&config
)
469 #[export(serialize_error)]
471 #[try_from_ref] this: &NotificationConfig,
473 ) -> Result
<FilterConfig
, ApiError
> {
474 let config
= this
.config
.lock().unwrap();
475 api
::filter
::get_filter(&config
, id
)
478 #[export(serialize_error)]
479 #[allow(clippy::too_many_arguments)]
481 #[try_from_ref] this: &NotificationConfig,
483 min_severity
: Option
<Severity
>,
484 mode
: Option
<FilterModeOperator
>,
485 invert_match
: Option
<bool
>,
486 comment
: Option
<String
>,
487 ) -> Result
<(), ApiError
> {
488 let mut config
= this
.config
.lock().unwrap();
489 api
::filter
::add_filter(
501 #[export(serialize_error)]
502 #[allow(clippy::too_many_arguments)]
504 #[try_from_ref] this: &NotificationConfig,
506 min_severity
: Option
<Severity
>,
507 mode
: Option
<FilterModeOperator
>,
508 invert_match
: Option
<bool
>,
509 comment
: Option
<String
>,
510 delete
: Option
<Vec
<DeleteableFilterProperty
>>,
511 digest
: Option
<&str>,
512 ) -> Result
<(), ApiError
> {
513 let mut config
= this
.config
.lock().unwrap();
514 let digest
= digest
.map(hex
::decode
).transpose().map_err(|e
| {
515 ApiError
::internal_server_error(format
!("invalid digest: {e}"), Some(Box
::new(e
)))
518 api
::filter
::update_filter(
521 &FilterConfigUpdater
{
532 #[export(serialize_error)]
534 #[try_from_ref] this: &NotificationConfig,
536 ) -> Result
<(), ApiError
> {
537 let mut config
= this
.config
.lock().unwrap();
538 api
::filter
::delete_filter(&mut config
, name
)