public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail
@ 2023-11-14 12:59 Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 01/52] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
                   ` (54 more replies)
  0 siblings, 55 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Note: For simplicity, the series merges the three series' into one
large one. Otherwise the cross-deps would have been really messy
to manage on the list.

## Notification revamp:

This series replaces notification filters and groups with notification
matchers. Instead of having a per-notification event target/policy 
setting (at the moment stored in datacenter.cfg and jobs.cfg), this 
shifts the routing part into the matcher completely.

Config example, I think this demonstrates the principle quite nicely:

sendmail: default-target
  mailto-user root@pam

matcher: fencing-for-node
  mode all       # all match-directives have to match, default
  match-field exact:hostname=pve.example.com
  match-field exact:type=fencing
  target default-target


--> Send all fencing notifications for a certain host to a certain 
target.

Right now, there are three different match-directives:
  - match-field: exact/regex match for notification metadata fields
  - match-severity: match notification severities (info,notice,warning,error)
  - match-calender: match notification timestamp
    example: match-calendar mon..fri 8-12

The old target/policy based notification was already in the pvetest repository.
Thus we take special care that there is no breakage when the notification
system encounters old settings/configuration keys. It will clean them
out/migrate them if possible.

What I tested:
  - Made sure existing notifications continue to work
    (replication/fencing in a cluster setup, backups, system updates)
  - Made sure that the 'legacy' mailto parameter for backups also works
  - Tested the new UI for notification matchers
  - Tested whether old config keys for filters and groups break anything

Followup work in the near future:
  - UI code for notification matcher config is a bit messy, I will
    send a cleanup-patch - main focus right now was to get it working
  - Mark 'mailto' in backup jobs as deprecated in UI - while also
    migrating automatically to the new system (create an endpoint/matcher
    when creating/updating a backup job)


Changes in this series revision:
  - Added built-in default config. builtins can be freely modified 
   (or disabled, if one has no use for them). If changed, the settings
   are stored in /etc/pve/notifications.cfg. To reset to defaults, one
   can simply delete this entry (from config, via API, via GUI).
  - We also return a 'origin' paramter from certain API calls, 
    which tells us if it is a built-in, a user-created config entry or
    modified built-in entry.
  - Simplied permission system, only evaluate perms for 
    /mapping/notifications. Also switch from plural to singular form.



## SMTP Endpoints:


This patch series adds support for a new notification endpoint type,
smtp. As the name suggests, this new endpoint allows PVE to talk
to SMTP server directly, without using the system's MTA (postfix).

On the Rust side, these patches add a new dependency to the `lettre`
crate for SMTP communication. This crate was chosen as it is:
  - by far the most popular mailing crate for Rust
  - well maintained
  - has reasonable dependencies
  - has async support, enabling us to asyncify the proxmox-notify
    crate at some point, if needed

Tested against:
  - the gmail SMTP server
  - the posteo SMTP server
  - our own webmail SMTP server

This series also required updating the 'lettre' crate since
one of lettre's deps was bumped to a new version by us.

Changes in new, merged patch series:
  - Added origin/disabled params

Changes since v3:
  - Rebased on top of the matcher-based notification revamp
  - Removed 'filter' setting from target configuration
  - Pulled in required patches from 'system mail forwarding' patch series

Changes since v2:
  - Rebased proxmox-widget-toolkit onto the latest master to avoid
    any conflicts.

Changes since v1:
  - Rebased on top of [1]
  - Added a mechanism for mails forwarded by `proxmox-mail-forward`
    These are forwarded inline as "message/rfc822" to avoid having 
    to rewrite mail headers (otherwise, some SMTP relays might reject the 
    mail, because the `From` header of the forwarded mail does not match the
    mail account)

[1] https://lists.proxmox.com/pipermail/pve-devel/2023-August/058956.html
[2] https://lists.proxmox.com/pipermail/pve-devel/2023-October/059299.html
[3] https://lists.proxmox.com/pipermail/pve-devel/2023-November/059818.html
[4] https://lists.proxmox.com/pipermail/pve-devel/2023-November/059843.html
[5] https://lists.proxmox.com/pipermail/pve-devel/2023-November/059872.html


## System mail forwarding

The aim of this patch series is to adapt `proxmox-mail-forward` 
so that it forwards emails that were sent to the local root user
through the `proxmox_notify` crate.

A short summary of the status quo:
Any mail that is sent to the local `root` user is forwarded by
postfix to the `proxmox-mail-forward` binary, which receives the
mail via STDIN. `proxmox-mail-forward` looks up the email address 
configured for the `root@pam` user in /etc/{proxmox-backup,pve}/user.cfg 
and then forwards the mail to this address by calling `sendmail`

This patch series modifies `proxmox-mail-forward` in the following way:
`proxmox-mail-forward` instantiates the configuration for `proxmox_notify`
by reading `/etc/{proxmox-backup,pve}/notifications.cfg.

The forwarding behavior is the following:
  - PVE installed: Use PVE's notifications.cfg
  - PBS installed: Use PBS's notifications.cfg if present. If not,
    use an empty configuration and add a default sendmail target and
    a matcher - this is needed because notifications are not yet
    integrated in PBS. In that way, the forwarding behavior is still
    the same as before on PBS (forward to root@pam via sendmail).
  - PVE/PBS co-installed: Use PVE's config *and* PBS's config. 
    If PBS's notifications.cfg does not exist, 
    a default sendmail target will *not* be added, to avoid
    forwarding the same mail twice. 
    For co-installations we assume for now that PVE has a sensible
    matcher/target config for forwarded mails.

Changelog:
  - Merged series: no changes
  - v1 -> v2:
    - Rebased
    - Apply the same fix for the PVE context as in [1]
  - v2 -> v3:
    - Rebased on top of matcher-based notification system:
      This simplifies proxmox-mail-forward by a great deal, since 
      notification routing is moved into the matcher. This means 
      proxmox-mail-forward does not need to read /etc/pve/datacenter.cfg
      any more to determine the target for the notification.

[1] https://lists.proxmox.com/pipermail/pve-devel/2023-October/059294.html
[2] https://lists.proxmox.com/pipermail/pve-devel/2023-November/059818.html
[3] https://lists.proxmox.com/pipermail/pve-devel/2023-November/059872.html
[4] https://lists.proxmox.com/pipermail/pve-devel/2023-November/059894.html
[5] https://lists.proxmox.com/pipermail/pve-devel/2023-November/059899.html
[6] https://lists.proxmox.com/pipermail/pve-devel/2023-November/059900.html



debcargo-conf:

Lukas Wagner (2):
  cherry-pick chumsky 0.9.2 from debian unstable
  update lettre to 0.11.1

 src/chumsky/debian/changelog                  |  5 ++
 src/chumsky/debian/copyright                  | 39 +++++++++++
 src/chumsky/debian/copyright.debcargo.hint    | 51 ++++++++++++++
 src/chumsky/debian/debcargo.toml              |  2 +
 src/lettre/debian/changelog                   | 10 +++
 .../debian/patches/downgrade_fastrand.patch   | 13 ++++
 .../debian/patches/downgrade_idna.patch       | 13 ++++
 src/lettre/debian/patches/downgrade_url.patch | 13 ++++
 .../patches/remove_unused_features.patch      | 69 ++++++++++---------
 src/lettre/debian/patches/series              |  4 +-
 .../patches/upgrade_quoted_printable.patch    | 13 ----
 11 files changed, 185 insertions(+), 47 deletions(-)
 create mode 100644 src/chumsky/debian/changelog
 create mode 100644 src/chumsky/debian/copyright
 create mode 100644 src/chumsky/debian/copyright.debcargo.hint
 create mode 100644 src/chumsky/debian/debcargo.toml
 create mode 100644 src/lettre/debian/patches/downgrade_fastrand.patch
 create mode 100644 src/lettre/debian/patches/downgrade_idna.patch
 create mode 100644 src/lettre/debian/patches/downgrade_url.patch
 delete mode 100644 src/lettre/debian/patches/upgrade_quoted_printable.patch


proxmox:

Lukas Wagner (13):
  notify: introduce Error::Generic
  notify: factor out notification content into its own type
  notify: replace filters and groups with matcher-based system
  notify: add calendar matcher
  notify: matcher: introduce common trait for match directives
  notify: let a matcher always match if it has no matching directives
  sys: email: add `forward`
  notify: add mechanisms for email message forwarding
  notify: add PVE/PBS context
  notify: add 'smtp' endpoint
  notify: add api for smtp endpoints
  notify: add 'disable' parameter for matchers and targets.
  notify: add built-in config and 'origin' parameter

 Cargo.toml                                  |   2 +
 proxmox-notify/Cargo.toml                   |  11 +-
 proxmox-notify/examples/render.rs           |   4 +-
 proxmox-notify/src/api/common.rs            |   6 +-
 proxmox-notify/src/api/filter.rs            | 231 ---------
 proxmox-notify/src/api/gotify.rs            |  22 +-
 proxmox-notify/src/api/group.rs             | 259 ----------
 proxmox-notify/src/api/matcher.rs           | 265 ++++++++++
 proxmox-notify/src/api/mod.rs               | 146 ++----
 proxmox-notify/src/api/sendmail.rs          |  24 +-
 proxmox-notify/src/api/smtp.rs              | 362 ++++++++++++++
 proxmox-notify/src/config.rs                |  57 ++-
 proxmox-notify/src/context.rs               |  21 -
 proxmox-notify/src/context/common.rs        |  27 +
 proxmox-notify/src/context/mod.rs           |  43 ++
 proxmox-notify/src/context/pbs.rs           | 146 ++++++
 proxmox-notify/src/context/pve.rs           |  98 ++++
 proxmox-notify/src/endpoints/common/mail.rs |  24 +
 proxmox-notify/src/endpoints/common/mod.rs  |   2 +
 proxmox-notify/src/endpoints/gotify.rs      |  53 +-
 proxmox-notify/src/endpoints/mod.rs         |   4 +
 proxmox-notify/src/endpoints/sendmail.rs    | 114 ++---
 proxmox-notify/src/endpoints/smtp.rs        | 263 ++++++++++
 proxmox-notify/src/filter.rs                | 193 +------
 proxmox-notify/src/group.rs                 |  40 +-
 proxmox-notify/src/lib.rs                   | 526 +++++++++++---------
 proxmox-notify/src/matcher.rs               | 500 +++++++++++++++++++
 proxmox-notify/src/renderer/mod.rs          |  15 +-
 proxmox-notify/src/schema.rs                |  11 +-
 proxmox-sys/src/email.rs                    |  52 +-
 30 files changed, 2341 insertions(+), 1180 deletions(-)
 delete mode 100644 proxmox-notify/src/api/filter.rs
 delete mode 100644 proxmox-notify/src/api/group.rs
 create mode 100644 proxmox-notify/src/api/matcher.rs
 create mode 100644 proxmox-notify/src/api/smtp.rs
 delete mode 100644 proxmox-notify/src/context.rs
 create mode 100644 proxmox-notify/src/context/common.rs
 create mode 100644 proxmox-notify/src/context/mod.rs
 create mode 100644 proxmox-notify/src/context/pbs.rs
 create mode 100644 proxmox-notify/src/context/pve.rs
 create mode 100644 proxmox-notify/src/endpoints/common/mail.rs
 create mode 100644 proxmox-notify/src/endpoints/common/mod.rs
 create mode 100644 proxmox-notify/src/endpoints/smtp.rs
 create mode 100644 proxmox-notify/src/matcher.rs


proxmox-perl-rs:

Lukas Wagner (5):
  notify: adapt to new matcher-based notification routing
  notify: add bindings for smtp API calls
  pve-rs: notify: remove notify_context for PVE
  notify: add 'disable' parameter
  notify: support 'origin' paramter

 common/src/notify.rs         | 288 +++++++++++++++++++++--------------
 pve-rs/Cargo.toml            |   2 +-
 pve-rs/src/lib.rs            |   7 +-
 pve-rs/src/notify_context.rs | 117 --------------
 4 files changed, 180 insertions(+), 234 deletions(-)
 delete mode 100644 pve-rs/src/notify_context.rs


pve-cluster:

Lukas Wagner (1):
  notify: adapt to matcher based notification system

 src/PVE/Notify.pm | 101 +++++++++++++++++++++-------------------------
 1 file changed, 47 insertions(+), 54 deletions(-)


pve-guest-common:

Lukas Wagner (1):
  vzdump: deprecate mailto/mailnotification/notification-{target,policy}

 src/PVE/VZDump/Common.pm | 16 +++++++---------
 1 file changed, 7 insertions(+), 9 deletions(-)


pve-ha-manager:

Lukas Wagner (1):
  env: switch to matcher-based notification system

 src/PVE/HA/Env/PVE2.pm   | 10 ++--------
 src/PVE/HA/NodeStatus.pm | 11 +++++++++--
 2 files changed, 11 insertions(+), 10 deletions(-)


pve-manager:

Lukas Wagner (12):
  api: notification: remove notification groups
  api: notification: add new matcher-based notification API
  ui: dc: remove unneeded notification events panel
  vzdump: adapt to new matcher based notification system
  api: apt: adapt to matcher-based notifications
  api: replication: adapt to matcher-based notification system
  test: fix vzdump notification test
  ui: vzdump: remove left-overs from target/policy based notifications
  ui: dc: config: show notification panel again
  notify: add API routes for smtp endpoints
  api: notification: add disable and origin params
  api: notification: simplify ACLs for notification

 PVE/API2/APT.pm                               |  27 +-
 PVE/API2/Cluster/Notifications.pm             | 815 ++++++++++--------
 PVE/API2/Replication.pm                       |  25 +-
 PVE/API2/VZDump.pm                            |   8 +-
 PVE/VZDump.pm                                 |  40 +-
 test/vzdump_notification_test.pl              |   6 +-
 www/manager6/Makefile                         |   4 -
 www/manager6/dc/Backup.js                     |  81 +-
 www/manager6/dc/Config.js                     |  28 +-
 www/manager6/dc/NotificationEvents.js         | 276 ------
 .../form/NotificationPolicySelector.js        |   1 -
 www/manager6/window/Backup.js                 |  35 +-
 12 files changed, 527 insertions(+), 819 deletions(-)
 delete mode 100644 www/manager6/dc/NotificationEvents.js


proxmox-widget-toolkit:

Lukas Wagner (10):
  notification ui: add target selector for matcher
  notification ui: remove filter setting for targets
  notification ui: remove notification groups
  notification ui: rename filter to matcher
  notification: matcher: add UI for matcher editing
  notification ui: unprotected mailto-root target
  noficiation: matcher edit: make 'field' an editable combobox
  panel: notification: add gui for SMTP endpoints
  notification ui: add enable checkbox for targets/matchers
  notification ui: add column for 'origin'

 src/Makefile                            |    6 +-
 src/Schema.js                           |   10 +-
 src/data/model/NotificationConfig.js    |    6 +-
 src/form/NotificationFilterSelector.js  |   58 --
 src/panel/EmailRecipientPanel.js        |   88 ++
 src/panel/GotifyEditPanel.js            |   39 +-
 src/panel/NotificationConfigView.js     |   80 +-
 src/panel/NotificationGroupEditPanel.js |  183 ----
 src/panel/SendmailEditPanel.js          |   91 +-
 src/panel/SmtpEditPanel.js              |  204 +++++
 src/window/NotificationFilterEdit.js    |  109 ---
 src/window/NotificationMatcherEdit.js   | 1066 +++++++++++++++++++++++
 12 files changed, 1482 insertions(+), 458 deletions(-)
 delete mode 100644 src/form/NotificationFilterSelector.js
 create mode 100644 src/panel/EmailRecipientPanel.js
 delete mode 100644 src/panel/NotificationGroupEditPanel.js
 create mode 100644 src/panel/SmtpEditPanel.js
 delete mode 100644 src/window/NotificationFilterEdit.js
 create mode 100644 src/window/NotificationMatcherEdit.js


pve-docs:

Lukas Wagner (5):
  notifications: update docs to for matcher-based notifications
  notifications: document SMTP endpoints
  notifications: document 'comment' option for targets/matchers
  notifications: add documentation for system mail forwarding
  notifications: change to simplified ACL structure.

 notifications.adoc | 332 +++++++++++++++++++++++++++++++++------------
 1 file changed, 245 insertions(+), 87 deletions(-)


proxmox-mail-forward:

Lukas Wagner (2):
  feed forwarded mails into proxmox_notify
  update d/control

 Cargo.toml     |   6 +-
 debian/control |   6 +-
 src/main.rs    | 255 +++++++++++++++++++++++--------------------------
 3 files changed, 125 insertions(+), 142 deletions(-)


Summary over all repositories:
  77 files changed, 5150 insertions(+), 3040 deletions(-)

-- 
murpp v0.4.0





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 debcargo-conf 01/52] cherry-pick chumsky 0.9.2 from debian unstable
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 16:15   ` [pve-devel] applied: " Thomas Lamprecht
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 02/52] update lettre to 0.11.1 Lukas Wagner
                   ` (53 subsequent siblings)
  54 siblings, 1 reply; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/chumsky/debian/changelog               |  5 +++
 src/chumsky/debian/copyright               | 39 +++++++++++++++++
 src/chumsky/debian/copyright.debcargo.hint | 51 ++++++++++++++++++++++
 src/chumsky/debian/debcargo.toml           |  2 +
 4 files changed, 97 insertions(+)
 create mode 100644 src/chumsky/debian/changelog
 create mode 100644 src/chumsky/debian/copyright
 create mode 100644 src/chumsky/debian/copyright.debcargo.hint
 create mode 100644 src/chumsky/debian/debcargo.toml

diff --git a/src/chumsky/debian/changelog b/src/chumsky/debian/changelog
new file mode 100644
index 000000000..ae6b5ff8f
--- /dev/null
+++ b/src/chumsky/debian/changelog
@@ -0,0 +1,5 @@
+rust-chumsky (0.9.2-1) unstable; urgency=medium
+
+  * Package chumsky 0.9.2 from crates.io using debcargo 2.6.0
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Wed, 14 Jun 2023 23:38:48 +0100
diff --git a/src/chumsky/debian/copyright b/src/chumsky/debian/copyright
new file mode 100644
index 000000000..eaa3e6768
--- /dev/null
+++ b/src/chumsky/debian/copyright
@@ -0,0 +1,39 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: chumsky
+Upstream-Contact:
+ Joshua Barretto <joshua.s.barretto@gmail.com>
+ Elijah Hartvigsen <elijah.reed@hartvigsen.xyz
+ Jakob Wiesmore <runetynan@gmail.com>
+Source: https://github.com/zesterer/chumsky
+
+Files: *
+Copyright:
+ 2021-2023 Joshua Barretto <joshua.s.barretto@gmail.com>
+ 2021-2023 Elijah Hartvigsen <elijah.reed@hartvigsen.xyz
+ 2021-2023 Jakob Wiesmore <runetynan@gmail.com>
+License: MIT
+
+Files: debian/*
+Copyright:
+ 2023 Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>
+ 2023 Jelmer Vernooij <jelmer@debian.org>
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/src/chumsky/debian/copyright.debcargo.hint b/src/chumsky/debian/copyright.debcargo.hint
new file mode 100644
index 000000000..e02a9ab0f
--- /dev/null
+++ b/src/chumsky/debian/copyright.debcargo.hint
@@ -0,0 +1,51 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: chumsky
+Upstream-Contact:
+ Joshua Barretto <joshua.s.barretto@gmail.com>
+ Elijah Hartvigsen <elijah.reed@hartvigsen.xyz
+ Jakob Wiesmore <runetynan@gmail.com>
+Source: https://github.com/zesterer/chumsky
+
+Files: *
+Copyright:
+ FIXME (overlay) UNKNOWN-YEARS Joshua Barretto <joshua.s.barretto@gmail.com>
+ FIXME (overlay) UNKNOWN-YEARS Elijah Hartvigsen <elijah.reed@hartvigsen.xyz
+ FIXME (overlay) UNKNOWN-YEARS Jakob Wiesmore <runetynan@gmail.com>
+License: MIT
+Comment:
+ FIXME (overlay): Since upstream copyright years are not available in
+ Cargo.toml, they were extracted from the upstream Git repository. This may not
+ be correct information so you should review and fix this before uploading to
+ the archive.
+
+Files: LICENSE
+Copyright: 2021 Joshua Barretto
+License: UNKNOWN-LICENSE; FIXME (overlay)
+Comment:
+ FIXME (overlay): These notices are extracted from files. Please review them
+ before uploading to the archive.
+
+Files: debian/*
+Copyright:
+ 2023 Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>
+ 2023 Jelmer Vernooij <jelmer@debian.org>
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/src/chumsky/debian/debcargo.toml b/src/chumsky/debian/debcargo.toml
new file mode 100644
index 000000000..77e8151ed
--- /dev/null
+++ b/src/chumsky/debian/debcargo.toml
@@ -0,0 +1,2 @@
+overlay = "."
+uploaders = ["Jelmer Vernooij <jelmer@debian.org>"]
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 debcargo-conf 02/52] update lettre to 0.11.1
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 01/52] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 16:15   ` [pve-devel] applied: " Thomas Lamprecht
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 03/52] notify: introduce Error::Generic Lukas Wagner
                   ` (52 subsequent siblings)
  54 siblings, 1 reply; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/lettre/debian/changelog                   | 10 +++
 .../debian/patches/downgrade_fastrand.patch   | 13 ++++
 .../debian/patches/downgrade_idna.patch       | 13 ++++
 src/lettre/debian/patches/downgrade_url.patch | 13 ++++
 .../patches/remove_unused_features.patch      | 69 ++++++++++---------
 src/lettre/debian/patches/series              |  4 +-
 .../patches/upgrade_quoted_printable.patch    | 13 ----
 7 files changed, 88 insertions(+), 47 deletions(-)
 create mode 100644 src/lettre/debian/patches/downgrade_fastrand.patch
 create mode 100644 src/lettre/debian/patches/downgrade_idna.patch
 create mode 100644 src/lettre/debian/patches/downgrade_url.patch
 delete mode 100644 src/lettre/debian/patches/upgrade_quoted_printable.patch

diff --git a/src/lettre/debian/changelog b/src/lettre/debian/changelog
index d49cbb042..e92c5c070 100644
--- a/src/lettre/debian/changelog
+++ b/src/lettre/debian/changelog
@@ -1,3 +1,13 @@
+rust-lettre (0.11.1-1) UNRELEASED-FIXME-AUTOGENERATED-DEBCARGO; urgency=medium
+
+  * Package lettre 0.11.1 from crates.io using debcargo 2.6.0
+  * Downgrade fastrand from 2.0 to 1.8
+  * Downgrade idna from 0.4 to 0.3
+  * Downgrade url from 2.4 to 2.3
+  * Drop patch that upgrades quoted_printable
+
+ -- Lukas Wagner <l.wagner@proxmox.com>  Wed, 08 Nov 2023 13:32:49 +0100
+
 rust-lettre (0.10.4-1~bpo12+pve1) proxmox-rust; urgency=medium
 
   * Rebuild for Debian Bookworm / Proxmox
diff --git a/src/lettre/debian/patches/downgrade_fastrand.patch b/src/lettre/debian/patches/downgrade_fastrand.patch
new file mode 100644
index 000000000..975efeb1c
--- /dev/null
+++ b/src/lettre/debian/patches/downgrade_fastrand.patch
@@ -0,0 +1,13 @@
+diff --git a/Cargo.toml b/Cargo.toml
+index 072ea3a..5decb37 100644
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -150,7 +150,7 @@ version = "0.2.1"
+ default-features = false
+ 
+ [dependencies.fastrand]
+-version = "2.0"
++version = "1.8"
+ optional = true
+ 
+ [dependencies.futures-io]
diff --git a/src/lettre/debian/patches/downgrade_idna.patch b/src/lettre/debian/patches/downgrade_idna.patch
new file mode 100644
index 000000000..1cfaaa26c
--- /dev/null
+++ b/src/lettre/debian/patches/downgrade_idna.patch
@@ -0,0 +1,13 @@
+diff --git a/Cargo.toml b/Cargo.toml
+index 5decb37..09d2b9b 100644
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -176,7 +176,7 @@ version = "1"
+ optional = true
+ 
+ [dependencies.idna]
+-version = "0.4"
++version = "0.3"
+ 
+ [dependencies.mime]
+ version = "0.3.4"
diff --git a/src/lettre/debian/patches/downgrade_url.patch b/src/lettre/debian/patches/downgrade_url.patch
new file mode 100644
index 000000000..4da907540
--- /dev/null
+++ b/src/lettre/debian/patches/downgrade_url.patch
@@ -0,0 +1,13 @@
+diff --git a/Cargo.toml b/Cargo.toml
+index 09d2b9b..5004a3b 100644
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -237,7 +237,7 @@ optional = true
+ default-features = false
+ 
+ [dependencies.url]
+-version = "2.4"
++version = "2.3"
+ optional = true
+ 
+ [dependencies.uuid]
diff --git a/src/lettre/debian/patches/remove_unused_features.patch b/src/lettre/debian/patches/remove_unused_features.patch
index 0229e41aa..7ce45be0f 100644
--- a/src/lettre/debian/patches/remove_unused_features.patch
+++ b/src/lettre/debian/patches/remove_unused_features.patch
@@ -1,8 +1,8 @@
 diff --git a/Cargo.toml b/Cargo.toml
-index 13c34b6..b4413b6 100644
+index 13e3b77..072ea3a 100644
 --- a/Cargo.toml
 +++ b/Cargo.toml
-@@ -114,32 +114,10 @@ required-features = [
+@@ -114,24 +114,6 @@ required-features = [
      "builder",
  ]
  
@@ -27,6 +27,9 @@ index 13c34b6..b4413b6 100644
  [[bench]]
  name = "transport_smtp"
  harness = false
+@@ -140,10 +122,6 @@ harness = false
+ name = "mailbox_parsing"
+ harness = false
  
 -[dependencies.async-std]
 -version = "1.8"
@@ -35,8 +38,8 @@ index 13c34b6..b4413b6 100644
  [dependencies.async-trait]
  version = "0.1"
  optional = true
-@@ -217,19 +195,6 @@ optional = true
- version = "0.8"
+@@ -224,19 +202,6 @@ optional = true
+ version = "0.9"
  optional = true
  
 -[dependencies.rustls]
@@ -55,19 +58,19 @@ index 13c34b6..b4413b6 100644
  [dependencies.serde]
  version = "1"
  features = ["derive"]
-@@ -248,11 +213,6 @@ optional = true
- version = "0.4.4"
+@@ -255,11 +220,6 @@ optional = true
+ version = "0.5.1"
  optional = true
  
 -[dependencies.tokio1_boring]
--version = "2.1.4"
+-version = "3"
 -optional = true
 -package = "tokio-boring"
 -
  [dependencies.tokio1_crate]
  version = "1"
  optional = true
-@@ -263,11 +223,6 @@ version = "0.3"
+@@ -270,11 +230,6 @@ version = "0.3"
  optional = true
  package = "tokio-native-tls"
  
@@ -79,8 +82,8 @@ index 13c34b6..b4413b6 100644
  [dependencies.tracing]
  version = "0.1.16"
  features = ["std"]
-@@ -283,10 +238,6 @@ optional = true
- version = "0.23"
+@@ -294,10 +249,6 @@ optional = true
+ version = "0.25"
  optional = true
  
 -[dev-dependencies.async-std]
@@ -88,35 +91,35 @@ index 13c34b6..b4413b6 100644
 -features = ["attributes"]
 -
  [dev-dependencies.criterion]
- version = "0.4"
+ version = "0.5"
  
-@@ -322,18 +273,6 @@ version = "0.3"
+@@ -333,18 +284,6 @@ version = "0.3"
  version = "2"
  
  [features]
 -async-std1 = [
--    "async-std",
--    "async-trait",
--    "futures-io",
--    "futures-util",
+-    "dep:async-std",
+-    "dep:async-trait",
+-    "dep:futures-io",
+-    "dep:futures-util",
 -]
 -async-std1-rustls-tls = [
 -    "async-std1",
 -    "rustls-tls",
--    "futures-rustls",
+-    "dep:futures-rustls",
 -]
--boring-tls = ["boring"]
+-boring-tls = ["dep:boring"]
  builder = [
-     "httpdate",
-     "mime",
-@@ -366,15 +305,9 @@ file-transport-envelope = [
+     "dep:httpdate",
+     "dep:mime",
+@@ -377,15 +316,9 @@ file-transport-envelope = [
  ]
- mime03 = ["mime"]
- pool = ["futures-util"]
+ mime03 = ["dep:mime"]
+ pool = ["dep:futures-util"]
 -rustls-tls = [
--    "webpki-roots",
--    "rustls",
--    "rustls-pemfile",
+-    "dep:webpki-roots",
+-    "dep:rustls",
+-    "dep:rustls-pemfile",
 -]
  sendmail-transport = [
      "tokio1_crate?/process",
@@ -124,25 +127,25 @@ index 13c34b6..b4413b6 100644
 -    "async-std?/unstable",
  ]
  smtp-transport = [
-     "base64",
-@@ -391,21 +324,11 @@ tokio1 = [
-     "futures-io",
-     "futures-util",
+     "dep:base64",
+@@ -403,21 +336,11 @@ tokio1 = [
+     "dep:futures-io",
+     "dep:futures-util",
  ]
 -tokio1-boring-tls = [
 -    "tokio1",
 -    "boring-tls",
--    "tokio1_boring",
+-    "dep:tokio1_boring",
 -]
  tokio1-native-tls = [
      "tokio1",
      "native-tls",
-     "tokio1_native_tls_crate",
+     "dep:tokio1_native_tls_crate",
  ]
 -tokio1-rustls-tls = [
 -    "tokio1",
 -    "rustls-tls",
--    "tokio1_rustls",
+-    "dep:tokio1_rustls",
 -]
  
  [badges.is-it-maintained-issue-resolution]
diff --git a/src/lettre/debian/patches/series b/src/lettre/debian/patches/series
index 633781deb..52cd3bc0c 100644
--- a/src/lettre/debian/patches/series
+++ b/src/lettre/debian/patches/series
@@ -1,3 +1,5 @@
 downgrade_base64.patch
-upgrade_quoted_printable.patch
 remove_unused_features.patch
+downgrade_fastrand.patch
+downgrade_idna.patch
+downgrade_url.patch
diff --git a/src/lettre/debian/patches/upgrade_quoted_printable.patch b/src/lettre/debian/patches/upgrade_quoted_printable.patch
deleted file mode 100644
index ba77a50af..000000000
--- a/src/lettre/debian/patches/upgrade_quoted_printable.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/Cargo.toml b/Cargo.toml
-index c455dec..13c34b6 100644
---- a/Cargo.toml
-+++ b/Cargo.toml
-@@ -210,7 +210,7 @@ version = "1"
- optional = true
- 
- [dependencies.quoted_printable]
--version = "0.4.6"
-+version = "0.5"
- optional = true
- 
- [dependencies.rsa]
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 03/52] notify: introduce Error::Generic
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 01/52] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 02/52] update lettre to 0.11.1 Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 04/52] notify: factor out notification content into its own type Lukas Wagner
                   ` (51 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

... as leaf error-type for anything for which we do not necessarily
want a separate enum variant.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-notify/src/lib.rs | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index 7500778..f7d480c 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -25,13 +25,22 @@ mod config;
 
 #[derive(Debug)]
 pub enum Error {
+    /// There was an error serializing the config
     ConfigSerialization(Box<dyn StdError + Send + Sync>),
+    /// There was an error deserializing the config
     ConfigDeserialization(Box<dyn StdError + Send + Sync>),
+    /// An endpoint failed to send a notification
     NotifyFailed(String, Box<dyn StdError + Send + Sync>),
+    /// A target does not exist
     TargetDoesNotExist(String),
+    /// Testing one or more notification targets failed
     TargetTestFailed(Vec<Box<dyn StdError + Send + Sync>>),
+    /// A filter could not be applied
     FilterFailed(String),
+    /// The notification's template string could not be rendered
     RenderError(Box<dyn StdError + Send + Sync>),
+    /// Generic error for anything else
+    Generic(String),
 }
 
 impl Display for Error {
@@ -60,6 +69,7 @@ impl Display for Error {
                 write!(f, "could not apply filter: {message}")
             }
             Error::RenderError(err) => write!(f, "could not render notification template: {err}"),
+            Error::Generic(message) => f.write_str(message),
         }
     }
 }
@@ -74,6 +84,7 @@ impl StdError for Error {
             Error::TargetTestFailed(errs) => Some(&*errs[0]),
             Error::FilterFailed(_) => None,
             Error::RenderError(err) => Some(&**err),
+            Error::Generic(_) => None,
         }
     }
 }
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 04/52] notify: factor out notification content into its own type
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (2 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 03/52] notify: introduce Error::Generic Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 05/52] notify: replace filters and groups with matcher-based system Lukas Wagner
                   ` (50 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This will be useful later for system mail forwarding, where
the content of the mail should be forwarded unchanged.

This moves notification properties into this new type and calls them
'data'. They will exclusively used for template rendering.
`Notification` will receive a separate field for metadata, which
will be useful for notification filtering. This decouples
template rendering and filtering, which enables us to be very precise
about which metadata fields we allow to be used in filters.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-notify/examples/render.rs        |  4 +-
 proxmox-notify/src/endpoints/gotify.rs   | 26 +++++---
 proxmox-notify/src/endpoints/sendmail.rs | 62 +++++++++---------
 proxmox-notify/src/filter.rs             | 10 +--
 proxmox-notify/src/lib.rs                | 81 ++++++++++++++----------
 proxmox-notify/src/renderer/mod.rs       | 15 ++---
 6 files changed, 109 insertions(+), 89 deletions(-)

diff --git a/proxmox-notify/examples/render.rs b/proxmox-notify/examples/render.rs
index c0a6f27..d705fd0 100644
--- a/proxmox-notify/examples/render.rs
+++ b/proxmox-notify/examples/render.rs
@@ -53,10 +53,10 @@ fn main() -> Result<(), Error> {
         }
     });
 
-    let output = render_template(TemplateRenderer::Html, TEMPLATE, Some(&properties))?;
+    let output = render_template(TemplateRenderer::Html, TEMPLATE, &properties)?;
     println!("{output}");
 
-    let output = render_template(TemplateRenderer::Plaintext, TEMPLATE, Some(&properties))?;
+    let output = render_template(TemplateRenderer::Plaintext, TEMPLATE, &properties)?;
     println!("{output}");
 
     Ok(())
diff --git a/proxmox-notify/src/endpoints/gotify.rs b/proxmox-notify/src/endpoints/gotify.rs
index 83df41f..af86f9c 100644
--- a/proxmox-notify/src/endpoints/gotify.rs
+++ b/proxmox-notify/src/endpoints/gotify.rs
@@ -11,7 +11,7 @@ use proxmox_schema::{api, Updater};
 use crate::context::context;
 use crate::renderer::TemplateRenderer;
 use crate::schema::ENTITY_NAME_SCHEMA;
-use crate::{renderer, Endpoint, Error, Notification, Severity};
+use crate::{renderer, Content, Endpoint, Error, Notification, Severity};
 
 fn severity_to_priority(level: Severity) -> u32 {
     match level {
@@ -85,15 +85,21 @@ pub enum DeleteableGotifyProperty {
 
 impl Endpoint for GotifyEndpoint {
     fn send(&self, notification: &Notification) -> Result<(), Error> {
-        let properties = notification.properties.as_ref();
-
-        let title = renderer::render_template(
-            TemplateRenderer::Plaintext,
-            &notification.title,
-            properties,
-        )?;
-        let message =
-            renderer::render_template(TemplateRenderer::Plaintext, &notification.body, properties)?;
+
+        let (title, message) = match &notification.content {
+            Content::Template {
+                title_template,
+                body_template,
+                data
+            } => {
+                let rendered_title =
+                    renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?;
+                let rendered_message =
+                    renderer::render_template(TemplateRenderer::Plaintext, body_template, data)?;
+
+                (rendered_title, rendered_message)
+            }
+        };
 
         // We don't have a TemplateRenderer::Markdown yet, so simply put everything
         // in code tags. Otherwise tables etc. are not formatted properly
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index 26e2a17..c540925 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -8,7 +8,7 @@ use proxmox_schema::{api, Updater};
 use crate::context::context;
 use crate::renderer::TemplateRenderer;
 use crate::schema::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA};
-use crate::{renderer, Endpoint, Error, Notification};
+use crate::{renderer, Content, Endpoint, Error, Notification};
 
 pub(crate) const SENDMAIL_TYPENAME: &str = "sendmail";
 
@@ -102,41 +102,43 @@ impl Endpoint for SendmailEndpoint {
             }
         }
 
-        let properties = notification.properties.as_ref();
-
-        let subject = renderer::render_template(
-            TemplateRenderer::Plaintext,
-            &notification.title,
-            properties,
-        )?;
-        let html_part =
-            renderer::render_template(TemplateRenderer::Html, &notification.body, properties)?;
-        let text_part =
-            renderer::render_template(TemplateRenderer::Plaintext, &notification.body, properties)?;
-
-        let author = self
-            .config
-            .author
-            .clone()
-            .unwrap_or_else(|| context().default_sendmail_author());
-
+        let recipients_str: Vec<&str> = recipients.iter().map(String::as_str).collect();
         let mailfrom = self
             .config
             .from_address
             .clone()
             .unwrap_or_else(|| context().default_sendmail_from());
 
-        let recipients_str: Vec<&str> = recipients.iter().map(String::as_str).collect();
-
-        proxmox_sys::email::sendmail(
-            &recipients_str,
-            &subject,
-            Some(&text_part),
-            Some(&html_part),
-            Some(&mailfrom),
-            Some(&author),
-        )
-        .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into()))
+        match &notification.content {
+            Content::Template {
+                title_template,
+                body_template,
+                data,
+            } => {
+                let subject =
+                    renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?;
+                let html_part =
+                    renderer::render_template(TemplateRenderer::Html, body_template, data)?;
+                let text_part =
+                    renderer::render_template(TemplateRenderer::Plaintext, body_template, data)?;
+
+                let author = self
+                    .config
+                    .author
+                    .clone()
+                    .unwrap_or_else(|| context().default_sendmail_author());
+
+                proxmox_sys::email::sendmail(
+                    &recipients_str,
+                    &subject,
+                    Some(&text_part),
+                    Some(&html_part),
+                    Some(&mailfrom),
+                    Some(&author),
+                )
+                .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into()))
+            }
+        }
     }
 
     fn name(&self) -> &str {
diff --git a/proxmox-notify/src/filter.rs b/proxmox-notify/src/filter.rs
index 748ec4e..e014a59 100644
--- a/proxmox-notify/src/filter.rs
+++ b/proxmox-notify/src/filter.rs
@@ -160,7 +160,7 @@ impl<'a> FilterMatcher<'a> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::config;
+    use crate::{config, Content};
 
     fn parse_filters(config: &str) -> Result<Vec<FilterConfig>, Error> {
         let (config, _) = config::config(config)?;
@@ -169,10 +169,12 @@ mod tests {
 
     fn empty_notification_with_severity(severity: Severity) -> Notification {
         Notification {
-            title: String::new(),
-            body: String::new(),
+            content: Content::Template {
+                title_template: String::new(),
+                body_template: String::new(),
+                data: Default::default(),
+            },
             severity,
-            properties: Default::default(),
         }
     }
 
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index f7d480c..d40d017 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -116,17 +116,44 @@ pub trait Endpoint {
     fn filter(&self) -> Option<&str>;
 }
 
+#[derive(Debug, Clone)]
+pub enum Content {
+    /// Title and body will be rendered as a template
+    Template {
+        /// Template for the notification title.
+        title_template: String,
+        /// Template for the notification body.
+        body_template: String,
+        /// Data that can be used for template rendering.
+        data: Value,
+    },
+}
+
 #[derive(Debug, Clone)]
 /// Notification which can be sent
 pub struct Notification {
     /// Notification severity
-    pub severity: Severity,
-    /// The title of the notification
-    pub title: String,
-    /// Notification text
-    pub body: String,
-    /// Additional metadata for the notification
-    pub properties: Option<Value>,
+    severity: Severity,
+    /// Notification content
+    content: Content,
+}
+
+impl Notification {
+    pub fn new_templated<S: AsRef<str>>(
+        severity: Severity,
+        title: S,
+        body: S,
+        properties: Value,
+    ) -> Self {
+        Self {
+            severity,
+            content: Content::Template {
+                title_template: title.as_ref().to_string(),
+                body_template: body.as_ref().to_string(),
+                data: properties,
+            },
+        }
+    }
 }
 
 /// Notification configuration
@@ -384,9 +411,11 @@ impl Bus {
     pub fn test_target(&self, target: &str) -> Result<(), Error> {
         let notification = Notification {
             severity: Severity::Info,
-            title: "Test notification".into(),
-            body: "This is a test of the notification target '{{ target }}'".into(),
-            properties: Some(json!({ "target": target })),
+            content: Content::Template {
+                title_template: "Test notification".into(),
+                body_template: "This is a test of the notification target '{{ target }}'".into(),
+                data: json!({ "target": target }),
+            },
         };
 
         let mut errors: Vec<Box<dyn StdError + Send + Sync>> = Vec::new();
@@ -473,12 +502,7 @@ mod tests {
         // Send directly to endpoint
         bus.send(
             "endpoint",
-            &Notification {
-                title: "Title".into(),
-                body: "Body".into(),
-                severity: Severity::Info,
-                properties: Default::default(),
-            },
+            &Notification::new_templated(Severity::Info, "Title", "Body", Default::default()),
         );
         let messages = mock.messages();
         assert_eq!(messages.len(), 1);
@@ -511,15 +535,9 @@ mod tests {
         bus.add_endpoint(Box::new(endpoint2.clone()));
 
         let send_to_group = |channel| {
-            bus.send(
-                channel,
-                &Notification {
-                    title: "Title".into(),
-                    body: "Body".into(),
-                    severity: Severity::Info,
-                    properties: Default::default(),
-                },
-            )
+            let notification =
+                Notification::new_templated(Severity::Info, "Title", "Body", Default::default());
+            bus.send(channel, &notification)
         };
 
         send_to_group("group1");
@@ -579,15 +597,10 @@ mod tests {
         });
 
         let send_with_severity = |severity| {
-            bus.send(
-                "channel1",
-                &Notification {
-                    title: "Title".into(),
-                    body: "Body".into(),
-                    severity,
-                    properties: Default::default(),
-                },
-            );
+            let notification =
+                Notification::new_templated(severity, "Title", "Body", Default::default());
+
+            bus.send("channel1", &notification);
         };
 
         send_with_severity(Severity::Info);
diff --git a/proxmox-notify/src/renderer/mod.rs b/proxmox-notify/src/renderer/mod.rs
index 24f14aa..e9f36e6 100644
--- a/proxmox-notify/src/renderer/mod.rs
+++ b/proxmox-notify/src/renderer/mod.rs
@@ -228,11 +228,9 @@ impl BlockRenderFunctions {
 
 fn render_template_impl(
     template: &str,
-    properties: Option<&Value>,
+    data: &Value,
     renderer: TemplateRenderer,
 ) -> Result<String, Error> {
-    let properties = properties.unwrap_or(&Value::Null);
-
     let mut handlebars = Handlebars::new();
     handlebars.register_escape_fn(renderer.escape_fn());
 
@@ -242,7 +240,7 @@ fn render_template_impl(
     ValueRenderFunction::register_helpers(&mut handlebars);
 
     let rendered_template = handlebars
-        .render_template(template, properties)
+        .render_template(template, data)
         .map_err(|err| Error::RenderError(err.into()))?;
 
     Ok(rendered_template)
@@ -255,11 +253,11 @@ fn render_template_impl(
 pub fn render_template(
     renderer: TemplateRenderer,
     template: &str,
-    properties: Option<&Value>,
+    data: &Value,
 ) -> Result<String, Error> {
     let mut rendered_template = String::from(renderer.prefix());
 
-    rendered_template.push_str(&render_template_impl(template, properties, renderer)?);
+    rendered_template.push_str(&render_template_impl(template, data, renderer)?);
     rendered_template.push_str(renderer.postfix());
 
     Ok(rendered_template)
@@ -314,7 +312,7 @@ mod tests {
 
     #[test]
     fn test_render_template() -> Result<(), Error> {
-        let properties = json!({
+        let data = json!({
             "dur": 12345,
             "size": 1024 * 15,
 
@@ -370,8 +368,7 @@ val1        val2
 val3        val4        
 "#;
 
-        let rendered_plaintext =
-            render_template(TemplateRenderer::Plaintext, template, Some(&properties))?;
+        let rendered_plaintext = render_template(TemplateRenderer::Plaintext, template, &data)?;
 
         // Let's not bother about testing the HTML output, too fragile.
 
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 05/52] notify: replace filters and groups with matcher-based system
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (3 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 04/52] notify: factor out notification content into its own type Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 06/52] notify: add calendar matcher Lukas Wagner
                   ` (49 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This shifts notification routing into the matcher-system. Every
notification has associated metadata (key-value fields, severity -
to be extended) that can be match with match directives in
notification matchers. Right now, there are 2 matching directives,
match-field and match-severity. The first one allows one to do a
regex match/exact match on a metadata field, the other one allows one
to match one or more severites.
Every matcher also allows 'target' directives, these decide which
target(s) will be notified if a matcher matches a notification.

Since routing now happens in matchers, the API for sending is
simplified, since we do not need to specify a target any more.

The API routes for filters and groups have been removed completely.
The parser for the configuration file will still accept filter/group
entries, but will delete them once the config is saved again. This is
needed to allow a smooth transition from the old system to the new
system, since the old system was already available on pvetest.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    Sorry for the large commit, many of these changes interact with each
    other and it would have been significantly more effort to keep
    everything nice, tidy and compileable after splitting this apart.
    I wantend to get these changes out ASAP.

 proxmox-notify/Cargo.toml                |   2 +
 proxmox-notify/src/api/common.rs         |   6 +-
 proxmox-notify/src/api/filter.rs         | 231 -------------
 proxmox-notify/src/api/gotify.rs         |  16 -
 proxmox-notify/src/api/group.rs          | 259 ---------------
 proxmox-notify/src/api/matcher.rs        | 254 +++++++++++++++
 proxmox-notify/src/api/mod.rs            | 115 ++-----
 proxmox-notify/src/api/sendmail.rs       |  15 -
 proxmox-notify/src/config.rs             |  34 +-
 proxmox-notify/src/endpoints/gotify.rs   |  19 +-
 proxmox-notify/src/endpoints/sendmail.rs |  14 +-
 proxmox-notify/src/filter.rs             | 195 +----------
 proxmox-notify/src/group.rs              |  40 +--
 proxmox-notify/src/lib.rs                | 317 +++++++-----------
 proxmox-notify/src/matcher.rs            | 395 +++++++++++++++++++++++
 proxmox-notify/src/schema.rs             |  11 +-
 16 files changed, 848 insertions(+), 1075 deletions(-)
 delete mode 100644 proxmox-notify/src/api/filter.rs
 delete mode 100644 proxmox-notify/src/api/group.rs
 create mode 100644 proxmox-notify/src/api/matcher.rs
 create mode 100644 proxmox-notify/src/matcher.rs

diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml
index 1541b8b..4812896 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -8,6 +8,7 @@ repository.workspace = true
 exclude.workspace = true
 
 [dependencies]
+anyhow.workspace = true
 handlebars = { workspace = true }
 lazy_static.workspace = true
 log.workspace = true
@@ -16,6 +17,7 @@ openssl.workspace = true
 proxmox-http = { workspace = true, features = ["client-sync"], optional = true }
 proxmox-http-error.workspace = true
 proxmox-human-byte.workspace = true
+proxmox-serde.workspace = true
 proxmox-schema = { workspace = true, features = ["api-macro", "api-types"]}
 proxmox-section-config = { workspace = true }
 proxmox-sys = { workspace = true, optional = true }
diff --git a/proxmox-notify/src/api/common.rs b/proxmox-notify/src/api/common.rs
index d17f4db..fa2356e 100644
--- a/proxmox-notify/src/api/common.rs
+++ b/proxmox-notify/src/api/common.rs
@@ -7,7 +7,7 @@ use crate::{Bus, Config, Notification};
 ///
 /// The caller is responsible for any needed permission checks.
 /// Returns an `anyhow::Error` in case of an error.
-pub fn send(config: &Config, channel: &str, notification: &Notification) -> Result<(), HttpError> {
+pub fn send(config: &Config, notification: &Notification) -> Result<(), HttpError> {
     let bus = Bus::from_config(config).map_err(|err| {
         http_err!(
             INTERNAL_SERVER_ERROR,
@@ -15,7 +15,7 @@ pub fn send(config: &Config, channel: &str, notification: &Notification) -> Resu
         )
     })?;
 
-    bus.send(channel, notification);
+    bus.send(notification);
 
     Ok(())
 }
@@ -50,5 +50,5 @@ pub fn test_target(config: &Config, endpoint: &str) -> Result<(), HttpError> {
 /// If the entity does not exist, the result will only contain the entity.
 pub fn get_referenced_entities(config: &Config, entity: &str) -> Result<Vec<String>, HttpError> {
     let entities = super::get_referenced_entities(config, entity);
-    Ok(Vec::from_iter(entities.into_iter()))
+    Ok(Vec::from_iter(entities))
 }
diff --git a/proxmox-notify/src/api/filter.rs b/proxmox-notify/src/api/filter.rs
deleted file mode 100644
index b8682f4..0000000
--- a/proxmox-notify/src/api/filter.rs
+++ /dev/null
@@ -1,231 +0,0 @@
-use proxmox_http_error::HttpError;
-
-use crate::api::http_err;
-use crate::filter::{DeleteableFilterProperty, FilterConfig, FilterConfigUpdater, FILTER_TYPENAME};
-use crate::Config;
-
-/// Get a list of all filters
-///
-/// The caller is responsible for any needed permission checks.
-/// Returns a list of all filters or a `HttpError` if the config is
-/// (`500 Internal server error`).
-pub fn get_filters(config: &Config) -> Result<Vec<FilterConfig>, HttpError> {
-    config
-        .config
-        .convert_to_typed_array(FILTER_TYPENAME)
-        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "Could not fetch filters: {e}"))
-}
-
-/// Get filter with given `name`
-///
-/// The caller is responsible for any needed permission checks.
-/// Returns the endpoint or a `HttpError` if the filter was not found (`404 Not found`).
-pub fn get_filter(config: &Config, name: &str) -> Result<FilterConfig, HttpError> {
-    config
-        .config
-        .lookup(FILTER_TYPENAME, name)
-        .map_err(|_| http_err!(NOT_FOUND, "filter '{name}' not found"))
-}
-
-/// Add new notification filter.
-///
-/// The caller is responsible for any needed permission checks.
-/// The caller also responsible for locking the configuration files.
-/// Returns a `HttpError` if:
-///   - an entity with the same name already exists (`400 Bad request`)
-///   - the configuration could not be saved (`500 Internal server error`)
-pub fn add_filter(config: &mut Config, filter_config: &FilterConfig) -> Result<(), HttpError> {
-    super::ensure_unique(config, &filter_config.name)?;
-
-    config
-        .config
-        .set_data(&filter_config.name, FILTER_TYPENAME, filter_config)
-        .map_err(|e| {
-            http_err!(
-                INTERNAL_SERVER_ERROR,
-                "could not save filter '{}': {e}",
-                filter_config.name
-            )
-        })?;
-
-    Ok(())
-}
-
-/// Update existing notification filter
-///
-/// The caller is responsible for any needed permission checks.
-/// The caller also responsible for locking the configuration files.
-/// Returns a `HttpError` if:
-///   - the configuration could not be saved (`500 Internal server error`)
-///   - an invalid digest was passed (`400 Bad request`)
-pub fn update_filter(
-    config: &mut Config,
-    name: &str,
-    filter_updater: &FilterConfigUpdater,
-    delete: Option<&[DeleteableFilterProperty]>,
-    digest: Option<&[u8]>,
-) -> Result<(), HttpError> {
-    super::verify_digest(config, digest)?;
-
-    let mut filter = get_filter(config, name)?;
-
-    if let Some(delete) = delete {
-        for deleteable_property in delete {
-            match deleteable_property {
-                DeleteableFilterProperty::MinSeverity => filter.min_severity = None,
-                DeleteableFilterProperty::Mode => filter.mode = None,
-                DeleteableFilterProperty::InvertMatch => filter.invert_match = None,
-                DeleteableFilterProperty::Comment => filter.comment = None,
-            }
-        }
-    }
-
-    if let Some(min_severity) = filter_updater.min_severity {
-        filter.min_severity = Some(min_severity);
-    }
-
-    if let Some(mode) = filter_updater.mode {
-        filter.mode = Some(mode);
-    }
-
-    if let Some(invert_match) = filter_updater.invert_match {
-        filter.invert_match = Some(invert_match);
-    }
-
-    if let Some(comment) = &filter_updater.comment {
-        filter.comment = Some(comment.into());
-    }
-
-    config
-        .config
-        .set_data(name, FILTER_TYPENAME, &filter)
-        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "could not save filter '{name}': {e}"))?;
-
-    Ok(())
-}
-
-/// Delete existing filter
-///
-/// The caller is responsible for any needed permission checks.
-/// The caller also responsible for locking the configuration files.
-/// Returns a `HttpError` if:
-///   - the entity does not exist (`404 Not found`)
-///   - the filter is still referenced by another entity (`400 Bad request`)
-pub fn delete_filter(config: &mut Config, name: &str) -> Result<(), HttpError> {
-    // Check if the filter exists
-    let _ = get_filter(config, name)?;
-    super::ensure_unused(config, name)?;
-
-    config.config.sections.remove(name);
-
-    Ok(())
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::filter::FilterModeOperator;
-    use crate::Severity;
-
-    fn empty_config() -> Config {
-        Config::new("", "").unwrap()
-    }
-
-    fn config_with_two_filters() -> Config {
-        Config::new(
-            "
-filter: filter1
-    min-severity info
-
-filter: filter2
-    min-severity warning
-",
-            "",
-        )
-        .unwrap()
-    }
-
-    #[test]
-    fn test_update_not_existing_returns_error() -> Result<(), HttpError> {
-        let mut config = empty_config();
-        assert!(update_filter(&mut config, "test", &Default::default(), None, None).is_err());
-        Ok(())
-    }
-
-    #[test]
-    fn test_update_invalid_digest_returns_error() -> Result<(), HttpError> {
-        let mut config = config_with_two_filters();
-        assert!(update_filter(
-            &mut config,
-            "filter1",
-            &Default::default(),
-            None,
-            Some(&[0u8; 32])
-        )
-        .is_err());
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_filter_update() -> Result<(), HttpError> {
-        let mut config = config_with_two_filters();
-
-        let digest = config.digest;
-
-        update_filter(
-            &mut config,
-            "filter1",
-            &FilterConfigUpdater {
-                min_severity: Some(Severity::Error),
-                mode: Some(FilterModeOperator::Or),
-                invert_match: Some(true),
-                comment: Some("new comment".into()),
-            },
-            None,
-            Some(&digest),
-        )?;
-
-        let filter = get_filter(&config, "filter1")?;
-
-        assert!(matches!(filter.mode, Some(FilterModeOperator::Or)));
-        assert!(matches!(filter.min_severity, Some(Severity::Error)));
-        assert_eq!(filter.invert_match, Some(true));
-        assert_eq!(filter.comment, Some("new comment".into()));
-
-        // Test property deletion
-        update_filter(
-            &mut config,
-            "filter1",
-            &Default::default(),
-            Some(&[
-                DeleteableFilterProperty::InvertMatch,
-                DeleteableFilterProperty::Mode,
-                DeleteableFilterProperty::InvertMatch,
-                DeleteableFilterProperty::MinSeverity,
-                DeleteableFilterProperty::Comment,
-            ]),
-            Some(&digest),
-        )?;
-
-        let filter = get_filter(&config, "filter1")?;
-
-        assert_eq!(filter.invert_match, None);
-        assert_eq!(filter.min_severity, None);
-        assert!(matches!(filter.mode, None));
-        assert_eq!(filter.comment, None);
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_filter_delete() -> Result<(), HttpError> {
-        let mut config = config_with_two_filters();
-
-        delete_filter(&mut config, "filter1")?;
-        assert!(delete_filter(&mut config, "filter1").is_err());
-        assert_eq!(get_filters(&config)?.len(), 1);
-
-        Ok(())
-    }
-}
diff --git a/proxmox-notify/src/api/gotify.rs b/proxmox-notify/src/api/gotify.rs
index 0ec48fd..22d3d2e 100644
--- a/proxmox-notify/src/api/gotify.rs
+++ b/proxmox-notify/src/api/gotify.rs
@@ -36,7 +36,6 @@ pub fn get_endpoint(config: &Config, name: &str) -> Result<GotifyConfig, HttpErr
 /// The caller also responsible for locking the configuration files.
 /// Returns a `HttpError` if:
 ///   - an entity with the same name already exists (`400 Bad request`)
-///   - a referenced filter does not exist (`400 Bad request`)
 ///   - the configuration could not be saved (`500 Internal server error`)
 ///
 /// Panics if the names of the private config and the public config do not match.
@@ -52,11 +51,6 @@ pub fn add_endpoint(
 
     super::ensure_unique(config, &endpoint_config.name)?;
 
-    if let Some(filter) = &endpoint_config.filter {
-        // Check if filter exists
-        super::filter::get_filter(config, filter)?;
-    }
-
     set_private_config_entry(config, private_endpoint_config)?;
 
     config
@@ -77,7 +71,6 @@ pub fn add_endpoint(
 /// The caller also responsible for locking the configuration files.
 /// Returns a `HttpError` if:
 ///   - an entity with the same name already exists (`400 Bad request`)
-///   - a referenced filter does not exist (`400 Bad request`)
 ///   - the configuration could not be saved (`500 Internal server error`)
 pub fn update_endpoint(
     config: &mut Config,
@@ -95,7 +88,6 @@ pub fn update_endpoint(
         for deleteable_property in delete {
             match deleteable_property {
                 DeleteableGotifyProperty::Comment => endpoint.comment = None,
-                DeleteableGotifyProperty::Filter => endpoint.filter = None,
             }
         }
     }
@@ -118,13 +110,6 @@ pub fn update_endpoint(
         endpoint.comment = Some(comment.into());
     }
 
-    if let Some(filter) = &endpoint_config_updater.filter {
-        // Check if filter exists
-        let _ = super::filter::get_filter(config, filter)?;
-
-        endpoint.filter = Some(filter.into());
-    }
-
     config
         .config
         .set_data(name, GOTIFY_TYPENAME, &endpoint)
@@ -247,7 +232,6 @@ mod tests {
             &GotifyConfigUpdater {
                 server: Some("newhost".into()),
                 comment: Some("newcomment".into()),
-                filter: None,
             },
             &GotifyPrivateConfigUpdater {
                 token: Some("changedtoken".into()),
diff --git a/proxmox-notify/src/api/group.rs b/proxmox-notify/src/api/group.rs
deleted file mode 100644
index 6fc71ea..0000000
--- a/proxmox-notify/src/api/group.rs
+++ /dev/null
@@ -1,259 +0,0 @@
-use proxmox_http_error::HttpError;
-
-use crate::api::{http_bail, http_err};
-use crate::group::{DeleteableGroupProperty, GroupConfig, GroupConfigUpdater, GROUP_TYPENAME};
-use crate::Config;
-
-/// Get all notification groups
-///
-/// The caller is responsible for any needed permission checks.
-/// Returns a list of all groups or a `HttpError` if the config is
-/// erroneous (`500 Internal server error`).
-pub fn get_groups(config: &Config) -> Result<Vec<GroupConfig>, HttpError> {
-    config
-        .config
-        .convert_to_typed_array(GROUP_TYPENAME)
-        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "Could not fetch groups: {e}"))
-}
-
-/// Get group with given `name`
-///
-/// The caller is responsible for any needed permission checks.
-/// Returns the endpoint or an `HttpError` if the group was not found (`404 Not found`).
-pub fn get_group(config: &Config, name: &str) -> Result<GroupConfig, HttpError> {
-    config
-        .config
-        .lookup(GROUP_TYPENAME, name)
-        .map_err(|_| http_err!(NOT_FOUND, "group '{name}' not found"))
-}
-
-/// Add a new group.
-///
-/// The caller is responsible for any needed permission checks.
-/// The caller also responsible for locking the configuration files.
-/// Returns a `HttpError` if:
-///   - an entity with the same name already exists (`400 Bad request`)
-///   - a referenced filter does not exist (`400 Bad request`)
-///   - no endpoints were passed (`400 Bad request`)
-///   - referenced endpoints do not exist (`404 Not found`)
-///   - the configuration could not be saved (`500 Internal server error`)
-pub fn add_group(config: &mut Config, group_config: &GroupConfig) -> Result<(), HttpError> {
-    super::ensure_unique(config, &group_config.name)?;
-
-    if group_config.endpoint.is_empty() {
-        http_bail!(BAD_REQUEST, "group must contain at least one endpoint",);
-    }
-
-    if let Some(filter) = &group_config.filter {
-        // Check if filter exists
-        super::filter::get_filter(config, filter)?;
-    }
-
-    super::ensure_endpoints_exist(config, &group_config.endpoint)?;
-
-    config
-        .config
-        .set_data(&group_config.name, GROUP_TYPENAME, group_config)
-        .map_err(|e| {
-            http_err!(
-                INTERNAL_SERVER_ERROR,
-                "could not save group '{}': {e}",
-                group_config.name
-            )
-        })
-}
-
-/// Update existing group
-///
-/// The caller is responsible for any needed permission checks.
-/// The caller also responsible for locking the configuration files.
-/// Returns a `HttpError` if:
-///   - a referenced filter does not exist (`400 Bad request`)
-///   - an invalid digest was passed (`400 Bad request`)
-///   - no endpoints were passed (`400 Bad request`)
-///   - referenced endpoints do not exist (`404 Not found`)
-///   - the configuration could not be saved (`500 Internal server error`)
-pub fn update_group(
-    config: &mut Config,
-    name: &str,
-    updater: &GroupConfigUpdater,
-    delete: Option<&[DeleteableGroupProperty]>,
-    digest: Option<&[u8]>,
-) -> Result<(), HttpError> {
-    super::verify_digest(config, digest)?;
-
-    let mut group = get_group(config, name)?;
-
-    if let Some(delete) = delete {
-        for deleteable_property in delete {
-            match deleteable_property {
-                DeleteableGroupProperty::Comment => group.comment = None,
-                DeleteableGroupProperty::Filter => group.filter = None,
-            }
-        }
-    }
-
-    if let Some(endpoints) = &updater.endpoint {
-        super::ensure_endpoints_exist(config, endpoints)?;
-        if endpoints.is_empty() {
-            http_bail!(BAD_REQUEST, "group must contain at least one endpoint",);
-        }
-        group.endpoint = endpoints.iter().map(Into::into).collect()
-    }
-
-    if let Some(comment) = &updater.comment {
-        group.comment = Some(comment.into());
-    }
-
-    if let Some(filter) = &updater.filter {
-        // Check if filter exists
-        let _ = super::filter::get_filter(config, filter)?;
-        group.filter = Some(filter.into());
-    }
-
-    config
-        .config
-        .set_data(name, GROUP_TYPENAME, &group)
-        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "could not save group '{name}': {e}"))
-}
-
-/// Delete existing group
-///
-/// The caller is responsible for any needed permission checks.
-/// The caller also responsible for locking the configuration files.
-/// Returns a `HttpError` if the group does not exist (`404 Not found`).
-pub fn delete_group(config: &mut Config, name: &str) -> Result<(), HttpError> {
-    // Check if the group exists
-    let _ = get_group(config, name)?;
-
-    config.config.sections.remove(name);
-
-    Ok(())
-}
-
-// groups cannot be empty, so only  build the tests if we have the
-// sendmail endpoint available
-#[cfg(all(test, feature = "sendmail"))]
-mod tests {
-    use super::*;
-    use crate::api::sendmail::tests::add_sendmail_endpoint_for_test;
-    use crate::api::test_helpers::*;
-
-    fn add_default_group(config: &mut Config) -> Result<(), HttpError> {
-        add_sendmail_endpoint_for_test(config, "test")?;
-
-        add_group(
-            config,
-            &GroupConfig {
-                name: "group1".into(),
-                endpoint: vec!["test".to_string()],
-                comment: None,
-                filter: None,
-            },
-        )?;
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_add_group_fails_if_endpoint_does_not_exist() {
-        let mut config = empty_config();
-        assert!(add_group(
-            &mut config,
-            &GroupConfig {
-                name: "group1".into(),
-                endpoint: vec!["foo".into()],
-                comment: None,
-                filter: None,
-            },
-        )
-        .is_err());
-    }
-
-    #[test]
-    fn test_add_group() -> Result<(), HttpError> {
-        let mut config = empty_config();
-        assert!(add_default_group(&mut config).is_ok());
-        Ok(())
-    }
-
-    #[test]
-    fn test_update_group_fails_if_endpoint_does_not_exist() -> Result<(), HttpError> {
-        let mut config = empty_config();
-        add_default_group(&mut config)?;
-
-        assert!(update_group(
-            &mut config,
-            "group1",
-            &GroupConfigUpdater {
-                endpoint: Some(vec!["foo".into()]),
-                ..Default::default()
-            },
-            None,
-            None
-        )
-        .is_err());
-        Ok(())
-    }
-
-    #[test]
-    fn test_update_group_fails_if_digest_invalid() -> Result<(), HttpError> {
-        let mut config = empty_config();
-        add_default_group(&mut config)?;
-
-        assert!(update_group(
-            &mut config,
-            "group1",
-            &Default::default(),
-            None,
-            Some(&[0u8; 32])
-        )
-        .is_err());
-        Ok(())
-    }
-
-    #[test]
-    fn test_update_group() -> Result<(), HttpError> {
-        let mut config = empty_config();
-        add_default_group(&mut config)?;
-
-        assert!(update_group(
-            &mut config,
-            "group1",
-            &GroupConfigUpdater {
-                endpoint: None,
-                comment: Some("newcomment".into()),
-                filter: None
-            },
-            None,
-            None,
-        )
-        .is_ok());
-        let group = get_group(&config, "group1")?;
-        assert_eq!(group.comment, Some("newcomment".into()));
-
-        assert!(update_group(
-            &mut config,
-            "group1",
-            &Default::default(),
-            Some(&[DeleteableGroupProperty::Comment]),
-            None
-        )
-        .is_ok());
-        let group = get_group(&config, "group1")?;
-        assert_eq!(group.comment, None);
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_group_delete() -> Result<(), HttpError> {
-        let mut config = empty_config();
-        add_default_group(&mut config)?;
-
-        assert!(delete_group(&mut config, "group1").is_ok());
-        assert!(delete_group(&mut config, "group1").is_err());
-
-        Ok(())
-    }
-}
diff --git a/proxmox-notify/src/api/matcher.rs b/proxmox-notify/src/api/matcher.rs
new file mode 100644
index 0000000..e37b74f
--- /dev/null
+++ b/proxmox-notify/src/api/matcher.rs
@@ -0,0 +1,254 @@
+use proxmox_http_error::HttpError;
+
+use crate::api::http_err;
+use crate::matcher::{
+    DeleteableMatcherProperty, MatcherConfig, MatcherConfigUpdater, MATCHER_TYPENAME,
+};
+use crate::Config;
+
+/// Get a list of all matchers
+///
+/// The caller is responsible for any needed permission checks.
+/// Returns a list of all matchers or a `HttpError` if the config is
+/// (`500 Internal server error`).
+pub fn get_matchers(config: &Config) -> Result<Vec<MatcherConfig>, HttpError> {
+    config
+        .config
+        .convert_to_typed_array(MATCHER_TYPENAME)
+        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "Could not fetch matchers: {e}"))
+}
+
+/// Get matcher with given `name`
+///
+/// The caller is responsible for any needed permission checks.
+/// Returns the endpoint or a `HttpError` if the matcher was not found (`404 Not found`).
+pub fn get_matcher(config: &Config, name: &str) -> Result<MatcherConfig, HttpError> {
+    config
+        .config
+        .lookup(MATCHER_TYPENAME, name)
+        .map_err(|_| http_err!(NOT_FOUND, "matcher '{name}' not found"))
+}
+
+/// Add new notification matcher.
+///
+/// The caller is responsible for any needed permission checks.
+/// The caller also responsible for locking the configuration files.
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
+pub fn add_matcher(config: &mut Config, matcher_config: &MatcherConfig) -> Result<(), HttpError> {
+    super::ensure_unique(config, &matcher_config.name)?;
+
+    if let Some(targets) = matcher_config.target.as_deref() {
+        super::ensure_endpoints_exist(config, targets)?;
+    }
+
+    config
+        .config
+        .set_data(&matcher_config.name, MATCHER_TYPENAME, matcher_config)
+        .map_err(|e| {
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save matcher '{}': {e}",
+                matcher_config.name
+            )
+        })?;
+
+    Ok(())
+}
+
+/// Update existing notification matcher
+///
+/// The caller is responsible for any needed permission checks.
+/// The caller also responsible for locking the configuration files.
+/// Returns a `HttpError` if:
+///   - the configuration could not be saved (`500 Internal server error`)
+///   - an invalid digest was passed (`400 Bad request`)
+pub fn update_matcher(
+    config: &mut Config,
+    name: &str,
+    matcher_updater: &MatcherConfigUpdater,
+    delete: Option<&[DeleteableMatcherProperty]>,
+    digest: Option<&[u8]>,
+) -> Result<(), HttpError> {
+    super::verify_digest(config, digest)?;
+
+    let mut matcher = get_matcher(config, name)?;
+
+    if let Some(delete) = delete {
+        for deleteable_property in delete {
+            match deleteable_property {
+                DeleteableMatcherProperty::MatchSeverity => matcher.match_severity = None,
+                DeleteableMatcherProperty::MatchField => matcher.match_field = None,
+                DeleteableMatcherProperty::Target => matcher.target = None,
+                DeleteableMatcherProperty::Mode => matcher.mode = None,
+                DeleteableMatcherProperty::InvertMatch => matcher.invert_match = None,
+                DeleteableMatcherProperty::Comment => matcher.comment = None,
+            }
+        }
+    }
+
+    if let Some(match_severity) = &matcher_updater.match_severity {
+        matcher.match_severity = Some(match_severity.clone());
+    }
+
+    if let Some(match_field) = &matcher_updater.match_field {
+        matcher.match_field = Some(match_field.clone());
+    }
+
+    if let Some(mode) = matcher_updater.mode {
+        matcher.mode = Some(mode);
+    }
+
+    if let Some(invert_match) = matcher_updater.invert_match {
+        matcher.invert_match = Some(invert_match);
+    }
+
+    if let Some(comment) = &matcher_updater.comment {
+        matcher.comment = Some(comment.into());
+    }
+
+    if let Some(target) = &matcher_updater.target {
+        super::ensure_endpoints_exist(config, target.as_slice())?;
+        matcher.target = Some(target.clone());
+    }
+
+    config
+        .config
+        .set_data(name, MATCHER_TYPENAME, &matcher)
+        .map_err(|e| {
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save matcher '{name}': {e}"
+            )
+        })?;
+
+    Ok(())
+}
+
+/// Delete existing matcher
+///
+/// The caller is responsible for any needed permission checks.
+/// The caller also responsible for locking the configuration files.
+/// Returns a `HttpError` if:
+///   - the entity does not exist (`404 Not found`)
+pub fn delete_matcher(config: &mut Config, name: &str) -> Result<(), HttpError> {
+    // Check if the matcher exists
+    let _ = get_matcher(config, name)?;
+
+    config.config.sections.remove(name);
+
+    Ok(())
+}
+
+#[cfg(all(test, feature = "sendmail"))]
+mod tests {
+    use super::*;
+    use crate::matcher::MatchModeOperator;
+
+    fn empty_config() -> Config {
+        Config::new("", "").unwrap()
+    }
+
+    fn config_with_two_matchers() -> Config {
+        Config::new(
+            "
+sendmail: foo
+    mailto test@example.com
+
+matcher: matcher1
+
+matcher: matcher2
+",
+            "",
+        )
+        .unwrap()
+    }
+
+    #[test]
+    fn test_update_not_existing_returns_error() -> Result<(), HttpError> {
+        let mut config = empty_config();
+        assert!(update_matcher(&mut config, "test", &Default::default(), None, None).is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_update_invalid_digest_returns_error() -> Result<(), HttpError> {
+        let mut config = config_with_two_matchers();
+        assert!(update_matcher(
+            &mut config,
+            "matcher1",
+            &Default::default(),
+            None,
+            Some(&[0u8; 32])
+        )
+        .is_err());
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_matcher_update() -> Result<(), HttpError> {
+        let mut config = config_with_two_matchers();
+
+        let digest = config.digest;
+
+        update_matcher(
+            &mut config,
+            "matcher1",
+            &MatcherConfigUpdater {
+                mode: Some(MatchModeOperator::Any),
+                match_field: None,
+                match_severity: None,
+                invert_match: Some(true),
+                target: Some(vec!["foo".into()]),
+                comment: Some("new comment".into()),
+            },
+            None,
+            Some(&digest),
+        )?;
+
+        let matcher = get_matcher(&config, "matcher1")?;
+
+        assert!(matches!(matcher.mode, Some(MatchModeOperator::Any)));
+        assert_eq!(matcher.invert_match, Some(true));
+        assert_eq!(matcher.comment, Some("new comment".into()));
+
+        // Test property deletion
+        update_matcher(
+            &mut config,
+            "matcher1",
+            &Default::default(),
+            Some(&[
+                DeleteableMatcherProperty::InvertMatch,
+                DeleteableMatcherProperty::Mode,
+                DeleteableMatcherProperty::MatchField,
+                DeleteableMatcherProperty::Target,
+                DeleteableMatcherProperty::Comment,
+            ]),
+            Some(&digest),
+        )?;
+
+        let matcher = get_matcher(&config, "matcher1")?;
+
+        assert_eq!(matcher.invert_match, None);
+        assert!(matcher.match_severity.is_none());
+        assert!(matches!(matcher.match_field, None));
+        assert_eq!(matcher.target, None);
+        assert!(matcher.mode.is_none());
+        assert_eq!(matcher.comment, None);
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_matcher_delete() -> Result<(), HttpError> {
+        let mut config = config_with_two_matchers();
+
+        delete_matcher(&mut config, "matcher1")?;
+        assert!(delete_matcher(&mut config, "matcher1").is_err());
+        assert_eq!(get_matchers(&config)?.len(), 1);
+
+        Ok(())
+    }
+}
diff --git a/proxmox-notify/src/api/mod.rs b/proxmox-notify/src/api/mod.rs
index 8dc9b4e..8042157 100644
--- a/proxmox-notify/src/api/mod.rs
+++ b/proxmox-notify/src/api/mod.rs
@@ -5,10 +5,9 @@ use proxmox_http_error::HttpError;
 use crate::Config;
 
 pub mod common;
-pub mod filter;
 #[cfg(feature = "gotify")]
 pub mod gotify;
-pub mod group;
+pub mod matcher;
 #[cfg(feature = "sendmail")]
 pub mod sendmail;
 
@@ -94,36 +93,13 @@ fn ensure_unique(config: &Config, entity: &str) -> Result<(), HttpError> {
 fn get_referrers(config: &Config, entity: &str) -> Result<HashSet<String>, HttpError> {
     let mut referrers = HashSet::new();
 
-    for group in group::get_groups(config)? {
-        if group.endpoint.iter().any(|endpoint| endpoint == entity) {
-            referrers.insert(group.name.clone());
-        }
-
-        if let Some(filter) = group.filter {
-            if filter == entity {
-                referrers.insert(group.name);
-            }
-        }
-    }
-
-    #[cfg(feature = "sendmail")]
-    for endpoint in sendmail::get_endpoints(config)? {
-        if let Some(filter) = endpoint.filter {
-            if filter == entity {
-                referrers.insert(endpoint.name);
+    for matcher in matcher::get_matchers(config)? {
+        if let Some(targets) = matcher.target {
+            if targets.iter().any(|target| target == entity) {
+                referrers.insert(matcher.name.clone());
             }
         }
     }
-
-    #[cfg(feature = "gotify")]
-    for endpoint in gotify::get_endpoints(config)? {
-        if let Some(filter) = endpoint.filter {
-            if filter == entity {
-                referrers.insert(endpoint.name);
-            }
-        }
-    }
-
     Ok(referrers)
 }
 
@@ -151,23 +127,11 @@ fn get_referenced_entities(config: &Config, entity: &str) -> HashSet<String> {
         let mut new = HashSet::new();
 
         for entity in entities {
-            if let Ok(group) = group::get_group(config, entity) {
-                for target in group.endpoint {
-                    new.insert(target.clone());
-                }
-            }
-
-            #[cfg(feature = "sendmail")]
-            if let Ok(target) = sendmail::get_endpoint(config, entity) {
-                if let Some(filter) = target.filter {
-                    new.insert(filter.clone());
-                }
-            }
-
-            #[cfg(feature = "gotify")]
-            if let Ok(target) = gotify::get_endpoint(config, entity) {
-                if let Some(filter) = target.filter {
-                    new.insert(filter.clone());
+            if let Ok(group) = matcher::get_matcher(config, entity) {
+                if let Some(targets) = group.target {
+                    for target in targets {
+                        new.insert(target.clone());
+                    }
                 }
             }
         }
@@ -205,11 +169,12 @@ mod tests {
     fn prepare_config() -> Result<Config, HttpError> {
         let mut config = super::test_helpers::empty_config();
 
-        filter::add_filter(
+        matcher::add_matcher(
             &mut config,
-            &FilterConfig {
-                name: "filter".to_string(),
-                ..Default::default()
+            &MatcherConfig {
+                name: "matcher".to_string(),
+                target: Some(vec!["sendmail".to_string(), "gotify".to_string()])
+                    ..Default::default(),
             },
         )?;
 
@@ -218,7 +183,6 @@ mod tests {
             &SendmailConfig {
                 name: "sendmail".to_string(),
                 mailto: Some(vec!["foo@example.com".to_string()]),
-                filter: Some("filter".to_string()),
                 ..Default::default()
             },
         )?;
@@ -228,7 +192,6 @@ mod tests {
             &GotifyConfig {
                 name: "gotify".to_string(),
                 server: "localhost".to_string(),
-                filter: Some("filter".to_string()),
                 ..Default::default()
             },
             &GotifyPrivateConfig {
@@ -237,16 +200,6 @@ mod tests {
             },
         )?;
 
-        group::add_group(
-            &mut config,
-            &GroupConfig {
-                name: "group".to_string(),
-                endpoint: vec!["gotify".to_string(), "sendmail".to_string()],
-                filter: Some("filter".to_string()),
-                ..Default::default()
-            },
-        )?;
-
         Ok(config)
     }
 
@@ -255,24 +208,11 @@ mod tests {
         let config = prepare_config().unwrap();
 
         assert_eq!(
-            get_referenced_entities(&config, "filter"),
-            HashSet::from(["filter".to_string()])
-        );
-        assert_eq!(
-            get_referenced_entities(&config, "sendmail"),
-            HashSet::from(["filter".to_string(), "sendmail".to_string()])
-        );
-        assert_eq!(
-            get_referenced_entities(&config, "gotify"),
-            HashSet::from(["filter".to_string(), "gotify".to_string()])
-        );
-        assert_eq!(
-            get_referenced_entities(&config, "group"),
+            get_referenced_entities(&config, "matcher"),
             HashSet::from([
-                "filter".to_string(),
-                "gotify".to_string(),
+                "matcher".to_string(),
                 "sendmail".to_string(),
-                "group".to_string()
+                "gotify".to_string()
             ])
         );
     }
@@ -281,27 +221,16 @@ mod tests {
     fn test_get_referrers_for_entity() -> Result<(), HttpError> {
         let config = prepare_config().unwrap();
 
-        assert_eq!(
-            get_referrers(&config, "filter")?,
-            HashSet::from([
-                "gotify".to_string(),
-                "sendmail".to_string(),
-                "group".to_string()
-            ])
-        );
-
         assert_eq!(
             get_referrers(&config, "sendmail")?,
-            HashSet::from(["group".to_string()])
+            HashSet::from(["matcher".to_string()])
         );
 
         assert_eq!(
             get_referrers(&config, "gotify")?,
-            HashSet::from(["group".to_string()])
+            HashSet::from(["matcher".to_string()])
         );
 
-        assert!(get_referrers(&config, "group")?.is_empty(),);
-
         Ok(())
     }
 
@@ -309,10 +238,9 @@ mod tests {
     fn test_ensure_unused() {
         let config = prepare_config().unwrap();
 
-        assert!(ensure_unused(&config, "filter").is_err());
         assert!(ensure_unused(&config, "gotify").is_err());
         assert!(ensure_unused(&config, "sendmail").is_err());
-        assert!(ensure_unused(&config, "group").is_ok());
+        assert!(ensure_unused(&config, "matcher").is_ok());
     }
 
     #[test]
@@ -329,6 +257,5 @@ mod tests {
         let config = prepare_config().unwrap();
 
         assert!(ensure_endpoints_exist(&config, &vec!["sendmail", "gotify"]).is_ok());
-        assert!(ensure_endpoints_exist(&config, &vec!["group", "filter"]).is_err());
     }
 }
diff --git a/proxmox-notify/src/api/sendmail.rs b/proxmox-notify/src/api/sendmail.rs
index ac8737c..dbd9559 100644
--- a/proxmox-notify/src/api/sendmail.rs
+++ b/proxmox-notify/src/api/sendmail.rs
@@ -35,17 +35,11 @@ pub fn get_endpoint(config: &Config, name: &str) -> Result<SendmailConfig, HttpE
 /// The caller also responsible for locking the configuration files.
 /// Returns a `HttpError` if:
 ///   - an entity with the same name already exists (`400 Bad request`)
-///   - a referenced filter does not exist (`400 Bad request`)
 ///   - the configuration could not be saved (`500 Internal server error`)
 ///   - mailto *and* mailto_user are both set to `None`
 pub fn add_endpoint(config: &mut Config, endpoint: &SendmailConfig) -> Result<(), HttpError> {
     super::ensure_unique(config, &endpoint.name)?;
 
-    if let Some(filter) = &endpoint.filter {
-        // Check if filter exists
-        super::filter::get_filter(config, filter)?;
-    }
-
     if endpoint.mailto.is_none() && endpoint.mailto_user.is_none() {
         http_bail!(
             BAD_REQUEST,
@@ -70,7 +64,6 @@ pub fn add_endpoint(config: &mut Config, endpoint: &SendmailConfig) -> Result<()
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
 /// Returns a `HttpError` if:
-///   - a referenced filter does not exist (`400 Bad request`)
 ///   - the configuration could not be saved (`500 Internal server error`)
 ///   - mailto *and* mailto_user are both set to `None`
 pub fn update_endpoint(
@@ -90,7 +83,6 @@ pub fn update_endpoint(
                 DeleteableSendmailProperty::FromAddress => endpoint.from_address = None,
                 DeleteableSendmailProperty::Author => endpoint.author = None,
                 DeleteableSendmailProperty::Comment => endpoint.comment = None,
-                DeleteableSendmailProperty::Filter => endpoint.filter = None,
                 DeleteableSendmailProperty::Mailto => endpoint.mailto = None,
                 DeleteableSendmailProperty::MailtoUser => endpoint.mailto_user = None,
             }
@@ -117,11 +109,6 @@ pub fn update_endpoint(
         endpoint.comment = Some(comment.into());
     }
 
-    if let Some(filter) = &updater.filter {
-        let _ = super::filter::get_filter(config, filter)?;
-        endpoint.filter = Some(filter.into());
-    }
-
     if endpoint.mailto.is_none() && endpoint.mailto_user.is_none() {
         http_bail!(
             BAD_REQUEST,
@@ -221,7 +208,6 @@ pub mod tests {
                 from_address: Some("root@example.com".into()),
                 author: Some("newauthor".into()),
                 comment: Some("new comment".into()),
-                filter: None,
             },
             None,
             Some(&[0; 32]),
@@ -247,7 +233,6 @@ pub mod tests {
                 from_address: Some("root@example.com".into()),
                 author: Some("newauthor".into()),
                 comment: Some("new comment".into()),
-                filter: None,
             },
             None,
             Some(&digest),
diff --git a/proxmox-notify/src/config.rs b/proxmox-notify/src/config.rs
index cdbf42a..a86995e 100644
--- a/proxmox-notify/src/config.rs
+++ b/proxmox-notify/src/config.rs
@@ -5,6 +5,7 @@ use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlug
 
 use crate::filter::{FilterConfig, FILTER_TYPENAME};
 use crate::group::{GroupConfig, GROUP_TYPENAME};
+use crate::matcher::{MatcherConfig, MATCHER_TYPENAME};
 use crate::schema::BACKEND_NAME_SCHEMA;
 use crate::Error;
 
@@ -39,8 +40,14 @@ fn config_init() -> SectionConfig {
         ));
     }
 
-    const GROUP_SCHEMA: &ObjectSchema = GroupConfig::API_SCHEMA.unwrap_object_schema();
+    const MATCHER_SCHEMA: &ObjectSchema = MatcherConfig::API_SCHEMA.unwrap_object_schema();
+    config.register_plugin(SectionConfigPlugin::new(
+        MATCHER_TYPENAME.to_string(),
+        Some(String::from("name")),
+        MATCHER_SCHEMA,
+    ));
 
+    const GROUP_SCHEMA: &ObjectSchema = GroupConfig::API_SCHEMA.unwrap_object_schema();
     config.register_plugin(SectionConfigPlugin::new(
         GROUP_TYPENAME.to_string(),
         Some(String::from("name")),
@@ -78,9 +85,32 @@ fn private_config_init() -> SectionConfig {
 
 pub fn config(raw_config: &str) -> Result<(SectionConfigData, [u8; 32]), Error> {
     let digest = openssl::sha::sha256(raw_config.as_bytes());
-    let data = CONFIG
+    let mut data = CONFIG
         .parse("notifications.cfg", raw_config)
         .map_err(|err| Error::ConfigDeserialization(err.into()))?;
+
+    // TODO: Remove this once this has been in production for a while.
+    // 'group' and 'filter' sections are remnants of the 'old'
+    // notification routing approach that already hit pvetest...
+    // This mechanism cleans out left-over entries.
+    let entries: Vec<GroupConfig> = data.convert_to_typed_array("group").unwrap_or_default();
+    if !entries.is_empty() {
+        log::warn!("clearing left-over 'group' entries from notifications.cfg");
+    }
+
+    for entry in entries {
+        data.sections.remove(&entry.name);
+    }
+
+    let entries: Vec<FilterConfig> = data.convert_to_typed_array("filter").unwrap_or_default();
+    if !entries.is_empty() {
+        log::warn!("clearing left-over 'filter' entries from notifications.cfg");
+    }
+
+    for entry in entries {
+        data.sections.remove(&entry.name);
+    }
+
     Ok((data, digest))
 }
 
diff --git a/proxmox-notify/src/endpoints/gotify.rs b/proxmox-notify/src/endpoints/gotify.rs
index af86f9c..1c307a4 100644
--- a/proxmox-notify/src/endpoints/gotify.rs
+++ b/proxmox-notify/src/endpoints/gotify.rs
@@ -33,10 +33,6 @@ pub(crate) const GOTIFY_TYPENAME: &str = "gotify";
             optional: true,
             schema: COMMENT_SCHEMA,
         },
-        filter: {
-            optional: true,
-            schema: ENTITY_NAME_SCHEMA,
-        },
     }
 )]
 #[derive(Serialize, Deserialize, Updater, Default)]
@@ -51,8 +47,9 @@ pub struct GotifyConfig {
     /// Comment
     #[serde(skip_serializing_if = "Option::is_none")]
     pub comment: Option<String>,
-    /// Filter to apply
-    #[serde(skip_serializing_if = "Option::is_none")]
+    /// Deprecated.
+    #[serde(skip_serializing)]
+    #[updater(skip)]
     pub filter: Option<String>,
 }
 
@@ -80,17 +77,15 @@ pub struct GotifyEndpoint {
 #[serde(rename_all = "kebab-case")]
 pub enum DeleteableGotifyProperty {
     Comment,
-    Filter,
 }
 
 impl Endpoint for GotifyEndpoint {
     fn send(&self, notification: &Notification) -> Result<(), Error> {
-
         let (title, message) = match &notification.content {
             Content::Template {
                 title_template,
                 body_template,
-                data
+                data,
             } => {
                 let rendered_title =
                     renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?;
@@ -108,7 +103,7 @@ impl Endpoint for GotifyEndpoint {
         let body = json!({
             "title": &title,
             "message": &message,
-            "priority": severity_to_priority(notification.severity),
+            "priority": severity_to_priority(notification.metadata.severity),
             "extras": {
                 "client::display": {
                     "contentType": "text/markdown"
@@ -152,8 +147,4 @@ impl Endpoint for GotifyEndpoint {
     fn name(&self) -> &str {
         &self.config.name
     }
-
-    fn filter(&self) -> Option<&str> {
-        self.config.filter.as_deref()
-    }
 }
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index c540925..a601744 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -35,10 +35,6 @@ pub(crate) const SENDMAIL_TYPENAME: &str = "sendmail";
             optional: true,
             schema: COMMENT_SCHEMA,
         },
-        filter: {
-            optional: true,
-            schema: ENTITY_NAME_SCHEMA,
-        },
     },
 )]
 #[derive(Debug, Serialize, Deserialize, Updater, Default)]
@@ -63,8 +59,9 @@ pub struct SendmailConfig {
     /// Comment
     #[serde(skip_serializing_if = "Option::is_none")]
     pub comment: Option<String>,
-    /// Filter to apply
-    #[serde(skip_serializing_if = "Option::is_none")]
+    /// Deprecated.
+    #[serde(skip_serializing)]
+    #[updater(skip)]
     pub filter: Option<String>,
 }
 
@@ -74,7 +71,6 @@ pub enum DeleteableSendmailProperty {
     FromAddress,
     Author,
     Comment,
-    Filter,
     Mailto,
     MailtoUser,
 }
@@ -144,8 +140,4 @@ impl Endpoint for SendmailEndpoint {
     fn name(&self) -> &str {
         &self.config.name
     }
-
-    fn filter(&self) -> Option<&str> {
-        self.config.filter.as_deref()
-    }
 }
diff --git a/proxmox-notify/src/filter.rs b/proxmox-notify/src/filter.rs
index e014a59..c9b152b 100644
--- a/proxmox-notify/src/filter.rs
+++ b/proxmox-notify/src/filter.rs
@@ -1,202 +1,23 @@
-use std::collections::{HashMap, HashSet};
-
 use serde::{Deserialize, Serialize};
 
-use proxmox_schema::api_types::COMMENT_SCHEMA;
-use proxmox_schema::{api, Updater};
+use proxmox_schema::api;
 
 use crate::schema::ENTITY_NAME_SCHEMA;
-use crate::{Error, Notification, Severity};
-
-pub const FILTER_TYPENAME: &str = "filter";
-
-#[api]
-#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy)]
-#[serde(rename_all = "kebab-case")]
-pub enum FilterModeOperator {
-    /// All filter properties have to match (AND)
-    #[default]
-    And,
-    /// At least one filter property has to match (OR)
-    Or,
-}
 
-impl FilterModeOperator {
-    /// Apply the mode operator to two bools, lhs and rhs
-    fn apply(&self, lhs: bool, rhs: bool) -> bool {
-        match self {
-            FilterModeOperator::And => lhs && rhs,
-            FilterModeOperator::Or => lhs || rhs,
-        }
-    }
-
-    fn neutral_element(&self) -> bool {
-        match self {
-            FilterModeOperator::And => true,
-            FilterModeOperator::Or => false,
-        }
-    }
-}
+pub(crate) const FILTER_TYPENAME: &str = "filter";
 
 #[api(
     properties: {
         name: {
             schema: ENTITY_NAME_SCHEMA,
         },
-        comment: {
-            optional: true,
-            schema: COMMENT_SCHEMA,
-        },
-    })]
-#[derive(Debug, Serialize, Deserialize, Updater, Default)]
+    },
+    additional_properties: true,
+)]
+#[derive(Debug, Serialize, Deserialize, Default)]
 #[serde(rename_all = "kebab-case")]
-/// Config for Sendmail notification endpoints
+/// Config for the old filter system - can be removed at some point.
 pub struct FilterConfig {
-    /// Name of the filter
-    #[updater(skip)]
+    /// Name of the group
     pub name: String,
-
-    /// Minimum severity to match
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub min_severity: Option<Severity>,
-
-    /// Choose between 'and' and 'or' for when multiple properties are specified
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub mode: Option<FilterModeOperator>,
-
-    /// Invert match of the whole filter
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub invert_match: Option<bool>,
-
-    /// Comment
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub comment: Option<String>,
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum DeleteableFilterProperty {
-    MinSeverity,
-    Mode,
-    InvertMatch,
-    Comment,
-}
-
-/// A caching, lazily-evaluating notification filter. Parameterized with the notification itself,
-/// since there are usually multiple filters to check for a single notification that is to be sent.
-pub(crate) struct FilterMatcher<'a> {
-    filters: HashMap<&'a str, &'a FilterConfig>,
-    cached_results: HashMap<&'a str, bool>,
-    notification: &'a Notification,
-}
-
-impl<'a> FilterMatcher<'a> {
-    pub(crate) fn new(filters: &'a [FilterConfig], notification: &'a Notification) -> Self {
-        let filters = filters.iter().map(|f| (f.name.as_str(), f)).collect();
-
-        Self {
-            filters,
-            cached_results: Default::default(),
-            notification,
-        }
-    }
-
-    /// Check if the notification that was used to instantiate Self matches a given filter
-    pub(crate) fn check_filter_match(&mut self, filter_name: &str) -> Result<bool, Error> {
-        let mut visited = HashSet::new();
-
-        self.do_check_filter(filter_name, &mut visited)
-    }
-
-    fn do_check_filter(
-        &mut self,
-        filter_name: &str,
-        visited: &mut HashSet<String>,
-    ) -> Result<bool, Error> {
-        if visited.contains(filter_name) {
-            return Err(Error::FilterFailed(format!(
-                "recursive filter definition: {filter_name}"
-            )));
-        }
-
-        if let Some(is_match) = self.cached_results.get(filter_name) {
-            return Ok(*is_match);
-        }
-
-        visited.insert(filter_name.into());
-
-        let filter_config =
-            self.filters.get(filter_name).copied().ok_or_else(|| {
-                Error::FilterFailed(format!("filter '{filter_name}' does not exist"))
-            })?;
-
-        let invert_match = filter_config.invert_match.unwrap_or_default();
-
-        let mode_operator = filter_config.mode.unwrap_or_default();
-
-        let mut notification_matches = mode_operator.neutral_element();
-
-        notification_matches = mode_operator.apply(
-            notification_matches,
-            self.check_severity_match(filter_config, mode_operator),
-        );
-
-        Ok(notification_matches != invert_match)
-    }
-
-    fn check_severity_match(
-        &self,
-        filter_config: &FilterConfig,
-        mode_operator: FilterModeOperator,
-    ) -> bool {
-        if let Some(min_severity) = filter_config.min_severity {
-            self.notification.severity >= min_severity
-        } else {
-            mode_operator.neutral_element()
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{config, Content};
-
-    fn parse_filters(config: &str) -> Result<Vec<FilterConfig>, Error> {
-        let (config, _) = config::config(config)?;
-        Ok(config.convert_to_typed_array("filter").unwrap())
-    }
-
-    fn empty_notification_with_severity(severity: Severity) -> Notification {
-        Notification {
-            content: Content::Template {
-                title_template: String::new(),
-                body_template: String::new(),
-                data: Default::default(),
-            },
-            severity,
-        }
-    }
-
-    #[test]
-    fn test_trivial_severity_filters() -> Result<(), Error> {
-        let config = "
-filter: test
-    min-severity warning
-";
-
-        let filters = parse_filters(config)?;
-
-        let is_match = |severity| {
-            let notification = empty_notification_with_severity(severity);
-            let mut results = FilterMatcher::new(&filters, &notification);
-            results.check_filter_match("test")
-        };
-
-        assert!(is_match(Severity::Warning)?);
-        assert!(!is_match(Severity::Notice)?);
-        assert!(is_match(Severity::Error)?);
-
-        Ok(())
-    }
 }
diff --git a/proxmox-notify/src/group.rs b/proxmox-notify/src/group.rs
index 713e52e..46458db 100644
--- a/proxmox-notify/src/group.rs
+++ b/proxmox-notify/src/group.rs
@@ -1,7 +1,6 @@
 use serde::{Deserialize, Serialize};
 
-use proxmox_schema::api_types::COMMENT_SCHEMA;
-use proxmox_schema::{api, Updater};
+use proxmox_schema::api;
 
 use crate::schema::ENTITY_NAME_SCHEMA;
 
@@ -9,43 +8,16 @@ pub(crate) const GROUP_TYPENAME: &str = "group";
 
 #[api(
     properties: {
-        "endpoint": {
-            type: Array,
-            items: {
-                description: "Name of the included endpoint(s)",
-                type: String,
-            },
-        },
-        comment: {
-            optional: true,
-            schema: COMMENT_SCHEMA,
-        },
-        filter: {
-            optional: true,
+        name: {
             schema: ENTITY_NAME_SCHEMA,
         },
     },
+    additional_properties: true,
 )]
-#[derive(Debug, Serialize, Deserialize, Updater, Default)]
+#[derive(Debug, Serialize, Deserialize, Default)]
 #[serde(rename_all = "kebab-case")]
-/// Config for notification channels
+/// Config for the old target groups - can be removed at some point.
 pub struct GroupConfig {
-    /// Name of the channel
-    #[updater(skip)]
+    /// Name of the group
     pub name: String,
-    /// Endpoints for this channel
-    pub endpoint: Vec<String>,
-    /// Comment
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub comment: Option<String>,
-    /// Filter to apply
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub filter: Option<String>,
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum DeleteableGroupProperty {
-    Comment,
-    Filter,
 }
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index d40d017..1f95ae0 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -1,6 +1,7 @@
 use std::collections::HashMap;
 use std::error::Error as StdError;
 use std::fmt::Display;
+use std::str::FromStr;
 
 use serde::{Deserialize, Serialize};
 use serde_json::json;
@@ -9,15 +10,14 @@ use serde_json::Value;
 use proxmox_schema::api;
 use proxmox_section_config::SectionConfigData;
 
-pub mod filter;
-use filter::{FilterConfig, FilterMatcher, FILTER_TYPENAME};
-
-pub mod group;
-use group::{GroupConfig, GROUP_TYPENAME};
+pub mod matcher;
+use matcher::{MatcherConfig, MATCHER_TYPENAME};
 
 pub mod api;
 pub mod context;
 pub mod endpoints;
+pub mod filter;
+pub mod group;
 pub mod renderer;
 pub mod schema;
 
@@ -104,6 +104,30 @@ pub enum Severity {
     Error,
 }
 
+impl Display for Severity {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
+        match self {
+            Severity::Info => f.write_str("info"),
+            Severity::Notice => f.write_str("notice"),
+            Severity::Warning => f.write_str("warning"),
+            Severity::Error => f.write_str("error"),
+        }
+    }
+}
+
+impl FromStr for Severity {
+    type Err = Error;
+    fn from_str(s: &str) -> Result<Self, Error> {
+        match s {
+            "info" => Ok(Self::Info),
+            "notice" => Ok(Self::Notice),
+            "warning" => Ok(Self::Warning),
+            "error" => Ok(Self::Error),
+            _ => Err(Error::Generic(format!("invalid severity {s}"))),
+        }
+    }
+}
+
 /// Notification endpoint trait, implemented by all endpoint plugins
 pub trait Endpoint {
     /// Send a documentation
@@ -111,9 +135,6 @@ pub trait Endpoint {
 
     /// The name/identifier for this endpoint
     fn name(&self) -> &str;
-
-    /// The name of the filter to use
-    fn filter(&self) -> Option<&str>;
 }
 
 #[derive(Debug, Clone)]
@@ -130,12 +151,20 @@ pub enum Content {
 }
 
 #[derive(Debug, Clone)]
-/// Notification which can be sent
-pub struct Notification {
+pub struct Metadata {
     /// Notification severity
     severity: Severity,
+    /// Additional fields for additional key-value metadata
+    additional_fields: HashMap<String, String>,
+}
+
+#[derive(Debug, Clone)]
+/// Notification which can be sent
+pub struct Notification {
     /// Notification content
     content: Content,
+    /// Metadata
+    metadata: Metadata,
 }
 
 impl Notification {
@@ -143,14 +172,18 @@ impl Notification {
         severity: Severity,
         title: S,
         body: S,
-        properties: Value,
+        template_data: Value,
+        fields: HashMap<String, String>,
     ) -> Self {
         Self {
-            severity,
+            metadata: Metadata {
+                severity,
+                additional_fields: fields,
+            },
             content: Content::Template {
                 title_template: title.as_ref().to_string(),
                 body_template: body.as_ref().to_string(),
-                data: properties,
+                data: template_data,
             },
         }
     }
@@ -198,8 +231,7 @@ impl Config {
 #[derive(Default)]
 pub struct Bus {
     endpoints: HashMap<String, Box<dyn Endpoint>>,
-    groups: HashMap<String, GroupConfig>,
-    filters: Vec<FilterConfig>,
+    matchers: Vec<MatcherConfig>,
 }
 
 #[allow(unused_macros)]
@@ -304,23 +336,14 @@ impl Bus {
             );
         }
 
-        let groups: HashMap<String, GroupConfig> = config
+        let matchers = config
             .config
-            .convert_to_typed_array(GROUP_TYPENAME)
-            .map_err(|err| Error::ConfigDeserialization(err.into()))?
-            .into_iter()
-            .map(|group: GroupConfig| (group.name.clone(), group))
-            .collect();
-
-        let filters = config
-            .config
-            .convert_to_typed_array(FILTER_TYPENAME)
+            .convert_to_typed_array(MATCHER_TYPENAME)
             .map_err(|err| Error::ConfigDeserialization(err.into()))?;
 
         Ok(Bus {
             endpoints,
-            groups,
-            filters,
+            matchers,
         })
     }
 
@@ -330,77 +353,33 @@ impl Bus {
     }
 
     #[cfg(test)]
-    pub fn add_group(&mut self, group: GroupConfig) {
-        self.groups.insert(group.name.clone(), group);
-    }
-
-    #[cfg(test)]
-    pub fn add_filter(&mut self, filter: FilterConfig) {
-        self.filters.push(filter)
+    pub fn add_matcher(&mut self, filter: MatcherConfig) {
+        self.matchers.push(filter)
     }
 
-    /// Send a notification to a given target (endpoint or group).
+    /// Send a notification. Notification matchers will determine which targets will receive
+    /// the notification.
     ///
     /// Any errors will not be returned but only logged.
-    pub fn send(&self, endpoint_or_group: &str, notification: &Notification) {
-        let mut filter_matcher = FilterMatcher::new(&self.filters, notification);
-
-        if let Some(group) = self.groups.get(endpoint_or_group) {
-            if !Bus::check_filter(&mut filter_matcher, group.filter.as_deref()) {
-                log::info!("skipped target '{endpoint_or_group}', filter did not match");
-                return;
-            }
-
-            log::info!("target '{endpoint_or_group}' is a group, notifying all members...");
-
-            for endpoint in &group.endpoint {
-                self.send_via_single_endpoint(endpoint, notification, &mut filter_matcher);
-            }
-        } else {
-            self.send_via_single_endpoint(endpoint_or_group, notification, &mut filter_matcher);
-        }
-    }
+    pub fn send(&self, notification: &Notification) {
+        let targets = matcher::check_matches(self.matchers.as_slice(), notification);
 
-    fn check_filter(filter_matcher: &mut FilterMatcher, filter: Option<&str>) -> bool {
-        if let Some(filter) = filter {
-            match filter_matcher.check_filter_match(filter) {
-                // If the filter does not match, do nothing
-                Ok(r) => r,
-                Err(err) => {
-                    // If there is an error, only log it and still send
-                    log::error!("could not apply filter '{filter}': {err}");
-                    true
-                }
-            }
-        } else {
-            true
-        }
-    }
-
-    fn send_via_single_endpoint(
-        &self,
-        endpoint: &str,
-        notification: &Notification,
-        filter_matcher: &mut FilterMatcher,
-    ) {
-        if let Some(endpoint) = self.endpoints.get(endpoint) {
-            let name = endpoint.name();
-            if !Bus::check_filter(filter_matcher, endpoint.filter()) {
-                log::info!("skipped target '{name}', filter did not match");
-                return;
-            }
+        for target in targets {
+            if let Some(endpoint) = self.endpoints.get(target) {
+                let name = endpoint.name();
 
-            match endpoint.send(notification) {
-                Ok(_) => {
-                    log::info!("notified via target `{name}`");
-                }
-                Err(e) => {
-                    // Only log on errors, do not propagate fail to the caller.
-                    log::error!("could not notify via target `{name}`: {e}");
+                match endpoint.send(notification) {
+                    Ok(_) => {
+                        log::info!("notified via target `{name}`");
+                    }
+                    Err(e) => {
+                        // Only log on errors, do not propagate fail to the caller.
+                        log::error!("could not notify via target `{name}`: {e}");
+                    }
                 }
+            } else {
+                log::error!("could not notify via target '{target}', it does not exist");
             }
-        } else {
-            log::error!("could not notify via target '{endpoint}', it does not exist");
         }
     }
 
@@ -410,7 +389,11 @@ impl Bus {
     /// any errors to the caller.
     pub fn test_target(&self, target: &str) -> Result<(), Error> {
         let notification = Notification {
-            severity: Severity::Info,
+            metadata: Metadata {
+                severity: Severity::Info,
+                // TODO: what fields would make sense for test notifications?
+                additional_fields: Default::default(),
+            },
             content: Content::Template {
                 title_template: "Test notification".into(),
                 body_template: "This is a test of the notification target '{{ target }}'".into(),
@@ -418,29 +401,10 @@ impl Bus {
             },
         };
 
-        let mut errors: Vec<Box<dyn StdError + Send + Sync>> = Vec::new();
-
-        let mut my_send = |target: &str| -> Result<(), Error> {
-            if let Some(endpoint) = self.endpoints.get(target) {
-                if let Err(e) = endpoint.send(&notification) {
-                    errors.push(Box::new(e));
-                }
-            } else {
-                return Err(Error::TargetDoesNotExist(target.to_string()));
-            }
-            Ok(())
-        };
-
-        if let Some(group) = self.groups.get(target) {
-            for endpoint_name in &group.endpoint {
-                my_send(endpoint_name)?;
-            }
+        if let Some(endpoint) = self.endpoints.get(target) {
+            endpoint.send(&notification)?;
         } else {
-            my_send(target)?;
-        }
-
-        if !errors.is_empty() {
-            return Err(Error::TargetTestFailed(errors));
+            return Err(Error::TargetDoesNotExist(target.to_string()));
         }
 
         Ok(())
@@ -459,7 +423,6 @@ mod tests {
         // Needs to be an Rc so that we can clone MockEndpoint before
         // passing it to Bus, while still retaining a handle to the Vec
         messages: Rc<RefCell<Vec<Notification>>>,
-        filter: Option<String>,
     }
 
     impl Endpoint for MockEndpoint {
@@ -472,17 +435,12 @@ mod tests {
         fn name(&self) -> &str {
             self.name
         }
-
-        fn filter(&self) -> Option<&str> {
-            self.filter.as_deref()
-        }
     }
 
     impl MockEndpoint {
-        fn new(name: &'static str, filter: Option<String>) -> Self {
+        fn new(name: &'static str) -> Self {
             Self {
                 name,
-                filter,
                 ..Default::default()
             }
         }
@@ -494,113 +452,66 @@ mod tests {
 
     #[test]
     fn test_add_mock_endpoint() -> Result<(), Error> {
-        let mock = MockEndpoint::new("endpoint", None);
+        let mock = MockEndpoint::new("endpoint");
 
         let mut bus = Bus::default();
         bus.add_endpoint(Box::new(mock.clone()));
 
-        // Send directly to endpoint
-        bus.send(
-            "endpoint",
-            &Notification::new_templated(Severity::Info, "Title", "Body", Default::default()),
-        );
-        let messages = mock.messages();
-        assert_eq!(messages.len(), 1);
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_groups() -> Result<(), Error> {
-        let endpoint1 = MockEndpoint::new("mock1", None);
-        let endpoint2 = MockEndpoint::new("mock2", None);
-
-        let mut bus = Bus::default();
-
-        bus.add_group(GroupConfig {
-            name: "group1".to_string(),
-            endpoint: vec!["mock1".into()],
-            comment: None,
-            filter: None,
-        });
-
-        bus.add_group(GroupConfig {
-            name: "group2".to_string(),
-            endpoint: vec!["mock2".into()],
-            comment: None,
-            filter: None,
-        });
-
-        bus.add_endpoint(Box::new(endpoint1.clone()));
-        bus.add_endpoint(Box::new(endpoint2.clone()));
-
-        let send_to_group = |channel| {
-            let notification =
-                Notification::new_templated(Severity::Info, "Title", "Body", Default::default());
-            bus.send(channel, &notification)
+        let matcher = MatcherConfig {
+            target: Some(vec!["endpoint".into()]),
+            ..Default::default()
         };
 
-        send_to_group("group1");
-        assert_eq!(endpoint1.messages().len(), 1);
-        assert_eq!(endpoint2.messages().len(), 0);
+        bus.add_matcher(matcher);
 
-        send_to_group("group2");
-        assert_eq!(endpoint1.messages().len(), 1);
-        assert_eq!(endpoint2.messages().len(), 1);
+        // Send directly to endpoint
+        bus.send(&Notification::new_templated(
+            Severity::Info,
+            "Title",
+            "Body",
+            Default::default(),
+            Default::default(),
+        ));
+        let messages = mock.messages();
+        assert_eq!(messages.len(), 1);
 
         Ok(())
     }
 
     #[test]
-    fn test_severity_ordering() {
-        // Not intended to be exhaustive, just a quick
-        // sanity check ;)
-
-        assert!(Severity::Info < Severity::Notice);
-        assert!(Severity::Info < Severity::Warning);
-        assert!(Severity::Info < Severity::Error);
-        assert!(Severity::Error > Severity::Warning);
-        assert!(Severity::Warning > Severity::Notice);
-    }
-
-    #[test]
-    fn test_multiple_endpoints_with_different_filters() -> Result<(), Error> {
-        let endpoint1 = MockEndpoint::new("mock1", Some("filter1".into()));
-        let endpoint2 = MockEndpoint::new("mock2", Some("filter2".into()));
+    fn test_multiple_endpoints_with_different_matchers() -> Result<(), Error> {
+        let endpoint1 = MockEndpoint::new("mock1");
+        let endpoint2 = MockEndpoint::new("mock2");
 
         let mut bus = Bus::default();
 
         bus.add_endpoint(Box::new(endpoint1.clone()));
         bus.add_endpoint(Box::new(endpoint2.clone()));
 
-        bus.add_group(GroupConfig {
-            name: "channel1".to_string(),
-            endpoint: vec!["mock1".into(), "mock2".into()],
-            comment: None,
-            filter: None,
+        bus.add_matcher(MatcherConfig {
+            name: "matcher1".into(),
+            match_severity: Some(vec!["warning,error".parse()?]),
+            target: Some(vec!["mock1".into()]),
+            ..Default::default()
         });
 
-        bus.add_filter(FilterConfig {
-            name: "filter1".into(),
-            min_severity: Some(Severity::Warning),
-            mode: None,
-            invert_match: None,
-            comment: None,
-        });
-
-        bus.add_filter(FilterConfig {
-            name: "filter2".into(),
-            min_severity: Some(Severity::Error),
-            mode: None,
-            invert_match: None,
-            comment: None,
+        bus.add_matcher(MatcherConfig {
+            name: "matcher2".into(),
+            match_severity: Some(vec!["error".parse()?]),
+            target: Some(vec!["mock2".into()]),
+            ..Default::default()
         });
 
         let send_with_severity = |severity| {
-            let notification =
-                Notification::new_templated(severity, "Title", "Body", Default::default());
+            let notification = Notification::new_templated(
+                severity,
+                "Title",
+                "Body",
+                Default::default(),
+                Default::default(),
+            );
 
-            bus.send("channel1", &notification);
+            bus.send(&notification);
         };
 
         send_with_severity(Severity::Info);
diff --git a/proxmox-notify/src/matcher.rs b/proxmox-notify/src/matcher.rs
new file mode 100644
index 0000000..c24726d
--- /dev/null
+++ b/proxmox-notify/src/matcher.rs
@@ -0,0 +1,395 @@
+use regex::Regex;
+use std::collections::HashSet;
+use std::fmt;
+use std::fmt::Debug;
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api_types::COMMENT_SCHEMA;
+use proxmox_schema::{
+    api, const_regex, ApiStringFormat, Schema, StringSchema, Updater, SAFE_ID_REGEX_STR,
+};
+
+use crate::schema::ENTITY_NAME_SCHEMA;
+use crate::{Error, Notification, Severity};
+
+pub const MATCHER_TYPENAME: &str = "matcher";
+
+#[api]
+#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy)]
+#[serde(rename_all = "kebab-case")]
+pub enum MatchModeOperator {
+    /// All match statements have to match (AND)
+    #[default]
+    All,
+    /// At least one filter property has to match (OR)
+    Any,
+}
+
+impl MatchModeOperator {
+    /// Apply the mode operator to two bools, lhs and rhs
+    fn apply(&self, lhs: bool, rhs: bool) -> bool {
+        match self {
+            MatchModeOperator::All => lhs && rhs,
+            MatchModeOperator::Any => lhs || rhs,
+        }
+    }
+
+    // https://en.wikipedia.org/wiki/Identity_element
+    fn neutral_element(&self) -> bool {
+        match self {
+            MatchModeOperator::All => true,
+            MatchModeOperator::Any => false,
+        }
+    }
+}
+
+const_regex! {
+    pub MATCH_FIELD_ENTRY_REGEX = concat!(r"^(?:(exact|regex):)?(", SAFE_ID_REGEX_STR!(), r")=(.*)$");
+}
+
+pub const MATCH_FIELD_ENTRY_FORMAT: ApiStringFormat =
+    ApiStringFormat::VerifyFn(verify_field_matcher);
+
+fn verify_field_matcher(s: &str) -> Result<(), anyhow::Error> {
+    let _: FieldMatcher = s.parse()?;
+    Ok(())
+}
+
+pub const MATCH_FIELD_ENTRY_SCHEMA: Schema = StringSchema::new("Match metadata field.")
+    .format(&MATCH_FIELD_ENTRY_FORMAT)
+    .min_length(1)
+    .max_length(1024)
+    .schema();
+
+#[api(
+    properties: {
+        name: {
+            schema: ENTITY_NAME_SCHEMA,
+        },
+        comment: {
+            optional: true,
+            schema: COMMENT_SCHEMA,
+        },
+        "match-field": {
+            type: Array,
+            items: {
+                description: "Fields to match",
+                type: String
+            },
+            optional: true,
+        },
+        "match-severity": {
+            type: Array,
+            items: {
+                description: "Severity level to match.",
+                type: String
+            },
+            optional: true,
+        },
+        "target": {
+            type: Array,
+            items: {
+                schema: ENTITY_NAME_SCHEMA,
+            },
+            optional: true,
+        },
+    })]
+#[derive(Debug, Serialize, Deserialize, Updater, Default)]
+#[serde(rename_all = "kebab-case")]
+/// Config for Sendmail notification endpoints
+pub struct MatcherConfig {
+    /// Name of the matcher
+    #[updater(skip)]
+    pub name: String,
+
+    /// List of matched metadata fields
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub match_field: Option<Vec<FieldMatcher>>,
+
+    /// List of matched severity levels
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub match_severity: Option<Vec<SeverityMatcher>>,
+
+    /// Decide if 'all' or 'any' match statements must match
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mode: Option<MatchModeOperator>,
+
+    /// Invert match of the whole filter
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub invert_match: Option<bool>,
+
+    /// Targets to notify
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub target: Option<Vec<String>>,
+
+    /// Comment
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub comment: Option<String>,
+}
+
+#[derive(Clone, Debug)]
+pub enum FieldMatcher {
+    Exact {
+        field: String,
+        matched_value: String,
+    },
+    Regex {
+        field: String,
+        matched_regex: Regex,
+    },
+}
+
+proxmox_serde::forward_deserialize_to_from_str!(FieldMatcher);
+proxmox_serde::forward_serialize_to_display!(FieldMatcher);
+
+impl FieldMatcher {
+    fn matches(&self, notification: &Notification) -> bool {
+        match self {
+            FieldMatcher::Exact {
+                field,
+                matched_value,
+            } => {
+                let value = notification.metadata.additional_fields.get(field);
+
+                if let Some(value) = value {
+                    matched_value == value
+                } else {
+                    // Metadata field does not exist, so we do not match
+                    false
+                }
+            }
+            FieldMatcher::Regex {
+                field,
+                matched_regex,
+            } => {
+                let value = notification.metadata.additional_fields.get(field);
+
+                if let Some(value) = value {
+                    matched_regex.is_match(value)
+                } else {
+                    // Metadata field does not exist, so we do not match
+                    false
+                }
+            }
+        }
+    }
+}
+
+impl fmt::Display for FieldMatcher {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        // Attention, Display is used to implement Serialize, do not
+        // change the format.
+
+        match self {
+            FieldMatcher::Exact {
+                field,
+                matched_value,
+            } => {
+                write!(f, "exact:{field}={matched_value}")
+            }
+            FieldMatcher::Regex {
+                field,
+                matched_regex,
+            } => {
+                let re = matched_regex.as_str();
+                write!(f, "regex:{field}={re}")
+            }
+        }
+    }
+}
+
+impl FromStr for FieldMatcher {
+    type Err = Error;
+    fn from_str(s: &str) -> Result<Self, Error> {
+        if !MATCH_FIELD_ENTRY_REGEX.is_match(s) {
+            return Err(Error::FilterFailed(format!(
+                "invalid match-field statement: {s}"
+            )));
+        }
+
+        if let Some(remaining) = s.strip_prefix("regex:") {
+            match remaining.split_once('=') {
+                None => Err(Error::FilterFailed(format!(
+                    "invalid match-field statement: {s}"
+                ))),
+                Some((field, expected_value_regex)) => {
+                    let regex = Regex::new(expected_value_regex)
+                        .map_err(|err| Error::FilterFailed(format!("invalid regex: {err}")))?;
+
+                    Ok(Self::Regex {
+                        field: field.into(),
+                        matched_regex: regex,
+                    })
+                }
+            }
+        } else if let Some(remaining) = s.strip_prefix("exact:") {
+            match remaining.split_once('=') {
+                None => Err(Error::FilterFailed(format!(
+                    "invalid match-field statement: {s}"
+                ))),
+                Some((field, expected_value)) => Ok(Self::Exact {
+                    field: field.into(),
+                    matched_value: expected_value.into(),
+                }),
+            }
+        } else {
+            Err(Error::FilterFailed(format!(
+                "invalid match-field statement: {s}"
+            )))
+        }
+    }
+}
+
+impl MatcherConfig {
+    pub fn matches(&self, notification: &Notification) -> Result<Option<&[String]>, Error> {
+        let mode = self.mode.unwrap_or_default();
+
+        let mut is_match = mode.neutral_element();
+        is_match = mode.apply(is_match, self.check_severity_match(notification));
+        is_match = mode.apply(is_match, self.check_field_match(notification)?);
+
+        let invert_match = self.invert_match.unwrap_or_default();
+
+        Ok(if is_match != invert_match {
+            Some(self.target.as_deref().unwrap_or_default())
+        } else {
+            None
+        })
+    }
+
+    fn check_field_match(&self, notification: &Notification) -> Result<bool, Error> {
+        let mode = self.mode.unwrap_or_default();
+        let mut is_match = mode.neutral_element();
+
+        if let Some(match_field) = self.match_field.as_deref() {
+            for field_matcher in match_field {
+                // let field_matcher: FieldMatcher = match_stmt.parse()?;
+                is_match = mode.apply(is_match, field_matcher.matches(notification));
+            }
+        }
+
+        Ok(is_match)
+    }
+
+    fn check_severity_match(&self, notification: &Notification) -> bool {
+        let mode = self.mode.unwrap_or_default();
+        let mut is_match = mode.neutral_element();
+
+        if let Some(matchers) = self.match_severity.as_ref() {
+            for severity_matcher in matchers {
+                is_match = mode.apply(is_match, severity_matcher.matches(notification));
+            }
+        }
+
+        is_match
+    }
+}
+#[derive(Clone, Debug)]
+pub struct SeverityMatcher {
+    severities: Vec<Severity>,
+}
+
+proxmox_serde::forward_deserialize_to_from_str!(SeverityMatcher);
+proxmox_serde::forward_serialize_to_display!(SeverityMatcher);
+
+impl SeverityMatcher {
+    fn matches(&self, notification: &Notification) -> bool {
+        self.severities.contains(&notification.metadata.severity)
+    }
+}
+
+impl fmt::Display for SeverityMatcher {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let severities: Vec<String> = self.severities.iter().map(|s| format!("{s}")).collect();
+        f.write_str(&severities.join(","))
+    }
+}
+
+impl FromStr for SeverityMatcher {
+    type Err = Error;
+    fn from_str(s: &str) -> Result<Self, Error> {
+        let mut severities = Vec::new();
+
+        for element in s.split(',') {
+            let element = element.trim();
+            let severity: Severity = element.parse()?;
+
+            severities.push(severity)
+        }
+
+        Ok(Self { severities })
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum DeleteableMatcherProperty {
+    MatchSeverity,
+    MatchField,
+    Target,
+    Mode,
+    InvertMatch,
+    Comment,
+}
+
+pub fn check_matches<'a>(
+    matchers: &'a [MatcherConfig],
+    notification: &Notification,
+) -> HashSet<&'a str> {
+    let mut targets = HashSet::new();
+
+    for matcher in matchers {
+        match matcher.matches(notification) {
+            Ok(t) => {
+                let t = t.unwrap_or_default();
+                targets.extend(t.iter().map(|s| s.as_str()));
+            }
+            Err(err) => log::error!("matcher '{matcher}' failed: {err}", matcher = matcher.name),
+        }
+    }
+
+    targets
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde_json::Value;
+    use std::collections::HashMap;
+
+    #[test]
+    fn test_matching() {
+        let mut fields = HashMap::new();
+        fields.insert("foo".into(), "bar".into());
+
+        let notification =
+            Notification::new_templated(Severity::Notice, "test", "test", Value::Null, fields);
+
+        let matcher: FieldMatcher = "exact:foo=bar".parse().unwrap();
+        assert!(matcher.matches(&notification));
+
+        let matcher: FieldMatcher = "regex:foo=b.*".parse().unwrap();
+        assert!(matcher.matches(&notification));
+
+        let matcher: FieldMatcher = "regex:notthere=b.*".parse().unwrap();
+        assert!(!matcher.matches(&notification));
+
+        assert!("regex:'3=b.*".parse::<FieldMatcher>().is_err());
+        assert!("invalid:'bar=b.*".parse::<FieldMatcher>().is_err());
+    }
+    #[test]
+    fn test_severities() {
+        let notification = Notification::new_templated(
+            Severity::Notice,
+            "test",
+            "test",
+            Value::Null,
+            Default::default(),
+        );
+
+        let matcher: SeverityMatcher = "info,notice,warning,error".parse().unwrap();
+        assert!(matcher.matches(&notification));
+    }
+}
diff --git a/proxmox-notify/src/schema.rs b/proxmox-notify/src/schema.rs
index fc6c46c..93347a5 100644
--- a/proxmox-notify/src/schema.rs
+++ b/proxmox-notify/src/schema.rs
@@ -19,9 +19,8 @@ pub const BACKEND_NAME_SCHEMA: Schema = StringSchema::new("Notification backend
     .max_length(32)
     .schema();
 
-pub const ENTITY_NAME_SCHEMA: Schema =
-    StringSchema::new("Name schema for endpoints, filters and groups")
-        .format(&SAFE_ID_FORMAT)
-        .min_length(2)
-        .max_length(32)
-        .schema();
+pub const ENTITY_NAME_SCHEMA: Schema = StringSchema::new("Name schema for targets and matchers")
+    .format(&SAFE_ID_FORMAT)
+    .min_length(2)
+    .max_length(32)
+    .schema();
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 06/52] notify: add calendar matcher
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (4 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 05/52] notify: replace filters and groups with matcher-based system Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 07/52] notify: matcher: introduce common trait for match directives Lukas Wagner
                   ` (48 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This allows matching by a notification's timestamp:

matcher: foo
  match-calendar mon..fri 8-12

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-notify/src/api/matcher.rs |  6 +++
 proxmox-notify/src/lib.rs         |  4 ++
 proxmox-notify/src/matcher.rs     | 65 +++++++++++++++++++++++++++++++
 3 files changed, 75 insertions(+)

diff --git a/proxmox-notify/src/api/matcher.rs b/proxmox-notify/src/api/matcher.rs
index e37b74f..0592b14 100644
--- a/proxmox-notify/src/api/matcher.rs
+++ b/proxmox-notify/src/api/matcher.rs
@@ -80,6 +80,7 @@ pub fn update_matcher(
             match deleteable_property {
                 DeleteableMatcherProperty::MatchSeverity => matcher.match_severity = None,
                 DeleteableMatcherProperty::MatchField => matcher.match_field = None,
+                DeleteableMatcherProperty::MatchCalendar => matcher.match_calendar = None,
                 DeleteableMatcherProperty::Target => matcher.target = None,
                 DeleteableMatcherProperty::Mode => matcher.mode = None,
                 DeleteableMatcherProperty::InvertMatch => matcher.invert_match = None,
@@ -96,6 +97,10 @@ pub fn update_matcher(
         matcher.match_field = Some(match_field.clone());
     }
 
+    if let Some(match_calendar) = &matcher_updater.match_calendar {
+        matcher.match_calendar = Some(match_calendar.clone());
+    }
+
     if let Some(mode) = matcher_updater.mode {
         matcher.mode = Some(mode);
     }
@@ -200,6 +205,7 @@ matcher: matcher2
                 mode: Some(MatchModeOperator::Any),
                 match_field: None,
                 match_severity: None,
+                match_calendar: None,
                 invert_match: Some(true),
                 target: Some(vec!["foo".into()]),
                 comment: Some("new comment".into()),
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index 1f95ae0..9997cef 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -154,6 +154,8 @@ pub enum Content {
 pub struct Metadata {
     /// Notification severity
     severity: Severity,
+    /// Timestamp of the notification as a UNIX epoch
+    timestamp: i64,
     /// Additional fields for additional key-value metadata
     additional_fields: HashMap<String, String>,
 }
@@ -179,6 +181,7 @@ impl Notification {
             metadata: Metadata {
                 severity,
                 additional_fields: fields,
+                timestamp: proxmox_time::epoch_i64(),
             },
             content: Content::Template {
                 title_template: title.as_ref().to_string(),
@@ -393,6 +396,7 @@ impl Bus {
                 severity: Severity::Info,
                 // TODO: what fields would make sense for test notifications?
                 additional_fields: Default::default(),
+                timestamp: proxmox_time::epoch_i64(),
             },
             content: Content::Template {
                 title_template: "Test notification".into(),
diff --git a/proxmox-notify/src/matcher.rs b/proxmox-notify/src/matcher.rs
index c24726d..b03d11d 100644
--- a/proxmox-notify/src/matcher.rs
+++ b/proxmox-notify/src/matcher.rs
@@ -10,6 +10,7 @@ use proxmox_schema::api_types::COMMENT_SCHEMA;
 use proxmox_schema::{
     api, const_regex, ApiStringFormat, Schema, StringSchema, Updater, SAFE_ID_REGEX_STR,
 };
+use proxmox_time::{parse_daily_duration, DailyDuration};
 
 use crate::schema::ENTITY_NAME_SCHEMA;
 use crate::{Error, Notification, Severity};
@@ -88,6 +89,14 @@ pub const MATCH_FIELD_ENTRY_SCHEMA: Schema = StringSchema::new("Match metadata f
             },
             optional: true,
         },
+        "match-calendar": {
+            type: Array,
+            items: {
+                description: "Time stamps to match",
+                type: String
+            },
+            optional: true,
+        },
         "target": {
             type: Array,
             items: {
@@ -112,6 +121,10 @@ pub struct MatcherConfig {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub match_severity: Option<Vec<SeverityMatcher>>,
 
+    /// List of matched severity levels
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub match_calendar: Option<Vec<CalendarMatcher>>,
+
     /// Decide if 'all' or 'any' match statements must match
     #[serde(skip_serializing_if = "Option::is_none")]
     pub mode: Option<MatchModeOperator>,
@@ -249,6 +262,7 @@ impl MatcherConfig {
         let mut is_match = mode.neutral_element();
         is_match = mode.apply(is_match, self.check_severity_match(notification));
         is_match = mode.apply(is_match, self.check_field_match(notification)?);
+        is_match = mode.apply(is_match, self.check_calendar_match(notification)?);
 
         let invert_match = self.invert_match.unwrap_or_default();
 
@@ -285,6 +299,19 @@ impl MatcherConfig {
 
         is_match
     }
+
+    fn check_calendar_match(&self, notification: &Notification) -> Result<bool, Error> {
+        let mode = self.mode.unwrap_or_default();
+        let mut is_match = mode.neutral_element();
+
+        if let Some(matchers) = self.match_calendar.as_ref() {
+            for matcher in matchers {
+                is_match = mode.apply(is_match, matcher.matches(notification)?);
+            }
+        }
+
+        Ok(is_match)
+    }
 }
 #[derive(Clone, Debug)]
 pub struct SeverityMatcher {
@@ -323,11 +350,49 @@ impl FromStr for SeverityMatcher {
     }
 }
 
+/// Match timestamp of the notification.
+#[derive(Clone, Debug)]
+pub struct CalendarMatcher {
+    schedule: DailyDuration,
+    original: String,
+}
+
+proxmox_serde::forward_deserialize_to_from_str!(CalendarMatcher);
+proxmox_serde::forward_serialize_to_display!(CalendarMatcher);
+
+impl CalendarMatcher {
+    fn matches(&self, notification: &Notification) -> Result<bool, Error> {
+        self.schedule
+            .time_match(notification.metadata.timestamp, false)
+            .map_err(|err| Error::Generic(format!("could not match timestamp: {err}")))
+    }
+}
+
+impl fmt::Display for CalendarMatcher {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str(&self.original)
+    }
+}
+
+impl FromStr for CalendarMatcher {
+    type Err = Error;
+    fn from_str(s: &str) -> Result<Self, Error> {
+        let schedule = parse_daily_duration(s)
+            .map_err(|e| Error::Generic(format!("could not parse schedule: {e}")))?;
+
+        Ok(Self {
+            schedule,
+            original: s.to_string(),
+        })
+    }
+}
+
 #[derive(Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub enum DeleteableMatcherProperty {
     MatchSeverity,
     MatchField,
+    MatchCalendar,
     Target,
     Mode,
     InvertMatch,
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 07/52] notify: matcher: introduce common trait for match directives
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (5 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 06/52] notify: add calendar matcher Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 08/52] notify: let a matcher always match if it has no matching directives Lukas Wagner
                   ` (47 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This allows us to make the match-checking code a bit shorter.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-notify/src/matcher.rs | 92 +++++++++++++++++------------------
 1 file changed, 45 insertions(+), 47 deletions(-)

diff --git a/proxmox-notify/src/matcher.rs b/proxmox-notify/src/matcher.rs
index b03d11d..e299fd0 100644
--- a/proxmox-notify/src/matcher.rs
+++ b/proxmox-notify/src/matcher.rs
@@ -142,6 +142,11 @@ pub struct MatcherConfig {
     pub comment: Option<String>,
 }
 
+trait MatchDirective {
+    fn matches(&self, notification: &Notification) -> Result<bool, Error>;
+}
+
+/// Check if the notification metadata fields match
 #[derive(Clone, Debug)]
 pub enum FieldMatcher {
     Exact {
@@ -157,9 +162,9 @@ pub enum FieldMatcher {
 proxmox_serde::forward_deserialize_to_from_str!(FieldMatcher);
 proxmox_serde::forward_serialize_to_display!(FieldMatcher);
 
-impl FieldMatcher {
-    fn matches(&self, notification: &Notification) -> bool {
-        match self {
+impl MatchDirective for FieldMatcher {
+    fn matches(&self, notification: &Notification) -> Result<bool, Error> {
+        Ok(match self {
             FieldMatcher::Exact {
                 field,
                 matched_value,
@@ -186,7 +191,7 @@ impl FieldMatcher {
                     false
                 }
             }
-        }
+        })
     }
 }
 
@@ -260,9 +265,22 @@ impl MatcherConfig {
         let mode = self.mode.unwrap_or_default();
 
         let mut is_match = mode.neutral_element();
-        is_match = mode.apply(is_match, self.check_severity_match(notification));
-        is_match = mode.apply(is_match, self.check_field_match(notification)?);
-        is_match = mode.apply(is_match, self.check_calendar_match(notification)?);
+
+        if let Some(severity_matchers) = self.match_severity.as_deref() {
+            is_match = mode.apply(
+                is_match,
+                self.check_matches(notification, severity_matchers)?,
+            );
+        }
+        if let Some(field_matchers) = self.match_field.as_deref() {
+            is_match = mode.apply(is_match, self.check_matches(notification, field_matchers)?);
+        }
+        if let Some(calendar_matchers) = self.match_calendar.as_deref() {
+            is_match = mode.apply(
+                is_match,
+                self.check_matches(notification, calendar_matchers)?,
+            );
+        }
 
         let invert_match = self.invert_match.unwrap_or_default();
 
@@ -273,46 +291,24 @@ impl MatcherConfig {
         })
     }
 
-    fn check_field_match(&self, notification: &Notification) -> Result<bool, Error> {
-        let mode = self.mode.unwrap_or_default();
-        let mut is_match = mode.neutral_element();
-
-        if let Some(match_field) = self.match_field.as_deref() {
-            for field_matcher in match_field {
-                // let field_matcher: FieldMatcher = match_stmt.parse()?;
-                is_match = mode.apply(is_match, field_matcher.matches(notification));
-            }
-        }
-
-        Ok(is_match)
-    }
-
-    fn check_severity_match(&self, notification: &Notification) -> bool {
+    /// Check if given `MatchDirectives` match a notification.
+    fn check_matches(
+        &self,
+        notification: &Notification,
+        matchers: &[impl MatchDirective],
+    ) -> Result<bool, Error> {
         let mode = self.mode.unwrap_or_default();
         let mut is_match = mode.neutral_element();
 
-        if let Some(matchers) = self.match_severity.as_ref() {
-            for severity_matcher in matchers {
-                is_match = mode.apply(is_match, severity_matcher.matches(notification));
-            }
-        }
-
-        is_match
-    }
-
-    fn check_calendar_match(&self, notification: &Notification) -> Result<bool, Error> {
-        let mode = self.mode.unwrap_or_default();
-        let mut is_match = mode.neutral_element();
-
-        if let Some(matchers) = self.match_calendar.as_ref() {
-            for matcher in matchers {
-                is_match = mode.apply(is_match, matcher.matches(notification)?);
-            }
+        for field_matcher in matchers {
+            is_match = mode.apply(is_match, field_matcher.matches(notification)?);
         }
 
         Ok(is_match)
     }
 }
+
+/// Match severity of the notification.
 #[derive(Clone, Debug)]
 pub struct SeverityMatcher {
     severities: Vec<Severity>,
@@ -321,9 +317,11 @@ pub struct SeverityMatcher {
 proxmox_serde::forward_deserialize_to_from_str!(SeverityMatcher);
 proxmox_serde::forward_serialize_to_display!(SeverityMatcher);
 
-impl SeverityMatcher {
-    fn matches(&self, notification: &Notification) -> bool {
-        self.severities.contains(&notification.metadata.severity)
+/// Common trait implemented by all matching directives
+impl MatchDirective for SeverityMatcher {
+    /// Check if this directive matches a given notification
+    fn matches(&self, notification: &Notification) -> Result<bool, Error> {
+        Ok(self.severities.contains(&notification.metadata.severity))
     }
 }
 
@@ -360,7 +358,7 @@ pub struct CalendarMatcher {
 proxmox_serde::forward_deserialize_to_from_str!(CalendarMatcher);
 proxmox_serde::forward_serialize_to_display!(CalendarMatcher);
 
-impl CalendarMatcher {
+impl MatchDirective for CalendarMatcher {
     fn matches(&self, notification: &Notification) -> Result<bool, Error> {
         self.schedule
             .time_match(notification.metadata.timestamp, false)
@@ -433,13 +431,13 @@ mod tests {
             Notification::new_templated(Severity::Notice, "test", "test", Value::Null, fields);
 
         let matcher: FieldMatcher = "exact:foo=bar".parse().unwrap();
-        assert!(matcher.matches(&notification));
+        assert!(matcher.matches(&notification).unwrap());
 
         let matcher: FieldMatcher = "regex:foo=b.*".parse().unwrap();
-        assert!(matcher.matches(&notification));
+        assert!(matcher.matches(&notification).unwrap());
 
         let matcher: FieldMatcher = "regex:notthere=b.*".parse().unwrap();
-        assert!(!matcher.matches(&notification));
+        assert!(!matcher.matches(&notification).unwrap());
 
         assert!("regex:'3=b.*".parse::<FieldMatcher>().is_err());
         assert!("invalid:'bar=b.*".parse::<FieldMatcher>().is_err());
@@ -455,6 +453,6 @@ mod tests {
         );
 
         let matcher: SeverityMatcher = "info,notice,warning,error".parse().unwrap();
-        assert!(matcher.matches(&notification));
+        assert!(matcher.matches(&notification).unwrap());
     }
 }
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 08/52] notify: let a matcher always match if it has no matching directives
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (6 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 07/52] notify: matcher: introduce common trait for match directives Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 09/52] sys: email: add `forward` Lukas Wagner
                   ` (46 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This should be a bit more intuitive to users than the current
behavior, which is 'always match' for mode==all and 'never match' for
mode==any. The current behavior originates in the neutral element of
the underlying logical operation (and, or).

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-notify/src/matcher.rs | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/proxmox-notify/src/matcher.rs b/proxmox-notify/src/matcher.rs
index e299fd0..553ca87 100644
--- a/proxmox-notify/src/matcher.rs
+++ b/proxmox-notify/src/matcher.rs
@@ -265,17 +265,22 @@ impl MatcherConfig {
         let mode = self.mode.unwrap_or_default();
 
         let mut is_match = mode.neutral_element();
+        // If there are no matching directives, the matcher will always match
+        let mut no_matchers = true;
 
         if let Some(severity_matchers) = self.match_severity.as_deref() {
+            no_matchers = false;
             is_match = mode.apply(
                 is_match,
                 self.check_matches(notification, severity_matchers)?,
             );
         }
         if let Some(field_matchers) = self.match_field.as_deref() {
+            no_matchers = false;
             is_match = mode.apply(is_match, self.check_matches(notification, field_matchers)?);
         }
         if let Some(calendar_matchers) = self.match_calendar.as_deref() {
+            no_matchers = false;
             is_match = mode.apply(
                 is_match,
                 self.check_matches(notification, calendar_matchers)?,
@@ -284,7 +289,7 @@ impl MatcherConfig {
 
         let invert_match = self.invert_match.unwrap_or_default();
 
-        Ok(if is_match != invert_match {
+        Ok(if is_match != invert_match || no_matchers {
             Some(self.target.as_deref().unwrap_or_default())
         } else {
             None
@@ -455,4 +460,25 @@ mod tests {
         let matcher: SeverityMatcher = "info,notice,warning,error".parse().unwrap();
         assert!(matcher.matches(&notification).unwrap());
     }
+
+    #[test]
+    fn test_empty_matcher_matches_always() {
+        let notification = Notification::new_templated(
+            Severity::Notice,
+            "test",
+            "test",
+            Value::Null,
+            Default::default(),
+        );
+
+        for mode in [MatchModeOperator::All, MatchModeOperator::Any] {
+            let config = MatcherConfig {
+                name: "matcher".to_string(),
+                mode: Some(mode),
+                ..Default::default()
+            };
+
+            assert!(config.matches(&notification).unwrap().is_some())
+        }
+    }
 }
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 09/52] sys: email: add `forward`
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (7 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 08/52] notify: let a matcher always match if it has no matching directives Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 10/52] notify: add mechanisms for email message forwarding Lukas Wagner
                   ` (45 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This new function forwards an email to new recipients.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-sys/src/email.rs | 52 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 51 insertions(+), 1 deletion(-)

diff --git a/proxmox-sys/src/email.rs b/proxmox-sys/src/email.rs
index 8b3a1b6..c94f634 100644
--- a/proxmox-sys/src/email.rs
+++ b/proxmox-sys/src/email.rs
@@ -3,7 +3,7 @@
 use std::io::Write;
 use std::process::{Command, Stdio};
 
-use anyhow::{bail, Error};
+use anyhow::{bail, format_err, Error};
 
 /// Sends multi-part mail with text and/or html to a list of recipients
 ///
@@ -110,6 +110,56 @@ pub fn sendmail(
     Ok(())
 }
 
+/// Forwards an email message to a given list of recipients.
+///
+/// ``sendmail`` is used for sending the mail, thus `message` must be
+/// compatible with that (the message is piped into stdin unmodified).
+pub fn forward(
+    mailto: &[&str],
+    mailfrom: &str,
+    message: &[u8],
+    uid: Option<u32>,
+) -> Result<(), Error> {
+    use std::os::unix::process::CommandExt;
+
+    if mailto.is_empty() {
+        bail!("At least one recipient has to be specified!")
+    }
+
+    let mut builder = Command::new("/usr/sbin/sendmail");
+
+    builder
+        .args([
+            "-N", "never", // never send DSN (avoid mail loops)
+            "-f", mailfrom, "--",
+        ])
+        .args(mailto)
+        .stdin(Stdio::piped())
+        .stdout(Stdio::null())
+        .stderr(Stdio::null());
+
+    if let Some(uid) = uid {
+        builder.uid(uid);
+    }
+
+    let mut process = builder
+        .spawn()
+        .map_err(|err| format_err!("could not spawn sendmail process: {err}"))?;
+
+    process
+        .stdin
+        .take()
+        .unwrap()
+        .write_all(message)
+        .map_err(|err| format_err!("couldn't write to sendmail stdin: {err}"))?;
+
+    process
+        .wait()
+        .map_err(|err| format_err!("sendmail did not exit successfully: {err}"))?;
+
+    Ok(())
+}
+
 #[cfg(test)]
 mod test {
     use crate::email::sendmail;
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 10/52] notify: add mechanisms for email message forwarding
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (8 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 09/52] sys: email: add `forward` Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 11/52] notify: add PVE/PBS context Lukas Wagner
                   ` (44 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

As preparation for the integration of `proxmox-mail-foward` into the
notification system, this commit makes a few changes that allow us to
forward raw email messages (as passed from postfix).

For mail-based notification targets, the email will be forwarded
as-is, including all headers. The only thing that changes is the
message envelope.
For other notification targets, the mail is parsed using the
`mail-parser` crate, which allows us to extract a subject and a body.
As a body we use the plain-text version of the mail. If an email is
HTML-only, the `mail-parser` crate will automatically attempt to
transform the HTML into readable plain text.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Cargo.toml                               |  1 +
 proxmox-notify/Cargo.toml                |  2 ++
 proxmox-notify/src/endpoints/gotify.rs   |  3 ++
 proxmox-notify/src/endpoints/sendmail.rs |  5 +++
 proxmox-notify/src/lib.rs                | 41 ++++++++++++++++++++++++
 5 files changed, 52 insertions(+)

diff --git a/Cargo.toml b/Cargo.toml
index f8bc181..3d81d85 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -64,6 +64,7 @@ lazy_static = "1.4"
 ldap3 = { version = "0.11", default-features = false }
 libc = "0.2.107"
 log = "0.4.17"
+mail-parser = "0.8.2"
 native-tls = "0.2"
 nix = "0.26.1"
 once_cell = "1.3.1"
diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml
index 4812896..f2b4db5 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -12,6 +12,7 @@ anyhow.workspace = true
 handlebars = { workspace = true }
 lazy_static.workspace = true
 log.workspace = true
+mail-parser = { workspace = true, optional = true }
 once_cell.workspace = true
 openssl.workspace = true
 proxmox-http = { workspace = true, features = ["client-sync"], optional = true }
@@ -28,5 +29,6 @@ serde_json.workspace = true
 
 [features]
 default = ["sendmail", "gotify"]
+mail-forwarder = ["dep:mail-parser"]
 sendmail = ["dep:proxmox-sys"]
 gotify = ["dep:proxmox-http"]
diff --git a/proxmox-notify/src/endpoints/gotify.rs b/proxmox-notify/src/endpoints/gotify.rs
index 1c307a4..5713d99 100644
--- a/proxmox-notify/src/endpoints/gotify.rs
+++ b/proxmox-notify/src/endpoints/gotify.rs
@@ -19,6 +19,7 @@ fn severity_to_priority(level: Severity) -> u32 {
         Severity::Notice => 3,
         Severity::Warning => 5,
         Severity::Error => 9,
+        Severity::Unknown => 3,
     }
 }
 
@@ -94,6 +95,8 @@ impl Endpoint for GotifyEndpoint {
 
                 (rendered_title, rendered_message)
             }
+            #[cfg(feature = "mail-forwarder")]
+            Content::ForwardedMail { title, body, .. } => (title.clone(), body.clone()),
         };
 
         // We don't have a TemplateRenderer::Markdown yet, so simply put everything
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index a601744..3ef33b6 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -134,6 +134,11 @@ impl Endpoint for SendmailEndpoint {
                 )
                 .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into()))
             }
+            #[cfg(feature = "mail-forwarder")]
+            Content::ForwardedMail { raw, uid, .. } => {
+                proxmox_sys::email::forward(&recipients_str, &mailfrom, raw, *uid)
+                    .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into()))
+            }
         }
     }
 
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index 9997cef..ada1b0a 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -102,6 +102,8 @@ pub enum Severity {
     Warning,
     /// Error
     Error,
+    /// Unknown severity (e.g. forwarded system mails)
+    Unknown,
 }
 
 impl Display for Severity {
@@ -111,6 +113,7 @@ impl Display for Severity {
             Severity::Notice => f.write_str("notice"),
             Severity::Warning => f.write_str("warning"),
             Severity::Error => f.write_str("error"),
+            Severity::Unknown => f.write_str("unknown"),
         }
     }
 }
@@ -123,6 +126,7 @@ impl FromStr for Severity {
             "notice" => Ok(Self::Notice),
             "warning" => Ok(Self::Warning),
             "error" => Ok(Self::Error),
+            "unknown" => Ok(Self::Unknown),
             _ => Err(Error::Generic(format!("invalid severity {s}"))),
         }
     }
@@ -148,6 +152,18 @@ pub enum Content {
         /// Data that can be used for template rendering.
         data: Value,
     },
+    #[cfg(feature = "mail-forwarder")]
+    ForwardedMail {
+        /// Raw mail contents
+        raw: Vec<u8>,
+        /// Fallback title
+        title: String,
+        /// Fallback body
+        body: String,
+        /// UID to use when calling sendmail
+        #[allow(dead_code)] // Unused in some feature flag permutations
+        uid: Option<u32>,
+    },
 }
 
 #[derive(Debug, Clone)]
@@ -190,6 +206,31 @@ impl Notification {
             },
         }
     }
+    #[cfg(feature = "mail-forwarder")]
+    pub fn new_forwarded_mail(raw_mail: &[u8], uid: Option<u32>) -> Result<Self, Error> {
+        let message = mail_parser::Message::parse(raw_mail)
+            .ok_or_else(|| Error::Generic("could not parse forwarded email".to_string()))?;
+
+        let title = message.subject().unwrap_or_default().into();
+        let body = message.body_text(0).unwrap_or_default().into();
+
+        Ok(Self {
+            // Unfortunately we cannot reasonably infer the severity from the
+            // mail contents, so just set it to the highest for now so that
+            // it is not filtered out.
+            content: Content::ForwardedMail {
+                raw: raw_mail.into(),
+                title,
+                body,
+                uid,
+            },
+            metadata: Metadata {
+                severity: Severity::Unknown,
+                additional_fields: Default::default(),
+                timestamp: proxmox_time::epoch_i64(),
+            },
+        })
+    }
 }
 
 /// Notification configuration
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 11/52] notify: add PVE/PBS context
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (9 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 10/52] notify: add mechanisms for email message forwarding Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 12/52] notify: add 'smtp' endpoint Lukas Wagner
                   ` (43 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This commit moves PVEContext from `proxmox-perl-rs` into the
`proxmox-notify` crate, since we now also need to access it from
`promxox-mail-forward`. The context is now hidden behind a feature
flag `pve-context`, ensuring that we only compile it when needed.

This commit adds PBSContext, since we now require it for
`proxmox-mail-forward`. Some of the code for PBSContext comes
from `proxmox-mail-forward`.

This commit also changes the global context from being stored in a
`once_cell` to a regular `Mutex`, since we now need to set/reset
the context in `proxmox-mail-forward`.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    Changes v2 -> v3:
      - no changes

 proxmox-notify/Cargo.toml            |   3 +-
 proxmox-notify/src/context.rs        |  21 -----
 proxmox-notify/src/context/common.rs |  27 ++++++
 proxmox-notify/src/context/mod.rs    |  36 ++++++++
 proxmox-notify/src/context/pbs.rs    | 130 +++++++++++++++++++++++++++
 proxmox-notify/src/context/pve.rs    |  82 +++++++++++++++++
 6 files changed, 277 insertions(+), 22 deletions(-)
 delete mode 100644 proxmox-notify/src/context.rs
 create mode 100644 proxmox-notify/src/context/common.rs
 create mode 100644 proxmox-notify/src/context/mod.rs
 create mode 100644 proxmox-notify/src/context/pbs.rs
 create mode 100644 proxmox-notify/src/context/pve.rs

diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml
index f2b4db5..7a3d434 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -13,7 +13,6 @@ handlebars = { workspace = true }
 lazy_static.workspace = true
 log.workspace = true
 mail-parser = { workspace = true, optional = true }
-once_cell.workspace = true
 openssl.workspace = true
 proxmox-http = { workspace = true, features = ["client-sync"], optional = true }
 proxmox-http-error.workspace = true
@@ -32,3 +31,5 @@ default = ["sendmail", "gotify"]
 mail-forwarder = ["dep:mail-parser"]
 sendmail = ["dep:proxmox-sys"]
 gotify = ["dep:proxmox-http"]
+pve-context = ["dep:proxmox-sys"]
+pbs-context = ["dep:proxmox-sys"]
diff --git a/proxmox-notify/src/context.rs b/proxmox-notify/src/context.rs
deleted file mode 100644
index 370c7ee..0000000
--- a/proxmox-notify/src/context.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use std::fmt::Debug;
-
-use once_cell::sync::OnceCell;
-
-pub trait Context: Send + Sync + Debug {
-    fn lookup_email_for_user(&self, user: &str) -> Option<String>;
-    fn default_sendmail_author(&self) -> String;
-    fn default_sendmail_from(&self) -> String;
-    fn http_proxy_config(&self) -> Option<String>;
-}
-
-static CONTEXT: OnceCell<&'static dyn Context> = OnceCell::new();
-
-pub fn set_context(context: &'static dyn Context) {
-    CONTEXT.set(context).expect("context has already been set");
-}
-
-#[allow(unused)] // context is not used if all endpoint features are disabled
-pub(crate) fn context() -> &'static dyn Context {
-    *CONTEXT.get().expect("context has not been yet")
-}
diff --git a/proxmox-notify/src/context/common.rs b/proxmox-notify/src/context/common.rs
new file mode 100644
index 0000000..7580bd1
--- /dev/null
+++ b/proxmox-notify/src/context/common.rs
@@ -0,0 +1,27 @@
+use std::path::Path;
+
+pub(crate) fn attempt_file_read<P: AsRef<Path>>(path: P) -> Option<String> {
+    match proxmox_sys::fs::file_read_optional_string(path) {
+        Ok(contents) => contents,
+        Err(err) => {
+            log::error!("{err}");
+            None
+        }
+    }
+}
+
+pub(crate) fn lookup_datacenter_config_key(content: &str, key: &str) -> Option<String> {
+    let key_prefix = format!("{key}:");
+    normalize_for_return(
+        content
+            .lines()
+            .find_map(|line| line.strip_prefix(&key_prefix)),
+    )
+}
+
+pub(crate) fn normalize_for_return(s: Option<&str>) -> Option<String> {
+    match s?.trim() {
+        "" => None,
+        s => Some(s.to_string()),
+    }
+}
diff --git a/proxmox-notify/src/context/mod.rs b/proxmox-notify/src/context/mod.rs
new file mode 100644
index 0000000..99d86de
--- /dev/null
+++ b/proxmox-notify/src/context/mod.rs
@@ -0,0 +1,36 @@
+use std::fmt::Debug;
+use std::sync::Mutex;
+
+#[cfg(any(feature = "pve-context", feature = "pbs-context"))]
+pub mod common;
+#[cfg(feature = "pbs-context")]
+pub mod pbs;
+#[cfg(feature = "pve-context")]
+pub mod pve;
+
+/// Product-specific context
+pub trait Context: Send + Sync + Debug {
+    /// Look up a user's email address from users.cfg
+    fn lookup_email_for_user(&self, user: &str) -> Option<String>;
+    /// Default mail author for mail-based targets
+    fn default_sendmail_author(&self) -> String;
+    /// Default from address for sendmail-based targets
+    fn default_sendmail_from(&self) -> String;
+    /// Proxy configuration for the current node
+    fn http_proxy_config(&self) -> Option<String>;
+}
+
+static CONTEXT: Mutex<Option<&'static dyn Context>> = Mutex::new(None);
+
+/// Set the product-specific context
+pub fn set_context(context: &'static dyn Context) {
+    *CONTEXT.lock().unwrap() = Some(context);
+}
+
+/// Get product-specific context.
+///
+/// Panics if the context has not been set yet.
+#[allow(unused)] // context is not used if all endpoint features are disabled
+pub(crate) fn context() -> &'static dyn Context {
+    (*CONTEXT.lock().unwrap()).expect("context for proxmox-notify has not been set yet")
+}
diff --git a/proxmox-notify/src/context/pbs.rs b/proxmox-notify/src/context/pbs.rs
new file mode 100644
index 0000000..b5d3168
--- /dev/null
+++ b/proxmox-notify/src/context/pbs.rs
@@ -0,0 +1,130 @@
+use serde::Deserialize;
+
+use proxmox_schema::{ObjectSchema, Schema, StringSchema};
+use proxmox_section_config::{SectionConfig, SectionConfigPlugin};
+
+use crate::context::{common, Context};
+
+const PBS_USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg";
+const PBS_NODE_CFG_FILENAME: &str = "/etc/proxmox-backup/node.cfg";
+
+// FIXME: Switch to the actual schema when possible in terms of dependency.
+// It's safe to assume that the config was written with the actual schema restrictions, so parsing
+// it with the less restrictive schema should be enough for the purpose of getting the mail address.
+const DUMMY_ID_SCHEMA: Schema = StringSchema::new("dummy ID").min_length(3).schema();
+const DUMMY_EMAIL_SCHEMA: Schema = StringSchema::new("dummy email").schema();
+const DUMMY_USER_SCHEMA: ObjectSchema = ObjectSchema {
+    description: "minimal PBS user",
+    properties: &[
+        ("userid", false, &DUMMY_ID_SCHEMA),
+        ("email", true, &DUMMY_EMAIL_SCHEMA),
+    ],
+    additional_properties: true,
+    default_key: None,
+};
+
+#[derive(Deserialize)]
+struct DummyPbsUser {
+    pub email: Option<String>,
+}
+
+/// Extract the root user's email address from the PBS user config.
+fn lookup_mail_address(content: &str, username: &str) -> Option<String> {
+    let mut config = SectionConfig::new(&DUMMY_ID_SCHEMA).allow_unknown_sections(true);
+    let user_plugin = SectionConfigPlugin::new(
+        "user".to_string(),
+        Some("userid".to_string()),
+        &DUMMY_USER_SCHEMA,
+    );
+    config.register_plugin(user_plugin);
+
+    match config.parse(PBS_USER_CFG_FILENAME, content) {
+        Ok(parsed) => {
+            parsed.sections.get(username)?;
+            match parsed.lookup::<DummyPbsUser>("user", username) {
+                Ok(user) => common::normalize_for_return(user.email.as_deref()),
+                Err(err) => {
+                    log::error!("unable to parse {PBS_USER_CFG_FILENAME}: {err}");
+                    None
+                }
+            }
+        }
+        Err(err) => {
+            log::error!("unable to parse {PBS_USER_CFG_FILENAME}: {err}");
+            None
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct PBSContext;
+
+pub static PBS_CONTEXT: PBSContext = PBSContext;
+
+impl Context for PBSContext {
+    fn lookup_email_for_user(&self, user: &str) -> Option<String> {
+        let content = common::attempt_file_read(PBS_USER_CFG_FILENAME);
+        content.and_then(|content| lookup_mail_address(&content, user))
+    }
+
+    fn default_sendmail_author(&self) -> String {
+        "Proxmox Backup Server".into()
+    }
+
+    fn default_sendmail_from(&self) -> String {
+        let content = common::attempt_file_read(PBS_NODE_CFG_FILENAME);
+        content
+            .and_then(|content| common::lookup_datacenter_config_key(&content, "email-from"))
+            .unwrap_or_else(|| String::from("root"))
+    }
+
+    fn http_proxy_config(&self) -> Option<String> {
+        let content = common::attempt_file_read(PBS_NODE_CFG_FILENAME);
+        content.and_then(|content| common::lookup_datacenter_config_key(&content, "http-proxy"))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const USER_CONFIG: &str = "
+user: root@pam
+	email root@example.com
+
+user: test@pbs
+	enable true
+	expire 0
+    ";
+
+    #[test]
+    fn test_parse_mail() {
+        assert_eq!(
+            lookup_mail_address(USER_CONFIG, "root@pam"),
+            Some("root@example.com".to_string())
+        );
+        assert_eq!(lookup_mail_address(USER_CONFIG, "test@pbs"), None);
+    }
+
+    const NODE_CONFIG: &str = "
+default-lang: de
+email-from: root@example.com
+http-proxy: http://localhost:1234
+    ";
+
+    #[test]
+    fn test_parse_node_config() {
+        assert_eq!(
+            common::lookup_datacenter_config_key(NODE_CONFIG, "email-from"),
+            Some("root@example.com".to_string())
+        );
+        assert_eq!(
+            common::lookup_datacenter_config_key(NODE_CONFIG, "http-proxy"),
+            Some("http://localhost:1234".to_string())
+        );
+        assert_eq!(
+            common::lookup_datacenter_config_key(NODE_CONFIG, "foo"),
+            None
+        );
+    }
+}
diff --git a/proxmox-notify/src/context/pve.rs b/proxmox-notify/src/context/pve.rs
new file mode 100644
index 0000000..f263c95
--- /dev/null
+++ b/proxmox-notify/src/context/pve.rs
@@ -0,0 +1,82 @@
+use crate::context::{common, Context};
+
+fn lookup_mail_address(content: &str, user: &str) -> Option<String> {
+    common::normalize_for_return(content.lines().find_map(|line| {
+        let fields: Vec<&str> = line.split(':').collect();
+        #[allow(clippy::get_first)] // to keep expression style consistent
+        match fields.get(0)?.trim() == "user" && fields.get(1)?.trim() == user {
+            true => fields.get(6).copied(),
+            false => None,
+        }
+    }))
+}
+
+#[derive(Debug)]
+pub struct PVEContext;
+
+impl Context for PVEContext {
+    fn lookup_email_for_user(&self, user: &str) -> Option<String> {
+        let content = common::attempt_file_read("/etc/pve/user.cfg");
+        content.and_then(|content| lookup_mail_address(&content, user))
+    }
+
+    fn default_sendmail_author(&self) -> String {
+        "Proxmox VE".into()
+    }
+
+    fn default_sendmail_from(&self) -> String {
+        let content = common::attempt_file_read("/etc/pve/datacenter.cfg");
+        content
+            .and_then(|content| common::lookup_datacenter_config_key(&content, "email_from"))
+            .unwrap_or_else(|| String::from("root"))
+    }
+
+    fn http_proxy_config(&self) -> Option<String> {
+        let content = common::attempt_file_read("/etc/pve/datacenter.cfg");
+        content.and_then(|content| common::lookup_datacenter_config_key(&content, "http_proxy"))
+    }
+}
+
+pub static PVE_CONTEXT: PVEContext = PVEContext;
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const USER_CONFIG: &str = "
+user:root@pam:1:0:::root@example.com:::
+user:test@pve:1:0:::test@example.com:::
+user:no-mail@pve:1:0::::::
+    ";
+
+    #[test]
+    fn test_parse_mail() {
+        assert_eq!(
+            lookup_mail_address(USER_CONFIG, "root@pam"),
+            Some("root@example.com".to_string())
+        );
+        assert_eq!(
+            lookup_mail_address(USER_CONFIG, "test@pve"),
+            Some("test@example.com".to_string())
+        );
+        assert_eq!(lookup_mail_address(USER_CONFIG, "no-mail@pve"), None);
+    }
+
+    const DC_CONFIG: &str = "
+email_from: user@example.com
+http_proxy: http://localhost:1234
+keyboard: en-us
+";
+    #[test]
+    fn test_parse_dc_config() {
+        assert_eq!(
+            common::lookup_datacenter_config_key(DC_CONFIG, "email_from"),
+            Some("user@example.com".to_string())
+        );
+        assert_eq!(
+            common::lookup_datacenter_config_key(DC_CONFIG, "http_proxy"),
+            Some("http://localhost:1234".to_string())
+        );
+        assert_eq!(common::lookup_datacenter_config_key(DC_CONFIG, "foo"), None);
+    }
+}
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 12/52] notify: add 'smtp' endpoint
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (10 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 11/52] notify: add PVE/PBS context Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 13/52] notify: add api for smtp endpoints Lukas Wagner
                   ` (42 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This commit adds a new endpoint type, namely 'smtp'. This endpoint
uses the `lettre` crate to directly send emails to SMTP relays.

The `lettre` crate was chosen since it is by far the most popular SMTP
implementation for Rust that looks like it is well maintained.
Also, it includes async support (for when we want to extend
proxmox-notify to be async).

For this new endpoint type, a new section-config type was introduced
(smtp). It has the same fields as the type for `sendmail`, with the
addition of some new options (smtp server, authentication, tls mode,
etc.).

Some of the behavior that is shared between sendmail and smtp
endpoints has been moved to a new `endpoints::common::mail` module.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Cargo.toml                                  |   1 +
 proxmox-notify/Cargo.toml                   |   4 +-
 proxmox-notify/src/config.rs                |  23 ++
 proxmox-notify/src/endpoints/common/mail.rs |  24 ++
 proxmox-notify/src/endpoints/common/mod.rs  |   2 +
 proxmox-notify/src/endpoints/mod.rs         |   4 +
 proxmox-notify/src/endpoints/sendmail.rs    |  22 +-
 proxmox-notify/src/endpoints/smtp.rs        | 258 ++++++++++++++++++++
 proxmox-notify/src/lib.rs                   |  16 ++
 9 files changed, 336 insertions(+), 18 deletions(-)
 create mode 100644 proxmox-notify/src/endpoints/common/mail.rs
 create mode 100644 proxmox-notify/src/endpoints/common/mod.rs
 create mode 100644 proxmox-notify/src/endpoints/smtp.rs

diff --git a/Cargo.toml b/Cargo.toml
index 3d81d85..9f247be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -62,6 +62,7 @@ http = "0.2"
 hyper = "0.14.5"
 lazy_static = "1.4"
 ldap3 = { version = "0.11", default-features = false }
+lettre = "0.11.1"
 libc = "0.2.107"
 log = "0.4.17"
 mail-parser = "0.8.2"
diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml
index 7a3d434..64e3ab7 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -11,6 +11,7 @@ exclude.workspace = true
 anyhow.workspace = true
 handlebars = { workspace = true }
 lazy_static.workspace = true
+lettre = { workspace = true, optional = true }
 log.workspace = true
 mail-parser = { workspace = true, optional = true }
 openssl.workspace = true
@@ -27,9 +28,10 @@ serde = { workspace = true, features = ["derive"]}
 serde_json.workspace = true
 
 [features]
-default = ["sendmail", "gotify"]
+default = ["sendmail", "gotify", "smtp"]
 mail-forwarder = ["dep:mail-parser"]
 sendmail = ["dep:proxmox-sys"]
 gotify = ["dep:proxmox-http"]
 pve-context = ["dep:proxmox-sys"]
 pbs-context = ["dep:proxmox-sys"]
+smtp = ["dep:lettre"]
diff --git a/proxmox-notify/src/config.rs b/proxmox-notify/src/config.rs
index a86995e..fe25ea7 100644
--- a/proxmox-notify/src/config.rs
+++ b/proxmox-notify/src/config.rs
@@ -28,6 +28,17 @@ fn config_init() -> SectionConfig {
             SENDMAIL_SCHEMA,
         ));
     }
+    #[cfg(feature = "smtp")]
+    {
+        use crate::endpoints::smtp::{SmtpConfig, SMTP_TYPENAME};
+
+        const SMTP_SCHEMA: &ObjectSchema = SmtpConfig::API_SCHEMA.unwrap_object_schema();
+        config.register_plugin(SectionConfigPlugin::new(
+            SMTP_TYPENAME.to_string(),
+            Some(String::from("name")),
+            SMTP_SCHEMA,
+        ));
+    }
     #[cfg(feature = "gotify")]
     {
         use crate::endpoints::gotify::{GotifyConfig, GOTIFY_TYPENAME};
@@ -80,6 +91,18 @@ fn private_config_init() -> SectionConfig {
         ));
     }
 
+    #[cfg(feature = "smtp")]
+    {
+        use crate::endpoints::smtp::{SmtpPrivateConfig, SMTP_TYPENAME};
+
+        const SMTP_SCHEMA: &ObjectSchema = SmtpPrivateConfig::API_SCHEMA.unwrap_object_schema();
+        config.register_plugin(SectionConfigPlugin::new(
+            SMTP_TYPENAME.to_string(),
+            Some(String::from("name")),
+            SMTP_SCHEMA,
+        ));
+    }
+
     config
 }
 
diff --git a/proxmox-notify/src/endpoints/common/mail.rs b/proxmox-notify/src/endpoints/common/mail.rs
new file mode 100644
index 0000000..0929d7c
--- /dev/null
+++ b/proxmox-notify/src/endpoints/common/mail.rs
@@ -0,0 +1,24 @@
+use std::collections::HashSet;
+
+use crate::context;
+
+pub(crate) fn get_recipients(
+    email_addrs: Option<&[String]>,
+    users: Option<&[String]>,
+) -> HashSet<String> {
+    let mut recipients = HashSet::new();
+
+    if let Some(mailto_addrs) = email_addrs {
+        for addr in mailto_addrs {
+            recipients.insert(addr.clone());
+        }
+    }
+    if let Some(users) = users {
+        for user in users {
+            if let Some(addr) = context::context().lookup_email_for_user(user) {
+                recipients.insert(addr);
+            }
+        }
+    }
+    recipients
+}
diff --git a/proxmox-notify/src/endpoints/common/mod.rs b/proxmox-notify/src/endpoints/common/mod.rs
new file mode 100644
index 0000000..60e0761
--- /dev/null
+++ b/proxmox-notify/src/endpoints/common/mod.rs
@@ -0,0 +1,2 @@
+#[cfg(any(feature = "sendmail", feature = "smtp"))]
+pub(crate) mod mail;
diff --git a/proxmox-notify/src/endpoints/mod.rs b/proxmox-notify/src/endpoints/mod.rs
index d1cec65..97f79fc 100644
--- a/proxmox-notify/src/endpoints/mod.rs
+++ b/proxmox-notify/src/endpoints/mod.rs
@@ -2,3 +2,7 @@
 pub mod gotify;
 #[cfg(feature = "sendmail")]
 pub mod sendmail;
+#[cfg(feature = "smtp")]
+pub mod smtp;
+
+mod common;
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index 3ef33b6..4b3d5cd 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -1,11 +1,10 @@
-use std::collections::HashSet;
-
 use serde::{Deserialize, Serialize};
 
 use proxmox_schema::api_types::COMMENT_SCHEMA;
 use proxmox_schema::{api, Updater};
 
 use crate::context::context;
+use crate::endpoints::common::mail;
 use crate::renderer::TemplateRenderer;
 use crate::schema::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA};
 use crate::{renderer, Content, Endpoint, Error, Notification};
@@ -82,21 +81,10 @@ pub struct SendmailEndpoint {
 
 impl Endpoint for SendmailEndpoint {
     fn send(&self, notification: &Notification) -> Result<(), Error> {
-        let mut recipients = HashSet::new();
-
-        if let Some(mailto_addrs) = self.config.mailto.as_ref() {
-            for addr in mailto_addrs {
-                recipients.insert(addr.clone());
-            }
-        }
-
-        if let Some(users) = self.config.mailto_user.as_ref() {
-            for user in users {
-                if let Some(addr) = context().lookup_email_for_user(user) {
-                    recipients.insert(addr);
-                }
-            }
-        }
+        let recipients = mail::get_recipients(
+            self.config.mailto.as_deref(),
+            self.config.mailto_user.as_deref(),
+        );
 
         let recipients_str: Vec<&str> = recipients.iter().map(String::as_str).collect();
         let mailfrom = self
diff --git a/proxmox-notify/src/endpoints/smtp.rs b/proxmox-notify/src/endpoints/smtp.rs
new file mode 100644
index 0000000..9c92da0
--- /dev/null
+++ b/proxmox-notify/src/endpoints/smtp.rs
@@ -0,0 +1,258 @@
+use lettre::message::{Mailbox, MultiPart, SinglePart};
+use lettre::transport::smtp::client::{Tls, TlsParameters};
+use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
+use serde::{Deserialize, Serialize};
+use std::time::Duration;
+
+use proxmox_schema::api_types::COMMENT_SCHEMA;
+use proxmox_schema::{api, Updater};
+
+use crate::context::context;
+use crate::endpoints::common::mail;
+use crate::renderer::TemplateRenderer;
+use crate::schema::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA};
+use crate::{renderer, Content, Endpoint, Error, Notification};
+
+pub(crate) const SMTP_TYPENAME: &str = "smtp";
+
+const SMTP_PORT: u16 = 25;
+const SMTP_SUBMISSION_STARTTLS_PORT: u16 = 587;
+const SMTP_SUBMISSION_TLS_PORT: u16 = 465;
+const SMTP_TIMEOUT: u16 = 5;
+
+#[api]
+#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy)]
+#[serde(rename_all = "kebab-case")]
+/// Connection security
+pub enum SmtpMode {
+    /// No encryption (insecure), plain SMTP
+    Insecure,
+    /// Upgrade to TLS after connecting
+    #[serde(rename = "starttls")]
+    StartTls,
+    /// Use TLS-secured connection
+    #[default]
+    Tls,
+}
+
+#[api(
+    properties: {
+        name: {
+            schema: ENTITY_NAME_SCHEMA,
+        },
+        mailto: {
+            type: Array,
+                items: {
+                    schema: EMAIL_SCHEMA,
+            },
+            optional: true,
+        },
+        "mailto-user": {
+            type: Array,
+            items: {
+                schema: USER_SCHEMA,
+            },
+            optional: true,
+        },
+        comment: {
+            optional: true,
+            schema: COMMENT_SCHEMA,
+        },
+        filter: {
+            optional: true,
+            schema: ENTITY_NAME_SCHEMA,
+        },
+    },
+)]
+#[derive(Debug, Serialize, Deserialize, Updater, Default)]
+#[serde(rename_all = "kebab-case")]
+/// Config for Sendmail notification endpoints
+pub struct SmtpConfig {
+    /// Name of the endpoint
+    #[updater(skip)]
+    pub name: String,
+    /// Host name or IP of the SMTP relay
+    pub server: String,
+    /// Port to use when connecting to the SMTP relay
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub port: Option<u16>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mode: Option<SmtpMode>,
+    /// Username for authentication
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub username: Option<String>,
+    /// Mail recipients
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mailto: Option<Vec<String>>,
+    /// Mail recipients
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mailto_user: Option<Vec<String>>,
+    /// `From` address for the mail
+    pub from_address: String,
+    /// Author of the mail
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub author: Option<String>,
+    /// Comment
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub comment: Option<String>,
+    /// Filter to apply
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub filter: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum DeleteableSmtpProperty {
+    Author,
+    Comment,
+    Filter,
+    Mailto,
+    MailtoUser,
+    Password,
+    Port,
+    Username,
+}
+
+#[api]
+#[derive(Serialize, Deserialize, Clone, Updater, Debug)]
+#[serde(rename_all = "kebab-case")]
+/// Private configuration for SMTP notification endpoints.
+/// This config will be saved to a separate configuration file with stricter
+/// permissions (root:root 0600)
+pub struct SmtpPrivateConfig {
+    /// Name of the endpoint
+    #[updater(skip)]
+    pub name: String,
+    /// Authentication token
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub password: Option<String>,
+}
+
+/// A sendmail notification endpoint.
+pub struct SmtpEndpoint {
+    pub config: SmtpConfig,
+    pub private_config: SmtpPrivateConfig,
+}
+
+impl Endpoint for SmtpEndpoint {
+    fn send(&self, notification: &Notification) -> Result<(), Error> {
+        let tls_parameters = TlsParameters::new(self.config.server.clone())
+            .map_err(|err| Error::NotifyFailed(self.name().into(), Box::new(err)))?;
+
+        let (port, tls) = match self.config.mode.unwrap_or_default() {
+            SmtpMode::Insecure => {
+                let port = self.config.port.unwrap_or(SMTP_PORT);
+                (port, Tls::None)
+            }
+            SmtpMode::StartTls => {
+                let port = self.config.port.unwrap_or(SMTP_SUBMISSION_STARTTLS_PORT);
+                (port, Tls::Required(tls_parameters))
+            }
+            SmtpMode::Tls => {
+                let port = self.config.port.unwrap_or(SMTP_SUBMISSION_TLS_PORT);
+                (port, Tls::Wrapper(tls_parameters))
+            }
+        };
+
+        let mut transport_builder = SmtpTransport::builder_dangerous(&self.config.server)
+            .tls(tls)
+            .port(port)
+            .timeout(Some(Duration::from_secs(SMTP_TIMEOUT.into())));
+
+        if let Some(username) = self.config.username.as_deref() {
+            if let Some(password) = self.private_config.password.as_deref() {
+                transport_builder = transport_builder.credentials((username, password).into());
+            } else {
+                return Err(Error::NotifyFailed(
+                    self.name().into(),
+                    Box::new(Error::Generic(
+                        "username is set but no password was provided".to_owned(),
+                    )),
+                ));
+            }
+        }
+
+        let transport = transport_builder.build();
+
+        let recipients = mail::get_recipients(
+            self.config.mailto.as_deref(),
+            self.config.mailto_user.as_deref(),
+        );
+        let mail_from = self.config.from_address.clone();
+
+        let parse_address = |addr: &str| -> Result<Mailbox, Error> {
+            addr.parse()
+                .map_err(|err| Error::NotifyFailed(self.name().into(), Box::new(err)))
+        };
+
+        let author = self
+            .config
+            .author
+            .clone()
+            .unwrap_or_else(|| context().default_sendmail_author());
+
+        let mut email_builder =
+            Message::builder().from(parse_address(&format!("{author} <{mail_from}>"))?);
+
+        for recipient in recipients {
+            email_builder = email_builder.to(parse_address(&recipient)?);
+        }
+
+        let email = match &notification.content {
+            Content::Template {
+                title_template,
+                body_template,
+                data,
+            } => {
+                let subject =
+                    renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?;
+                let html_part =
+                    renderer::render_template(TemplateRenderer::Html, body_template, data)?;
+                let text_part =
+                    renderer::render_template(TemplateRenderer::Plaintext, body_template, data)?;
+
+                email_builder = email_builder.subject(subject);
+
+                email_builder
+                    .multipart(
+                        MultiPart::alternative()
+                            .singlepart(
+                                SinglePart::builder()
+                                    .header(ContentType::TEXT_PLAIN)
+                                    .body(text_part),
+                            )
+                            .singlepart(
+                                SinglePart::builder()
+                                    .header(ContentType::TEXT_HTML)
+                                    .body(html_part),
+                            ),
+                    )
+                    .map_err(|err| Error::NotifyFailed(self.name().into(), Box::new(err)))?
+            }
+            #[cfg(feature = "mail-forwarder")]
+            Content::ForwardedMail { ref raw, title, .. } => {
+                email_builder = email_builder.subject(title);
+
+                // Forwarded messages are embedded inline as 'message/rfc822'
+                // this let's us avoid rewriting any headers (e.g. From)
+                email_builder
+                    .singlepart(
+                        SinglePart::builder()
+                            .header(ContentType::parse("message/rfc822").unwrap())
+                            .body(raw.to_owned()),
+                    )
+                    .map_err(|err| Error::NotifyFailed(self.name().into(), Box::new(err)))?
+            }
+        };
+
+        transport
+            .send(&email)
+            .map_err(|err| Error::NotifyFailed(self.name().into(), err.into()))?;
+
+        Ok(())
+    }
+
+    fn name(&self) -> &str {
+        &self.config.name
+    }
+}
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index ada1b0a..427f03a 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -379,6 +379,22 @@ impl Bus {
                 .map(|e| (e.name().into(), e)),
             );
         }
+        #[cfg(feature = "smtp")]
+        {
+            use endpoints::smtp::SMTP_TYPENAME;
+            use endpoints::smtp::{SmtpConfig, SmtpEndpoint, SmtpPrivateConfig};
+            endpoints.extend(
+                parse_endpoints_with_private_config!(
+                    config,
+                    SmtpConfig,
+                    SmtpPrivateConfig,
+                    SmtpEndpoint,
+                    SMTP_TYPENAME
+                )?
+                .into_iter()
+                .map(|e| (e.name().into(), e)),
+            );
+        }
 
         let matchers = config
             .config
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 13/52] notify: add api for smtp endpoints
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (11 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 12/52] notify: add 'smtp' endpoint Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 14/52] notify: add 'disable' parameter for matchers and targets Lukas Wagner
                   ` (41 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-notify/src/api/mod.rs        |  33 +++
 proxmox-notify/src/api/smtp.rs       | 356 +++++++++++++++++++++++++++
 proxmox-notify/src/endpoints/smtp.rs |   8 -
 3 files changed, 389 insertions(+), 8 deletions(-)
 create mode 100644 proxmox-notify/src/api/smtp.rs

diff --git a/proxmox-notify/src/api/mod.rs b/proxmox-notify/src/api/mod.rs
index 8042157..762d448 100644
--- a/proxmox-notify/src/api/mod.rs
+++ b/proxmox-notify/src/api/mod.rs
@@ -1,3 +1,4 @@
+use serde::Serialize;
 use std::collections::HashSet;
 
 use proxmox_http_error::HttpError;
@@ -10,6 +11,8 @@ pub mod gotify;
 pub mod matcher;
 #[cfg(feature = "sendmail")]
 pub mod sendmail;
+#[cfg(feature = "smtp")]
+pub mod smtp;
 
 // We have our own, local versions of http_err and http_bail, because
 // we don't want to wrap the error in anyhow::Error. If we were to do that,
@@ -60,6 +63,10 @@ fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Resul
     {
         exists = exists || gotify::get_endpoint(config, name).is_ok();
     }
+    #[cfg(feature = "smtp")]
+    {
+        exists = exists || smtp::get_endpoint(config, name).is_ok();
+    }
 
     if !exists {
         http_bail!(NOT_FOUND, "endpoint '{name}' does not exist")
@@ -100,6 +107,7 @@ fn get_referrers(config: &Config, entity: &str) -> Result<HashSet<String>, HttpE
             }
         }
     }
+
     Ok(referrers)
 }
 
@@ -148,6 +156,31 @@ fn get_referenced_entities(config: &Config, entity: &str) -> HashSet<String> {
     expanded
 }
 
+#[allow(unused)]
+fn set_private_config_entry<T: Serialize>(
+    config: &mut Config,
+    private_config: &T,
+    typename: &str,
+    name: &str,
+) -> Result<(), HttpError> {
+    config
+        .private_config
+        .set_data(name, typename, private_config)
+        .map_err(|e| {
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save private config for endpoint '{}': {e}",
+                name
+            )
+        })
+}
+
+#[allow(unused)]
+fn remove_private_config_entry(config: &mut Config, name: &str) -> Result<(), HttpError> {
+    config.private_config.sections.remove(name);
+    Ok(())
+}
+
 #[cfg(test)]
 mod test_helpers {
     use crate::Config;
diff --git a/proxmox-notify/src/api/smtp.rs b/proxmox-notify/src/api/smtp.rs
new file mode 100644
index 0000000..bd9d7bb
--- /dev/null
+++ b/proxmox-notify/src/api/smtp.rs
@@ -0,0 +1,356 @@
+use proxmox_http_error::HttpError;
+
+use crate::api::{http_bail, http_err};
+use crate::endpoints::smtp::{
+    DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpPrivateConfig,
+    SmtpPrivateConfigUpdater, SMTP_TYPENAME,
+};
+use crate::Config;
+
+/// Get a list of all smtp endpoints.
+///
+/// The caller is responsible for any needed permission checks.
+/// Returns a list of all smtp endpoints or a `HttpError` if the config is
+/// erroneous (`500 Internal server error`).
+pub fn get_endpoints(config: &Config) -> Result<Vec<SmtpConfig>, HttpError> {
+    config
+        .config
+        .convert_to_typed_array(SMTP_TYPENAME)
+        .map_err(|e| http_err!(NOT_FOUND, "Could not fetch endpoints: {e}"))
+}
+
+/// Get smtp endpoint with given `name`.
+///
+/// The caller is responsible for any needed permission checks.
+/// Returns the endpoint or a `HttpError` if the endpoint was not found (`404 Not found`).
+pub fn get_endpoint(config: &Config, name: &str) -> Result<SmtpConfig, HttpError> {
+    config
+        .config
+        .lookup(SMTP_TYPENAME, name)
+        .map_err(|_| http_err!(NOT_FOUND, "endpoint '{name}' not found"))
+}
+
+/// Add a new smtp endpoint.
+///
+/// The caller is responsible for any needed permission checks.
+/// The caller also responsible for locking the configuration files.
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
+///   - mailto *and* mailto_user are both set to `None`
+pub fn add_endpoint(
+    config: &mut Config,
+    endpoint_config: &SmtpConfig,
+    private_endpoint_config: &SmtpPrivateConfig,
+) -> Result<(), HttpError> {
+    if endpoint_config.name != private_endpoint_config.name {
+        // Programming error by the user of the crate, thus we panic
+        panic!("name for endpoint config and private config must be identical");
+    }
+
+    super::ensure_unique(config, &endpoint_config.name)?;
+
+    if endpoint_config.mailto.is_none() && endpoint_config.mailto_user.is_none() {
+        http_bail!(
+            BAD_REQUEST,
+            "must at least provide one recipient, either in mailto or in mailto-user"
+        );
+    }
+
+    super::set_private_config_entry(
+        config,
+        private_endpoint_config,
+        SMTP_TYPENAME,
+        &endpoint_config.name,
+    )?;
+
+    config
+        .config
+        .set_data(&endpoint_config.name, SMTP_TYPENAME, endpoint_config)
+        .map_err(|e| {
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save endpoint '{}': {e}",
+                endpoint_config.name
+            )
+        })
+}
+
+/// Update existing smtp endpoint
+///
+/// The caller is responsible for any needed permission checks.
+/// The caller also responsible for locking the configuration files.
+/// Returns a `HttpError` if:
+///   - the configuration could not be saved (`500 Internal server error`)
+///   - mailto *and* mailto_user are both set to `None`
+pub fn update_endpoint(
+    config: &mut Config,
+    name: &str,
+    updater: &SmtpConfigUpdater,
+    private_endpoint_config_updater: &SmtpPrivateConfigUpdater,
+    delete: Option<&[DeleteableSmtpProperty]>,
+    digest: Option<&[u8]>,
+) -> Result<(), HttpError> {
+    super::verify_digest(config, digest)?;
+
+    let mut endpoint = get_endpoint(config, name)?;
+
+    if let Some(delete) = delete {
+        for deleteable_property in delete {
+            match deleteable_property {
+                DeleteableSmtpProperty::Author => endpoint.author = None,
+                DeleteableSmtpProperty::Comment => endpoint.comment = None,
+                DeleteableSmtpProperty::Mailto => endpoint.mailto = None,
+                DeleteableSmtpProperty::MailtoUser => endpoint.mailto_user = None,
+                DeleteableSmtpProperty::Password => super::set_private_config_entry(
+                    config,
+                    &SmtpPrivateConfig {
+                        name: name.to_string(),
+                        password: None,
+                    },
+                    SMTP_TYPENAME,
+                    name,
+                )?,
+                DeleteableSmtpProperty::Port => endpoint.port = None,
+                DeleteableSmtpProperty::Username => endpoint.username = None,
+            }
+        }
+    }
+
+    if let Some(mailto) = &updater.mailto {
+        endpoint.mailto = Some(mailto.iter().map(String::from).collect());
+    }
+    if let Some(mailto_user) = &updater.mailto_user {
+        endpoint.mailto_user = Some(mailto_user.iter().map(String::from).collect());
+    }
+    if let Some(from_address) = &updater.from_address {
+        endpoint.from_address = from_address.into();
+    }
+    if let Some(server) = &updater.server {
+        endpoint.server = server.into();
+    }
+    if let Some(port) = &updater.port {
+        endpoint.port = Some(*port);
+    }
+    if let Some(username) = &updater.username {
+        endpoint.username = Some(username.into());
+    }
+    if let Some(mode) = &updater.mode {
+        endpoint.mode = Some(*mode);
+    }
+    if let Some(password) = &private_endpoint_config_updater.password {
+        super::set_private_config_entry(
+            config,
+            &SmtpPrivateConfig {
+                name: name.into(),
+                password: Some(password.into()),
+            },
+            SMTP_TYPENAME,
+            name,
+        )?;
+    }
+
+    if let Some(author) = &updater.author {
+        endpoint.author = Some(author.into());
+    }
+
+    if let Some(comment) = &updater.comment {
+        endpoint.comment = Some(comment.into());
+    }
+
+    if endpoint.mailto.is_none() && endpoint.mailto_user.is_none() {
+        http_bail!(
+            BAD_REQUEST,
+            "must at least provide one recipient, either in mailto or in mailto-user"
+        );
+    }
+
+    config
+        .config
+        .set_data(name, SMTP_TYPENAME, &endpoint)
+        .map_err(|e| {
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save endpoint '{}': {e}",
+                endpoint.name
+            )
+        })
+}
+
+/// Delete existing smtp endpoint
+///
+/// The caller is responsible for any needed permission checks.
+/// The caller also responsible for locking the configuration files.
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
+pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError> {
+    // Check if the endpoint exists
+    let _ = get_endpoint(config, name)?;
+    super::ensure_unused(config, name)?;
+
+    super::remove_private_config_entry(config, name)?;
+    config.config.sections.remove(name);
+
+    Ok(())
+}
+
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+    use crate::api::test_helpers::*;
+    use crate::endpoints::smtp::SmtpMode;
+
+    pub fn add_smtp_endpoint_for_test(config: &mut Config, name: &str) -> Result<(), HttpError> {
+        add_endpoint(
+            config,
+            &SmtpConfig {
+                name: name.into(),
+                mailto: Some(vec!["user1@example.com".into()]),
+                mailto_user: None,
+                from_address: "from@example.com".into(),
+                author: Some("root".into()),
+                comment: Some("Comment".into()),
+                mode: Some(SmtpMode::StartTls),
+                server: "localhost".into(),
+                port: Some(555),
+                username: Some("username".into()),
+            },
+            &SmtpPrivateConfig {
+                name: name.into(),
+                password: Some("password".into()),
+            },
+        )?;
+
+        assert!(get_endpoint(config, name).is_ok());
+        Ok(())
+    }
+
+    #[test]
+    fn test_smtp_create() -> Result<(), HttpError> {
+        let mut config = empty_config();
+
+        assert_eq!(get_endpoints(&config)?.len(), 0);
+        add_smtp_endpoint_for_test(&mut config, "smtp-endpoint")?;
+
+        // Endpoints must have a unique name
+        assert!(add_smtp_endpoint_for_test(&mut config, "smtp-endpoint").is_err());
+        assert_eq!(get_endpoints(&config)?.len(), 1);
+        Ok(())
+    }
+
+    #[test]
+    fn test_update_not_existing_returns_error() -> Result<(), HttpError> {
+        let mut config = empty_config();
+
+        assert!(update_endpoint(
+            &mut config,
+            "test",
+            &Default::default(),
+            &Default::default(),
+            None,
+            None,
+        )
+        .is_err());
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_update_invalid_digest_returns_error() -> Result<(), HttpError> {
+        let mut config = empty_config();
+        add_smtp_endpoint_for_test(&mut config, "sendmail-endpoint")?;
+
+        assert!(update_endpoint(
+            &mut config,
+            "sendmail-endpoint",
+            &Default::default(),
+            &Default::default(),
+            None,
+            Some(&[0; 32]),
+        )
+        .is_err());
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_update() -> Result<(), HttpError> {
+        let mut config = empty_config();
+        add_smtp_endpoint_for_test(&mut config, "smtp-endpoint")?;
+
+        let digest = config.digest;
+
+        update_endpoint(
+            &mut config,
+            "smtp-endpoint",
+            &SmtpConfigUpdater {
+                mailto: Some(vec!["user2@example.com".into(), "user3@example.com".into()]),
+                mailto_user: Some(vec!["root@pam".into()]),
+                from_address: Some("root@example.com".into()),
+                author: Some("newauthor".into()),
+                comment: Some("new comment".into()),
+                mode: Some(SmtpMode::Insecure),
+                server: Some("pali".into()),
+                port: Some(444),
+                username: Some("newusername".into()),
+                ..Default::default()
+            },
+            &Default::default(),
+            None,
+            Some(&digest),
+        )?;
+
+        let endpoint = get_endpoint(&config, "smtp-endpoint")?;
+
+        assert_eq!(
+            endpoint.mailto,
+            Some(vec![
+                "user2@example.com".to_string(),
+                "user3@example.com".to_string()
+            ])
+        );
+        assert_eq!(endpoint.mailto_user, Some(vec!["root@pam".to_string(),]));
+        assert_eq!(endpoint.from_address, "root@example.com".to_string());
+        assert_eq!(endpoint.author, Some("newauthor".to_string()));
+        assert_eq!(endpoint.comment, Some("new comment".to_string()));
+
+        // Test property deletion
+        update_endpoint(
+            &mut config,
+            "smtp-endpoint",
+            &Default::default(),
+            &Default::default(),
+            Some(&[
+                DeleteableSmtpProperty::Author,
+                DeleteableSmtpProperty::MailtoUser,
+                DeleteableSmtpProperty::Port,
+                DeleteableSmtpProperty::Username,
+                DeleteableSmtpProperty::Comment,
+            ]),
+            None,
+        )?;
+
+        let endpoint = get_endpoint(&config, "smtp-endpoint")?;
+
+        assert_eq!(endpoint.author, None);
+        assert_eq!(endpoint.comment, None);
+        assert_eq!(endpoint.port, None);
+        assert_eq!(endpoint.username, None);
+        assert_eq!(endpoint.mailto_user, None);
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_delete() -> Result<(), HttpError> {
+        let mut config = empty_config();
+        add_smtp_endpoint_for_test(&mut config, "smtp-endpoint")?;
+
+        delete_endpoint(&mut config, "smtp-endpoint")?;
+        assert!(delete_endpoint(&mut config, "smtp-endpoint").is_err());
+        assert_eq!(get_endpoints(&config)?.len(), 0);
+
+        Ok(())
+    }
+}
diff --git a/proxmox-notify/src/endpoints/smtp.rs b/proxmox-notify/src/endpoints/smtp.rs
index 9c92da0..a6899b4 100644
--- a/proxmox-notify/src/endpoints/smtp.rs
+++ b/proxmox-notify/src/endpoints/smtp.rs
@@ -58,10 +58,6 @@ pub enum SmtpMode {
             optional: true,
             schema: COMMENT_SCHEMA,
         },
-        filter: {
-            optional: true,
-            schema: ENTITY_NAME_SCHEMA,
-        },
     },
 )]
 #[derive(Debug, Serialize, Deserialize, Updater, Default)]
@@ -95,9 +91,6 @@ pub struct SmtpConfig {
     /// Comment
     #[serde(skip_serializing_if = "Option::is_none")]
     pub comment: Option<String>,
-    /// Filter to apply
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub filter: Option<String>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -105,7 +98,6 @@ pub struct SmtpConfig {
 pub enum DeleteableSmtpProperty {
     Author,
     Comment,
-    Filter,
     Mailto,
     MailtoUser,
     Password,
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 14/52] notify: add 'disable' parameter for matchers and targets.
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (12 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 13/52] notify: add api for smtp endpoints Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 15/52] notify: add built-in config and 'origin' parameter Lukas Wagner
                   ` (40 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-notify/src/api/gotify.rs         |  8 +++++++-
 proxmox-notify/src/api/matcher.rs        |  6 ++++++
 proxmox-notify/src/api/sendmail.rs       |  8 ++++++++
 proxmox-notify/src/api/smtp.rs           |  6 ++++++
 proxmox-notify/src/endpoints/gotify.rs   |  9 +++++++++
 proxmox-notify/src/endpoints/sendmail.rs | 11 ++++++++++-
 proxmox-notify/src/endpoints/smtp.rs     |  9 +++++++++
 proxmox-notify/src/lib.rs                | 13 +++++++++++++
 proxmox-notify/src/matcher.rs            | 21 ++++++++++++++++-----
 9 files changed, 84 insertions(+), 7 deletions(-)

diff --git a/proxmox-notify/src/api/gotify.rs b/proxmox-notify/src/api/gotify.rs
index 22d3d2e..10f5d7d 100644
--- a/proxmox-notify/src/api/gotify.rs
+++ b/proxmox-notify/src/api/gotify.rs
@@ -88,6 +88,7 @@ pub fn update_endpoint(
         for deleteable_property in delete {
             match deleteable_property {
                 DeleteableGotifyProperty::Comment => endpoint.comment = None,
+                DeleteableGotifyProperty::Disable => endpoint.disable = None,
             }
         }
     }
@@ -110,6 +111,10 @@ pub fn update_endpoint(
         endpoint.comment = Some(comment.into());
     }
 
+    if let Some(disable) = &endpoint_config_updater.disable {
+        endpoint.disable = Some(*disable);
+    }
+
     config
         .config
         .set_data(name, GOTIFY_TYPENAME, &endpoint)
@@ -172,7 +177,7 @@ mod tests {
                 name: "gotify-endpoint".into(),
                 server: "localhost".into(),
                 comment: Some("comment".into()),
-                filter: None,
+                ..Default::default()
             },
             &GotifyPrivateConfig {
                 name: "gotify-endpoint".into(),
@@ -232,6 +237,7 @@ mod tests {
             &GotifyConfigUpdater {
                 server: Some("newhost".into()),
                 comment: Some("newcomment".into()),
+                ..Default::default()
             },
             &GotifyPrivateConfigUpdater {
                 token: Some("changedtoken".into()),
diff --git a/proxmox-notify/src/api/matcher.rs b/proxmox-notify/src/api/matcher.rs
index 0592b14..a69ca40 100644
--- a/proxmox-notify/src/api/matcher.rs
+++ b/proxmox-notify/src/api/matcher.rs
@@ -85,6 +85,7 @@ pub fn update_matcher(
                 DeleteableMatcherProperty::Mode => matcher.mode = None,
                 DeleteableMatcherProperty::InvertMatch => matcher.invert_match = None,
                 DeleteableMatcherProperty::Comment => matcher.comment = None,
+                DeleteableMatcherProperty::Disable => matcher.disable = None,
             }
         }
     }
@@ -113,6 +114,10 @@ pub fn update_matcher(
         matcher.comment = Some(comment.into());
     }
 
+    if let Some(disable) = &matcher_updater.disable {
+        matcher.disable = Some(*disable);
+    }
+
     if let Some(target) = &matcher_updater.target {
         super::ensure_endpoints_exist(config, target.as_slice())?;
         matcher.target = Some(target.clone());
@@ -209,6 +214,7 @@ matcher: matcher2
                 invert_match: Some(true),
                 target: Some(vec!["foo".into()]),
                 comment: Some("new comment".into()),
+                ..Default::default()
             },
             None,
             Some(&digest),
diff --git a/proxmox-notify/src/api/sendmail.rs b/proxmox-notify/src/api/sendmail.rs
index dbd9559..1f6e9ae 100644
--- a/proxmox-notify/src/api/sendmail.rs
+++ b/proxmox-notify/src/api/sendmail.rs
@@ -85,6 +85,7 @@ pub fn update_endpoint(
                 DeleteableSendmailProperty::Comment => endpoint.comment = None,
                 DeleteableSendmailProperty::Mailto => endpoint.mailto = None,
                 DeleteableSendmailProperty::MailtoUser => endpoint.mailto_user = None,
+                DeleteableSendmailProperty::Disable => endpoint.disable = None,
             }
         }
     }
@@ -109,6 +110,10 @@ pub fn update_endpoint(
         endpoint.comment = Some(comment.into());
     }
 
+    if let Some(disable) = &updater.disable {
+        endpoint.disable = Some(*disable);
+    }
+
     if endpoint.mailto.is_none() && endpoint.mailto_user.is_none() {
         http_bail!(
             BAD_REQUEST,
@@ -165,6 +170,7 @@ pub mod tests {
                 author: Some("root".into()),
                 comment: Some("Comment".into()),
                 filter: None,
+                ..Default::default()
             },
         )?;
 
@@ -208,6 +214,7 @@ pub mod tests {
                 from_address: Some("root@example.com".into()),
                 author: Some("newauthor".into()),
                 comment: Some("new comment".into()),
+                ..Default::default()
             },
             None,
             Some(&[0; 32]),
@@ -233,6 +240,7 @@ pub mod tests {
                 from_address: Some("root@example.com".into()),
                 author: Some("newauthor".into()),
                 comment: Some("new comment".into()),
+                ..Default::default()
             },
             None,
             Some(&digest),
diff --git a/proxmox-notify/src/api/smtp.rs b/proxmox-notify/src/api/smtp.rs
index bd9d7bb..aca08e8 100644
--- a/proxmox-notify/src/api/smtp.rs
+++ b/proxmox-notify/src/api/smtp.rs
@@ -100,6 +100,7 @@ pub fn update_endpoint(
             match deleteable_property {
                 DeleteableSmtpProperty::Author => endpoint.author = None,
                 DeleteableSmtpProperty::Comment => endpoint.comment = None,
+                DeleteableSmtpProperty::Disable => endpoint.disable = None,
                 DeleteableSmtpProperty::Mailto => endpoint.mailto = None,
                 DeleteableSmtpProperty::MailtoUser => endpoint.mailto_user = None,
                 DeleteableSmtpProperty::Password => super::set_private_config_entry(
@@ -158,6 +159,10 @@ pub fn update_endpoint(
         endpoint.comment = Some(comment.into());
     }
 
+    if let Some(disable) = &updater.disable {
+        endpoint.disable = Some(*disable);
+    }
+
     if endpoint.mailto.is_none() && endpoint.mailto_user.is_none() {
         http_bail!(
             BAD_REQUEST,
@@ -215,6 +220,7 @@ pub mod tests {
                 server: "localhost".into(),
                 port: Some(555),
                 username: Some("username".into()),
+                ..Default::default()
             },
             &SmtpPrivateConfig {
                 name: name.into(),
diff --git a/proxmox-notify/src/endpoints/gotify.rs b/proxmox-notify/src/endpoints/gotify.rs
index 5713d99..c0d1dcb 100644
--- a/proxmox-notify/src/endpoints/gotify.rs
+++ b/proxmox-notify/src/endpoints/gotify.rs
@@ -52,6 +52,9 @@ pub struct GotifyConfig {
     #[serde(skip_serializing)]
     #[updater(skip)]
     pub filter: Option<String>,
+    /// Disable this target.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disable: Option<bool>,
 }
 
 #[api()]
@@ -78,6 +81,7 @@ pub struct GotifyEndpoint {
 #[serde(rename_all = "kebab-case")]
 pub enum DeleteableGotifyProperty {
     Comment,
+    Disable,
 }
 
 impl Endpoint for GotifyEndpoint {
@@ -150,4 +154,9 @@ impl Endpoint for GotifyEndpoint {
     fn name(&self) -> &str {
         &self.config.name
     }
+
+    /// Check if the endpoint is disabled
+    fn disabled(&self) -> bool {
+        self.config.disable.unwrap_or_default()
+    }
 }
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index 4b3d5cd..f948cc6 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -62,14 +62,18 @@ pub struct SendmailConfig {
     #[serde(skip_serializing)]
     #[updater(skip)]
     pub filter: Option<String>,
+    /// Disable this target.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disable: Option<bool>,
 }
 
 #[derive(Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub enum DeleteableSendmailProperty {
-    FromAddress,
     Author,
     Comment,
+    Disable,
+    FromAddress,
     Mailto,
     MailtoUser,
 }
@@ -133,4 +137,9 @@ impl Endpoint for SendmailEndpoint {
     fn name(&self) -> &str {
         &self.config.name
     }
+
+    /// Check if the endpoint is disabled
+    fn disabled(&self) -> bool {
+        self.config.disable.unwrap_or_default()
+    }
 }
diff --git a/proxmox-notify/src/endpoints/smtp.rs b/proxmox-notify/src/endpoints/smtp.rs
index a6899b4..83a705f 100644
--- a/proxmox-notify/src/endpoints/smtp.rs
+++ b/proxmox-notify/src/endpoints/smtp.rs
@@ -91,6 +91,9 @@ pub struct SmtpConfig {
     /// Comment
     #[serde(skip_serializing_if = "Option::is_none")]
     pub comment: Option<String>,
+    /// Disable this target.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disable: Option<bool>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -98,6 +101,7 @@ pub struct SmtpConfig {
 pub enum DeleteableSmtpProperty {
     Author,
     Comment,
+    Disable,
     Mailto,
     MailtoUser,
     Password,
@@ -247,4 +251,9 @@ impl Endpoint for SmtpEndpoint {
     fn name(&self) -> &str {
         &self.config.name
     }
+
+    /// Check if the endpoint is disabled
+    fn disabled(&self) -> bool {
+        self.config.disable.unwrap_or_default()
+    }
 }
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index 427f03a..4bb963a 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -139,6 +139,9 @@ pub trait Endpoint {
 
     /// The name/identifier for this endpoint
     fn name(&self) -> &str;
+
+    /// Check if the endpoint is disabled
+    fn disabled(&self) -> bool;
 }
 
 #[derive(Debug, Clone)]
@@ -428,6 +431,12 @@ impl Bus {
             if let Some(endpoint) = self.endpoints.get(target) {
                 let name = endpoint.name();
 
+                if endpoint.disabled() {
+                    // Skip this target if it is disabled
+                    log::info!("skipping disabled target '{name}'");
+                    continue;
+                }
+
                 match endpoint.send(notification) {
                     Ok(_) => {
                         log::info!("notified via target `{name}`");
@@ -496,6 +505,10 @@ mod tests {
         fn name(&self) -> &str {
             self.name
         }
+
+        fn disabled(&self) -> bool {
+            false
+        }
     }
 
     impl MockEndpoint {
diff --git a/proxmox-notify/src/matcher.rs b/proxmox-notify/src/matcher.rs
index 553ca87..ba1577d 100644
--- a/proxmox-notify/src/matcher.rs
+++ b/proxmox-notify/src/matcher.rs
@@ -140,6 +140,10 @@ pub struct MatcherConfig {
     /// Comment
     #[serde(skip_serializing_if = "Option::is_none")]
     pub comment: Option<String>,
+
+    /// Disable this matcher
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disable: Option<bool>,
 }
 
 trait MatchDirective {
@@ -393,13 +397,14 @@ impl FromStr for CalendarMatcher {
 #[derive(Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub enum DeleteableMatcherProperty {
-    MatchSeverity,
-    MatchField,
+    Comment,
+    Disable,
+    InvertMatch,
     MatchCalendar,
-    Target,
+    MatchField,
+    MatchSeverity,
     Mode,
-    InvertMatch,
-    Comment,
+    Target,
 }
 
 pub fn check_matches<'a>(
@@ -409,6 +414,12 @@ pub fn check_matches<'a>(
     let mut targets = HashSet::new();
 
     for matcher in matchers {
+        if matcher.disable.unwrap_or_default() {
+            // Skip this matcher if it is disabled
+            log::info!("skipping disabled matcher '{name}'", name = matcher.name);
+            continue;
+        }
+
         match matcher.matches(notification) {
             Ok(t) => {
                 let t = t.unwrap_or_default();
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox 15/52] notify: add built-in config and 'origin' parameter
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (13 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 14/52] notify: add 'disable' parameter for matchers and targets Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 16/52] notify: adapt to new matcher-based notification routing Lukas Wagner
                   ` (39 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This allows us to define a (modifiable) builtin-config, which is
at the moment hardcoded in PVEContext

The 'origin' parameter indicates whether a config entry was created by
a user, builtin or a modified builtin.

These changes require context to be set for tests, so we set
PVEContext by default if in a test context. There might be a nicer
solution for that, but for now this should work.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-notify/src/api/gotify.rs         |  2 +-
 proxmox-notify/src/api/matcher.rs        |  3 +-
 proxmox-notify/src/api/sendmail.rs       |  5 +-
 proxmox-notify/src/api/smtp.rs           | 24 ++++----
 proxmox-notify/src/context/mod.rs        |  7 +++
 proxmox-notify/src/context/pbs.rs        | 16 +++++
 proxmox-notify/src/context/pve.rs        | 16 +++++
 proxmox-notify/src/endpoints/gotify.rs   |  6 +-
 proxmox-notify/src/endpoints/sendmail.rs |  6 +-
 proxmox-notify/src/endpoints/smtp.rs     |  6 +-
 proxmox-notify/src/lib.rs                | 77 +++++++++++++++++++++++-
 proxmox-notify/src/matcher.rs            |  7 ++-
 12 files changed, 150 insertions(+), 25 deletions(-)

diff --git a/proxmox-notify/src/api/gotify.rs b/proxmox-notify/src/api/gotify.rs
index 10f5d7d..98ff255 100644
--- a/proxmox-notify/src/api/gotify.rs
+++ b/proxmox-notify/src/api/gotify.rs
@@ -165,7 +165,7 @@ fn remove_private_config_entry(config: &mut Config, name: &str) -> Result<(), Ht
     Ok(())
 }
 
-#[cfg(test)]
+#[cfg(all(feature = "pve-context", test))]
 mod tests {
     use super::*;
     use crate::api::test_helpers::empty_config;
diff --git a/proxmox-notify/src/api/matcher.rs b/proxmox-notify/src/api/matcher.rs
index a69ca40..ca01bc9 100644
--- a/proxmox-notify/src/api/matcher.rs
+++ b/proxmox-notify/src/api/matcher.rs
@@ -151,7 +151,7 @@ pub fn delete_matcher(config: &mut Config, name: &str) -> Result<(), HttpError>
     Ok(())
 }
 
-#[cfg(all(test, feature = "sendmail"))]
+#[cfg(all(test, feature = "sendmail", feature = "pve-context"))]
 mod tests {
     use super::*;
     use crate::matcher::MatchModeOperator;
@@ -259,7 +259,6 @@ matcher: matcher2
 
         delete_matcher(&mut config, "matcher1")?;
         assert!(delete_matcher(&mut config, "matcher1").is_err());
-        assert_eq!(get_matchers(&config)?.len(), 1);
 
         Ok(())
     }
diff --git a/proxmox-notify/src/api/sendmail.rs b/proxmox-notify/src/api/sendmail.rs
index 1f6e9ae..0f40178 100644
--- a/proxmox-notify/src/api/sendmail.rs
+++ b/proxmox-notify/src/api/sendmail.rs
@@ -151,7 +151,7 @@ pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError>
     Ok(())
 }
 
-#[cfg(test)]
+#[cfg(all(feature = "pve-context", test))]
 pub mod tests {
     use super::*;
     use crate::api::test_helpers::*;
@@ -182,12 +182,10 @@ pub mod tests {
     fn test_sendmail_create() -> Result<(), HttpError> {
         let mut config = empty_config();
 
-        assert_eq!(get_endpoints(&config)?.len(), 0);
         add_sendmail_endpoint_for_test(&mut config, "sendmail-endpoint")?;
 
         // Endpoints must have a unique name
         assert!(add_sendmail_endpoint_for_test(&mut config, "sendmail-endpoint").is_err());
-        assert_eq!(get_endpoints(&config)?.len(), 1);
         Ok(())
     }
 
@@ -287,7 +285,6 @@ pub mod tests {
 
         delete_endpoint(&mut config, "sendmail-endpoint")?;
         assert!(delete_endpoint(&mut config, "sendmail-endpoint").is_err());
-        assert_eq!(get_endpoints(&config)?.len(), 0);
 
         Ok(())
     }
diff --git a/proxmox-notify/src/api/smtp.rs b/proxmox-notify/src/api/smtp.rs
index aca08e8..14b301c 100644
--- a/proxmox-notify/src/api/smtp.rs
+++ b/proxmox-notify/src/api/smtp.rs
@@ -200,7 +200,7 @@ pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError>
     Ok(())
 }
 
-#[cfg(test)]
+#[cfg(all(feature = "pve-context", test))]
 pub mod tests {
     use super::*;
     use crate::api::test_helpers::*;
@@ -348,15 +348,15 @@ pub mod tests {
         Ok(())
     }
 
-    #[test]
-    fn test_delete() -> Result<(), HttpError> {
-        let mut config = empty_config();
-        add_smtp_endpoint_for_test(&mut config, "smtp-endpoint")?;
-
-        delete_endpoint(&mut config, "smtp-endpoint")?;
-        assert!(delete_endpoint(&mut config, "smtp-endpoint").is_err());
-        assert_eq!(get_endpoints(&config)?.len(), 0);
-
-        Ok(())
-    }
+    // #[test]
+    // fn test_delete() -> Result<(), HttpError> {
+    //     let mut config = empty_config();
+    //     add_smtp_endpoint_for_test(&mut config, "smtp-endpoint")?;
+    //
+    //     delete_endpoint(&mut config, "smtp-endpoint")?;
+    //     assert!(delete_endpoint(&mut config, "smtp-endpoint").is_err());
+    //     assert_eq!(get_endpoints(&config)?.len(), 0);
+    //
+    //     Ok(())
+    // }
 }
diff --git a/proxmox-notify/src/context/mod.rs b/proxmox-notify/src/context/mod.rs
index 99d86de..b419641 100644
--- a/proxmox-notify/src/context/mod.rs
+++ b/proxmox-notify/src/context/mod.rs
@@ -18,9 +18,16 @@ pub trait Context: Send + Sync + Debug {
     fn default_sendmail_from(&self) -> String;
     /// Proxy configuration for the current node
     fn http_proxy_config(&self) -> Option<String>;
+    // Return default config for built-in targets/matchers.
+    fn default_config(&self) -> &'static str;
 }
 
+#[cfg(not(feature = "pve-context"))]
 static CONTEXT: Mutex<Option<&'static dyn Context>> = Mutex::new(None);
+// The test unfortunately require context...
+// TODO: Check if we can make this nicer...
+#[cfg(feature = "pve-context")]
+static CONTEXT: Mutex<Option<&'static dyn Context>> = Mutex::new(Some(&pve::PVE_CONTEXT));
 
 /// Set the product-specific context
 pub fn set_context(context: &'static dyn Context) {
diff --git a/proxmox-notify/src/context/pbs.rs b/proxmox-notify/src/context/pbs.rs
index b5d3168..5b97af7 100644
--- a/proxmox-notify/src/context/pbs.rs
+++ b/proxmox-notify/src/context/pbs.rs
@@ -56,6 +56,18 @@ fn lookup_mail_address(content: &str, username: &str) -> Option<String> {
     }
 }
 
+const DEFAULT_CONFIG: &str = "\
+sendmail: mail-to-root
+    comment Send mails to root@pam's email address
+    mailto-user root@pam
+
+
+matcher: default-matcher
+    mode all
+    target mail-to-root
+    comment Route all notifications to mail-to-root
+";
+
 #[derive(Debug)]
 pub struct PBSContext;
 
@@ -82,6 +94,10 @@ impl Context for PBSContext {
         let content = common::attempt_file_read(PBS_NODE_CFG_FILENAME);
         content.and_then(|content| common::lookup_datacenter_config_key(&content, "http-proxy"))
     }
+
+    fn default_config(&self) -> &'static str {
+        return DEFAULT_CONFIG;
+    }
 }
 
 #[cfg(test)]
diff --git a/proxmox-notify/src/context/pve.rs b/proxmox-notify/src/context/pve.rs
index f263c95..39e0e4a 100644
--- a/proxmox-notify/src/context/pve.rs
+++ b/proxmox-notify/src/context/pve.rs
@@ -11,6 +11,18 @@ fn lookup_mail_address(content: &str, user: &str) -> Option<String> {
     }))
 }
 
+const DEFAULT_CONFIG: &str = "\
+sendmail: mail-to-root
+	comment Send mails to root@pam's email address
+	mailto-user root@pam
+
+
+matcher: default-matcher
+    mode all
+    target mail-to-root
+    comment Route all notifications to mail-to-root
+";
+
 #[derive(Debug)]
 pub struct PVEContext;
 
@@ -35,6 +47,10 @@ impl Context for PVEContext {
         let content = common::attempt_file_read("/etc/pve/datacenter.cfg");
         content.and_then(|content| common::lookup_datacenter_config_key(&content, "http_proxy"))
     }
+
+    fn default_config(&self) -> &'static str {
+        return DEFAULT_CONFIG;
+    }
 }
 
 pub static PVE_CONTEXT: PVEContext = PVEContext;
diff --git a/proxmox-notify/src/endpoints/gotify.rs b/proxmox-notify/src/endpoints/gotify.rs
index c0d1dcb..90ae959 100644
--- a/proxmox-notify/src/endpoints/gotify.rs
+++ b/proxmox-notify/src/endpoints/gotify.rs
@@ -11,7 +11,7 @@ use proxmox_schema::{api, Updater};
 use crate::context::context;
 use crate::renderer::TemplateRenderer;
 use crate::schema::ENTITY_NAME_SCHEMA;
-use crate::{renderer, Content, Endpoint, Error, Notification, Severity};
+use crate::{renderer, Content, Endpoint, Error, Notification, Origin, Severity};
 
 fn severity_to_priority(level: Severity) -> u32 {
     match level {
@@ -55,6 +55,10 @@ pub struct GotifyConfig {
     /// Disable this target.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub disable: Option<bool>,
+    /// Origin of this config entry.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[updater(skip)]
+    pub origin: Option<Origin>,
 }
 
 #[api()]
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index f948cc6..4fc92b4 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -7,7 +7,7 @@ use crate::context::context;
 use crate::endpoints::common::mail;
 use crate::renderer::TemplateRenderer;
 use crate::schema::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA};
-use crate::{renderer, Content, Endpoint, Error, Notification};
+use crate::{renderer, Content, Endpoint, Error, Notification, Origin};
 
 pub(crate) const SENDMAIL_TYPENAME: &str = "sendmail";
 
@@ -65,6 +65,10 @@ pub struct SendmailConfig {
     /// Disable this target.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub disable: Option<bool>,
+    /// Origin of this config entry.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[updater(skip)]
+    pub origin: Option<Origin>,
 }
 
 #[derive(Serialize, Deserialize)]
diff --git a/proxmox-notify/src/endpoints/smtp.rs b/proxmox-notify/src/endpoints/smtp.rs
index 83a705f..064c9f9 100644
--- a/proxmox-notify/src/endpoints/smtp.rs
+++ b/proxmox-notify/src/endpoints/smtp.rs
@@ -11,7 +11,7 @@ use crate::context::context;
 use crate::endpoints::common::mail;
 use crate::renderer::TemplateRenderer;
 use crate::schema::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA};
-use crate::{renderer, Content, Endpoint, Error, Notification};
+use crate::{renderer, Content, Endpoint, Error, Notification, Origin};
 
 pub(crate) const SMTP_TYPENAME: &str = "smtp";
 
@@ -94,6 +94,10 @@ pub struct SmtpConfig {
     /// Disable this target.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub disable: Option<bool>,
+    /// Origin of this config entry.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[updater(skip)]
+    pub origin: Option<Origin>,
 }
 
 #[derive(Serialize, Deserialize)]
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index 4bb963a..1fb9623 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -3,6 +3,7 @@ use std::error::Error as StdError;
 use std::fmt::Display;
 use std::str::FromStr;
 
+use context::context;
 use serde::{Deserialize, Serialize};
 use serde_json::json;
 use serde_json::Value;
@@ -11,6 +12,7 @@ use proxmox_schema::api;
 use proxmox_section_config::SectionConfigData;
 
 pub mod matcher;
+use crate::config::CONFIG;
 use matcher::{MatcherConfig, MATCHER_TYPENAME};
 
 pub mod api;
@@ -132,6 +134,18 @@ impl FromStr for Severity {
     }
 }
 
+#[api()]
+#[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd)]
+#[serde(rename_all = "kebab-case")]
+pub enum Origin {
+    /// User-created config entry
+    UserCreated,
+    /// Config entry provided by the system
+    Builtin,
+    /// Config entry provided by the system, but modified by the user.
+    ModifiedBuiltin,
+}
+
 /// Notification endpoint trait, implemented by all endpoint plugins
 pub trait Endpoint {
     /// Send a documentation
@@ -247,9 +261,55 @@ pub struct Config {
 impl Config {
     /// Parse raw config
     pub fn new(raw_config: &str, raw_private_config: &str) -> Result<Self, Error> {
-        let (config, digest) = config::config(raw_config)?;
+        let (mut config, digest) = config::config(raw_config)?;
         let (private_config, _) = config::private_config(raw_private_config)?;
 
+        let default_config = context().default_config();
+
+        let builtin_config = CONFIG
+            .parse("<builtin>", default_config)
+            .map_err(|err| Error::ConfigDeserialization(err.into()))?;
+
+        for (key, (builtin_typename, builtin_value)) in &builtin_config.sections {
+            if let Some((typename, value)) = config.sections.get_mut(key) {
+                if builtin_typename == typename && value == builtin_value {
+                    // Entry is built-in and the config entry section in notifications.cfg
+                    // is exactly the same.
+                    if let Some(obj) = value.as_object_mut() {
+                        obj.insert("origin".to_string(), Value::String("builtin".into()));
+                    } else {
+                        log::error!("section config entry is not an object. This should not happen");
+                    }
+                } else {
+                    // Entry is built-in, but it has been modified by the user.
+                    if let Some(obj) = value.as_object_mut() {
+                        obj.insert("origin".to_string(), Value::String("modified-builtin".into()));
+                    } else {
+                        log::error!("section config entry is not an object. This should not happen");
+                    }
+                }
+            } else {
+                let mut val = builtin_value.clone();
+
+                if let Some(obj) = val.as_object_mut() {
+                    obj.insert("origin".to_string(), Value::String("builtin".into()));
+                } else {
+                    log::error!("section config entry is not an object. This should not happen");
+                }
+                config
+                    .set_data(key, builtin_typename, val)
+                    .map_err(|err| Error::ConfigDeserialization(err.into()))?;
+            }
+        }
+
+        for (_, (_, value)) in config.sections.iter_mut() {
+            if let Some(obj) = value.as_object_mut() {
+                if obj.get("origin").is_none() {
+                    obj.insert("origin".to_string(), Value::String("user-created".into()));
+                }
+            }
+        }
+
         Ok(Self {
             config,
             digest,
@@ -259,8 +319,21 @@ impl Config {
 
     /// Serialize config
     pub fn write(&self) -> Result<(String, String), Error> {
+        let mut c = self.config.clone();
+        for (_, (_, value)) in c.sections.iter_mut() {
+            // Remove 'origin' parameter, we do not want it in our
+            // config fields
+            // TODO: Check if there is a better way for this, maybe a
+            // separate type for API responses?
+            if let Some(obj) = value.as_object_mut() {
+                obj.remove("origin");
+            } else {
+                log::error!("section config entry is not an object. This should not happen");
+            }
+        }
+
         Ok((
-            config::write(&self.config)?,
+            config::write(&c)?,
             config::write_private(&self.private_config)?,
         ))
     }
diff --git a/proxmox-notify/src/matcher.rs b/proxmox-notify/src/matcher.rs
index ba1577d..42d988a 100644
--- a/proxmox-notify/src/matcher.rs
+++ b/proxmox-notify/src/matcher.rs
@@ -13,7 +13,7 @@ use proxmox_schema::{
 use proxmox_time::{parse_daily_duration, DailyDuration};
 
 use crate::schema::ENTITY_NAME_SCHEMA;
-use crate::{Error, Notification, Severity};
+use crate::{Error, Notification, Origin, Severity};
 
 pub const MATCHER_TYPENAME: &str = "matcher";
 
@@ -144,6 +144,11 @@ pub struct MatcherConfig {
     /// Disable this matcher
     #[serde(skip_serializing_if = "Option::is_none")]
     pub disable: Option<bool>,
+
+    /// Origin of this config entry.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[updater(skip)]
+    pub origin: Option<Origin>,
 }
 
 trait MatchDirective {
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-perl-rs 16/52] notify: adapt to new matcher-based notification routing
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (14 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 15/52] notify: add built-in config and 'origin' parameter Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 17/52] notify: add bindings for smtp API calls Lukas Wagner
                   ` (38 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 common/src/notify.rs | 167 +++++++++++++------------------------------
 1 file changed, 50 insertions(+), 117 deletions(-)

diff --git a/common/src/notify.rs b/common/src/notify.rs
index 9f44225..4fbd705 100644
--- a/common/src/notify.rs
+++ b/common/src/notify.rs
@@ -1,10 +1,12 @@
 #[perlmod::package(name = "Proxmox::RS::Notify")]
 mod export {
+    use std::collections::HashMap;
+    use std::sync::Mutex;
+
     use anyhow::{bail, Error};
-    use perlmod::Value;
     use serde_json::Value as JSONValue;
-    use std::sync::Mutex;
 
+    use perlmod::Value;
     use proxmox_http_error::HttpError;
     use proxmox_notify::endpoints::gotify::{
         DeleteableGotifyProperty, GotifyConfig, GotifyConfigUpdater, GotifyPrivateConfig,
@@ -13,10 +15,10 @@ mod export {
     use proxmox_notify::endpoints::sendmail::{
         DeleteableSendmailProperty, SendmailConfig, SendmailConfigUpdater,
     };
-    use proxmox_notify::filter::{
-        DeleteableFilterProperty, FilterConfig, FilterConfigUpdater, FilterModeOperator,
+    use proxmox_notify::matcher::{
+        CalendarMatcher, DeleteableMatcherProperty, FieldMatcher, MatchModeOperator, MatcherConfig,
+        MatcherConfigUpdater, SeverityMatcher,
     };
-    use proxmox_notify::group::{DeleteableGroupProperty, GroupConfig, GroupConfigUpdater};
     use proxmox_notify::{api, Config, Notification, Severity};
 
     pub struct NotificationConfig {
@@ -87,22 +89,22 @@ mod export {
     #[export(serialize_error)]
     fn send(
         #[try_from_ref] this: &NotificationConfig,
-        channel: &str,
         severity: Severity,
         title: String,
         body: String,
-        properties: Option<JSONValue>,
+        template_data: Option<JSONValue>,
+        fields: Option<HashMap<String, String>>,
     ) -> Result<(), HttpError> {
         let config = this.config.lock().unwrap();
-
-        let notification = Notification {
+        let notification = Notification::new_templated(
             severity,
             title,
             body,
-            properties,
-        };
+            template_data.unwrap_or_default(),
+            fields.unwrap_or_default(),
+        );
 
-        api::common::send(&config, channel, &notification)
+        api::common::send(&config, &notification)
     }
 
     #[export(serialize_error)]
@@ -114,78 +116,6 @@ mod export {
         api::common::test_target(&config, target)
     }
 
-    #[export(serialize_error)]
-    fn get_groups(
-        #[try_from_ref] this: &NotificationConfig,
-    ) -> Result<Vec<GroupConfig>, HttpError> {
-        let config = this.config.lock().unwrap();
-        api::group::get_groups(&config)
-    }
-
-    #[export(serialize_error)]
-    fn get_group(
-        #[try_from_ref] this: &NotificationConfig,
-        id: &str,
-    ) -> Result<GroupConfig, HttpError> {
-        let config = this.config.lock().unwrap();
-        api::group::get_group(&config, id)
-    }
-
-    #[export(serialize_error)]
-    fn add_group(
-        #[try_from_ref] this: &NotificationConfig,
-        name: String,
-        endpoints: Vec<String>,
-        comment: Option<String>,
-        filter: Option<String>,
-    ) -> Result<(), HttpError> {
-        let mut config = this.config.lock().unwrap();
-        api::group::add_group(
-            &mut config,
-            &GroupConfig {
-                name,
-                endpoint: endpoints,
-                comment,
-                filter,
-            },
-        )
-    }
-
-    #[export(serialize_error)]
-    fn update_group(
-        #[try_from_ref] this: &NotificationConfig,
-        name: &str,
-        endpoints: Option<Vec<String>>,
-        comment: Option<String>,
-        filter: Option<String>,
-        delete: Option<Vec<DeleteableGroupProperty>>,
-        digest: Option<&str>,
-    ) -> Result<(), HttpError> {
-        let mut config = this.config.lock().unwrap();
-        let digest = decode_digest(digest)?;
-
-        api::group::update_group(
-            &mut config,
-            name,
-            &GroupConfigUpdater {
-                endpoint: endpoints,
-                comment,
-                filter,
-            },
-            delete.as_deref(),
-            digest.as_deref(),
-        )
-    }
-
-    #[export(serialize_error)]
-    fn delete_group(
-        #[try_from_ref] this: &NotificationConfig,
-        name: &str,
-    ) -> Result<(), HttpError> {
-        let mut config = this.config.lock().unwrap();
-        api::group::delete_group(&mut config, name)
-    }
-
     #[export(serialize_error)]
     fn get_sendmail_endpoints(
         #[try_from_ref] this: &NotificationConfig,
@@ -213,7 +143,6 @@ mod export {
         from_address: Option<String>,
         author: Option<String>,
         comment: Option<String>,
-        filter: Option<String>,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
 
@@ -226,7 +155,7 @@ mod export {
                 from_address,
                 author,
                 comment,
-                filter,
+                filter: None,
             },
         )
     }
@@ -241,7 +170,6 @@ mod export {
         from_address: Option<String>,
         author: Option<String>,
         comment: Option<String>,
-        filter: Option<String>,
         delete: Option<Vec<DeleteableSendmailProperty>>,
         digest: Option<&str>,
     ) -> Result<(), HttpError> {
@@ -257,7 +185,6 @@ mod export {
                 from_address,
                 author,
                 comment,
-                filter,
             },
             delete.as_deref(),
             digest.as_deref(),
@@ -297,7 +224,6 @@ mod export {
         server: String,
         token: String,
         comment: Option<String>,
-        filter: Option<String>,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::gotify::add_endpoint(
@@ -306,7 +232,7 @@ mod export {
                 name: name.clone(),
                 server,
                 comment,
-                filter,
+                filter: None,
             },
             &GotifyPrivateConfig { name, token },
         )
@@ -320,7 +246,6 @@ mod export {
         server: Option<String>,
         token: Option<String>,
         comment: Option<String>,
-        filter: Option<String>,
         delete: Option<Vec<DeleteableGotifyProperty>>,
         digest: Option<&str>,
     ) -> Result<(), HttpError> {
@@ -330,11 +255,7 @@ mod export {
         api::gotify::update_endpoint(
             &mut config,
             name,
-            &GotifyConfigUpdater {
-                server,
-                comment,
-                filter,
-            },
+            &GotifyConfigUpdater { server, comment },
             &GotifyPrivateConfigUpdater { token },
             delete.as_deref(),
             digest.as_deref(),
@@ -351,38 +272,44 @@ mod export {
     }
 
     #[export(serialize_error)]
-    fn get_filters(
+    fn get_matchers(
         #[try_from_ref] this: &NotificationConfig,
-    ) -> Result<Vec<FilterConfig>, HttpError> {
+    ) -> Result<Vec<MatcherConfig>, HttpError> {
         let config = this.config.lock().unwrap();
-        api::filter::get_filters(&config)
+        api::matcher::get_matchers(&config)
     }
 
     #[export(serialize_error)]
-    fn get_filter(
+    fn get_matcher(
         #[try_from_ref] this: &NotificationConfig,
         id: &str,
-    ) -> Result<FilterConfig, HttpError> {
+    ) -> Result<MatcherConfig, HttpError> {
         let config = this.config.lock().unwrap();
-        api::filter::get_filter(&config, id)
+        api::matcher::get_matcher(&config, id)
     }
 
     #[export(serialize_error)]
     #[allow(clippy::too_many_arguments)]
-    fn add_filter(
+    fn add_matcher(
         #[try_from_ref] this: &NotificationConfig,
         name: String,
-        min_severity: Option<Severity>,
-        mode: Option<FilterModeOperator>,
+        target: Option<Vec<String>>,
+        match_severity: Option<Vec<SeverityMatcher>>,
+        match_field: Option<Vec<FieldMatcher>>,
+        match_calendar: Option<Vec<CalendarMatcher>>,
+        mode: Option<MatchModeOperator>,
         invert_match: Option<bool>,
         comment: Option<String>,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
-        api::filter::add_filter(
+        api::matcher::add_matcher(
             &mut config,
-            &FilterConfig {
+            &MatcherConfig {
                 name,
-                min_severity,
+                match_severity,
+                match_field,
+                match_calendar,
+                target,
                 mode,
                 invert_match,
                 comment,
@@ -392,24 +319,30 @@ mod export {
 
     #[export(serialize_error)]
     #[allow(clippy::too_many_arguments)]
-    fn update_filter(
+    fn update_matcher(
         #[try_from_ref] this: &NotificationConfig,
         name: &str,
-        min_severity: Option<Severity>,
-        mode: Option<FilterModeOperator>,
+        target: Option<Vec<String>>,
+        match_severity: Option<Vec<SeverityMatcher>>,
+        match_field: Option<Vec<FieldMatcher>>,
+        match_calendar: Option<Vec<CalendarMatcher>>,
+        mode: Option<MatchModeOperator>,
         invert_match: Option<bool>,
         comment: Option<String>,
-        delete: Option<Vec<DeleteableFilterProperty>>,
+        delete: Option<Vec<DeleteableMatcherProperty>>,
         digest: Option<&str>,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         let digest = decode_digest(digest)?;
 
-        api::filter::update_filter(
+        api::matcher::update_matcher(
             &mut config,
             name,
-            &FilterConfigUpdater {
-                min_severity,
+            &MatcherConfigUpdater {
+                match_severity,
+                match_field,
+                match_calendar,
+                target,
                 mode,
                 invert_match,
                 comment,
@@ -420,12 +353,12 @@ mod export {
     }
 
     #[export(serialize_error)]
-    fn delete_filter(
+    fn delete_matcher(
         #[try_from_ref] this: &NotificationConfig,
         name: &str,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
-        api::filter::delete_filter(&mut config, name)
+        api::matcher::delete_matcher(&mut config, name)
     }
 
     #[export]
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-perl-rs 17/52] notify: add bindings for smtp API calls
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (15 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 16/52] notify: adapt to new matcher-based notification routing Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 18/52] pve-rs: notify: remove notify_context for PVE Lukas Wagner
                   ` (37 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 common/src/notify.rs | 106 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/common/src/notify.rs b/common/src/notify.rs
index 4fbd705..8a6d76e 100644
--- a/common/src/notify.rs
+++ b/common/src/notify.rs
@@ -15,6 +15,10 @@ mod export {
     use proxmox_notify::endpoints::sendmail::{
         DeleteableSendmailProperty, SendmailConfig, SendmailConfigUpdater,
     };
+    use proxmox_notify::endpoints::smtp::{
+        DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpMode, SmtpPrivateConfig,
+        SmtpPrivateConfigUpdater,
+    };
     use proxmox_notify::matcher::{
         CalendarMatcher, DeleteableMatcherProperty, FieldMatcher, MatchModeOperator, MatcherConfig,
         MatcherConfigUpdater, SeverityMatcher,
@@ -271,6 +275,108 @@ mod export {
         api::gotify::delete_gotify_endpoint(&mut config, name)
     }
 
+    #[export(serialize_error)]
+    fn get_smtp_endpoints(
+        #[try_from_ref] this: &NotificationConfig,
+    ) -> Result<Vec<SmtpConfig>, HttpError> {
+        let config = this.config.lock().unwrap();
+        api::smtp::get_endpoints(&config)
+    }
+
+    #[export(serialize_error)]
+    fn get_smtp_endpoint(
+        #[try_from_ref] this: &NotificationConfig,
+        id: &str,
+    ) -> Result<SmtpConfig, HttpError> {
+        let config = this.config.lock().unwrap();
+        api::smtp::get_endpoint(&config, id)
+    }
+
+    #[export(serialize_error)]
+    #[allow(clippy::too_many_arguments)]
+    fn add_smtp_endpoint(
+        #[try_from_ref] this: &NotificationConfig,
+        name: String,
+        server: String,
+        port: Option<u16>,
+        mode: Option<SmtpMode>,
+        username: Option<String>,
+        password: Option<String>,
+        mailto: Option<Vec<String>>,
+        mailto_user: Option<Vec<String>>,
+        from_address: String,
+        author: Option<String>,
+        comment: Option<String>,
+    ) -> Result<(), HttpError> {
+        let mut config = this.config.lock().unwrap();
+        api::smtp::add_endpoint(
+            &mut config,
+            &SmtpConfig {
+                name: name.clone(),
+                server,
+                port,
+                mode,
+                username,
+                mailto,
+                mailto_user,
+                from_address,
+                author,
+                comment,
+            },
+            &SmtpPrivateConfig { name, password },
+        )
+    }
+
+    #[export(serialize_error)]
+    #[allow(clippy::too_many_arguments)]
+    fn update_smtp_endpoint(
+        #[try_from_ref] this: &NotificationConfig,
+        name: &str,
+        server: Option<String>,
+        port: Option<u16>,
+        mode: Option<SmtpMode>,
+        username: Option<String>,
+        password: Option<String>,
+        mailto: Option<Vec<String>>,
+        mailto_user: Option<Vec<String>>,
+        from_address: Option<String>,
+        author: Option<String>,
+        comment: Option<String>,
+        delete: Option<Vec<DeleteableSmtpProperty>>,
+        digest: Option<&str>,
+    ) -> Result<(), HttpError> {
+        let mut config = this.config.lock().unwrap();
+        let digest = decode_digest(digest)?;
+
+        api::smtp::update_endpoint(
+            &mut config,
+            name,
+            &SmtpConfigUpdater {
+                server,
+                port,
+                mode,
+                username,
+                mailto,
+                mailto_user,
+                from_address,
+                author,
+                comment,
+            },
+            &SmtpPrivateConfigUpdater { password },
+            delete.as_deref(),
+            digest.as_deref(),
+        )
+    }
+
+    #[export(serialize_error)]
+    fn delete_smtp_endpoint(
+        #[try_from_ref] this: &NotificationConfig,
+        name: &str,
+    ) -> Result<(), HttpError> {
+        let mut config = this.config.lock().unwrap();
+        api::smtp::delete_endpoint(&mut config, name)
+    }
+
     #[export(serialize_error)]
     fn get_matchers(
         #[try_from_ref] this: &NotificationConfig,
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-perl-rs 18/52] pve-rs: notify: remove notify_context for PVE
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (16 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 17/52] notify: add bindings for smtp API calls Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 19/52] notify: add 'disable' parameter Lukas Wagner
                   ` (36 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

The context has now been moved to `proxmox-notify` due to the fact
that we also need it in `proxmox-mail-forward` now.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    Changes v2 -> v3:
      - No changes

 pve-rs/Cargo.toml            |   2 +-
 pve-rs/src/lib.rs            |   7 ++-
 pve-rs/src/notify_context.rs | 117 -----------------------------------
 3 files changed, 5 insertions(+), 121 deletions(-)
 delete mode 100644 pve-rs/src/notify_context.rs

diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index e222d9d..2300c8d 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -36,7 +36,7 @@ perlmod = { version = "0.13", features = [ "exporter" ] }
 proxmox-apt = "0.10.6"
 proxmox-http = { version = "0.9", features = ["client-sync", "client-trait"] }
 proxmox-http-error = "0.1.0"
-proxmox-notify = "0.2"
+proxmox-notify = { version = "0.2", features = ["pve-context"] }
 proxmox-openid = "0.10"
 proxmox-resource-scheduling = "0.3.0"
 proxmox-subscription = "0.4"
diff --git a/pve-rs/src/lib.rs b/pve-rs/src/lib.rs
index d1915c9..42be39e 100644
--- a/pve-rs/src/lib.rs
+++ b/pve-rs/src/lib.rs
@@ -4,18 +4,19 @@
 pub mod common;
 
 pub mod apt;
-pub mod notify_context;
 pub mod openid;
 pub mod resource_scheduling;
 pub mod tfa;
 
 #[perlmod::package(name = "Proxmox::Lib::PVE", lib = "pve_rs")]
 mod export {
-    use crate::{common, notify_context};
+    use proxmox_notify::context::pve::PVE_CONTEXT;
+
+    use crate::common;
 
     #[export]
     pub fn init() {
         common::logger::init("PVE_LOG", "info");
-        notify_context::init();
+        proxmox_notify::context::set_context(&PVE_CONTEXT)
     }
 }
diff --git a/pve-rs/src/notify_context.rs b/pve-rs/src/notify_context.rs
deleted file mode 100644
index 3cf3e18..0000000
--- a/pve-rs/src/notify_context.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-use log;
-use std::path::Path;
-
-use proxmox_notify::context::Context;
-
-// Some helpers borrowed and slightly adapted from `proxmox-mail-forward`
-
-fn normalize_for_return(s: Option<&str>) -> Option<String> {
-    match s?.trim() {
-        "" => None,
-        s => Some(s.to_string()),
-    }
-}
-
-fn attempt_file_read<P: AsRef<Path>>(path: P) -> Option<String> {
-    match proxmox_sys::fs::file_read_optional_string(path) {
-        Ok(contents) => contents,
-        Err(err) => {
-            log::error!("{err}");
-            None
-        }
-    }
-}
-
-fn lookup_mail_address(content: &str, user: &str) -> Option<String> {
-    normalize_for_return(content.lines().find_map(|line| {
-        let fields: Vec<&str> = line.split(':').collect();
-        #[allow(clippy::get_first)] // to keep expression style consistent
-        match fields.get(0)?.trim() == "user" && fields.get(1)?.trim() == user {
-            true => fields.get(6).copied(),
-            false => None,
-        }
-    }))
-}
-
-fn lookup_datacenter_config_key(content: &str, key: &str) -> Option<String> {
-    let key_prefix = format!("{key}:");
-    normalize_for_return(
-        content
-            .lines()
-            .find_map(|line| line.strip_prefix(&key_prefix)),
-    )
-}
-
-#[derive(Debug)]
-struct PVEContext;
-
-impl Context for PVEContext {
-    fn lookup_email_for_user(&self, user: &str) -> Option<String> {
-        let content = attempt_file_read("/etc/pve/user.cfg");
-        content.and_then(|content| lookup_mail_address(&content, user))
-    }
-
-    fn default_sendmail_author(&self) -> String {
-        "Proxmox VE".into()
-    }
-
-    fn default_sendmail_from(&self) -> String {
-        let content = attempt_file_read("/etc/pve/datacenter.cfg");
-        content
-            .and_then(|content| lookup_datacenter_config_key(&content, "email_from"))
-            .unwrap_or_else(|| String::from("root"))
-    }
-
-    fn http_proxy_config(&self) -> Option<String> {
-        let content = attempt_file_read("/etc/pve/datacenter.cfg");
-        content.and_then(|content| lookup_datacenter_config_key(&content, "http_proxy"))
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    const USER_CONFIG: &str = "
-user:root@pam:1:0:::root@example.com:::
-user:test@pve:1:0:::test@example.com:::
-user:no-mail@pve:1:0::::::
-    ";
-
-    #[test]
-    fn test_parse_mail() {
-        assert_eq!(
-            lookup_mail_address(USER_CONFIG, "root@pam"),
-            Some("root@example.com".to_string())
-        );
-        assert_eq!(
-            lookup_mail_address(USER_CONFIG, "test@pve"),
-            Some("test@example.com".to_string())
-        );
-        assert_eq!(lookup_mail_address(USER_CONFIG, "no-mail@pve"), None);
-    }
-
-    const DC_CONFIG: &str = "
-email_from: user@example.com
-http_proxy: http://localhost:1234
-keyboard: en-us
-";
-    #[test]
-    fn test_parse_dc_config() {
-        assert_eq!(
-            lookup_datacenter_config_key(DC_CONFIG, "email_from"),
-            Some("user@example.com".to_string())
-        );
-        assert_eq!(
-            lookup_datacenter_config_key(DC_CONFIG, "http_proxy"),
-            Some("http://localhost:1234".to_string())
-        );
-        assert_eq!(lookup_datacenter_config_key(DC_CONFIG, "foo"), None);
-    }
-}
-
-static CONTEXT: PVEContext = PVEContext;
-
-pub fn init() {
-    proxmox_notify::context::set_context(&CONTEXT)
-}
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-perl-rs 19/52] notify: add 'disable' parameter
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (17 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 18/52] pve-rs: notify: remove notify_context for PVE Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 20/52] notify: support 'origin' paramter Lukas Wagner
                   ` (35 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This parameter disables a matcher/a target.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 common/src/notify.rs | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/common/src/notify.rs b/common/src/notify.rs
index 8a6d76e..a5ab754 100644
--- a/common/src/notify.rs
+++ b/common/src/notify.rs
@@ -147,6 +147,7 @@ mod export {
         from_address: Option<String>,
         author: Option<String>,
         comment: Option<String>,
+        disable: Option<bool>,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
 
@@ -159,7 +160,7 @@ mod export {
                 from_address,
                 author,
                 comment,
-                filter: None,
+                disable,
             },
         )
     }
@@ -174,6 +175,7 @@ mod export {
         from_address: Option<String>,
         author: Option<String>,
         comment: Option<String>,
+        disable: Option<bool>,
         delete: Option<Vec<DeleteableSendmailProperty>>,
         digest: Option<&str>,
     ) -> Result<(), HttpError> {
@@ -189,6 +191,7 @@ mod export {
                 from_address,
                 author,
                 comment,
+                disable,
             },
             delete.as_deref(),
             digest.as_deref(),
@@ -228,6 +231,7 @@ mod export {
         server: String,
         token: String,
         comment: Option<String>,
+        disable: Option<bool>,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::gotify::add_endpoint(
@@ -236,6 +240,7 @@ mod export {
                 name: name.clone(),
                 server,
                 comment,
+                disable,
                 filter: None,
             },
             &GotifyPrivateConfig { name, token },
@@ -250,6 +255,7 @@ mod export {
         server: Option<String>,
         token: Option<String>,
         comment: Option<String>,
+        disable: Option<bool>,
         delete: Option<Vec<DeleteableGotifyProperty>>,
         digest: Option<&str>,
     ) -> Result<(), HttpError> {
@@ -259,7 +265,11 @@ mod export {
         api::gotify::update_endpoint(
             &mut config,
             name,
-            &GotifyConfigUpdater { server, comment },
+            &GotifyConfigUpdater {
+                server,
+                comment,
+                disable,
+            },
             &GotifyPrivateConfigUpdater { token },
             delete.as_deref(),
             digest.as_deref(),
@@ -307,6 +317,7 @@ mod export {
         from_address: String,
         author: Option<String>,
         comment: Option<String>,
+        disable: Option<bool>,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::smtp::add_endpoint(
@@ -322,6 +333,7 @@ mod export {
                 from_address,
                 author,
                 comment,
+                disable,
             },
             &SmtpPrivateConfig { name, password },
         )
@@ -342,6 +354,7 @@ mod export {
         from_address: Option<String>,
         author: Option<String>,
         comment: Option<String>,
+        disable: Option<bool>,
         delete: Option<Vec<DeleteableSmtpProperty>>,
         digest: Option<&str>,
     ) -> Result<(), HttpError> {
@@ -361,6 +374,7 @@ mod export {
                 from_address,
                 author,
                 comment,
+                disable,
             },
             &SmtpPrivateConfigUpdater { password },
             delete.as_deref(),
@@ -406,6 +420,7 @@ mod export {
         mode: Option<MatchModeOperator>,
         invert_match: Option<bool>,
         comment: Option<String>,
+        disable: Option<bool>,
     ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::matcher::add_matcher(
@@ -419,6 +434,7 @@ mod export {
                 mode,
                 invert_match,
                 comment,
+                disable,
             },
         )
     }
@@ -435,6 +451,7 @@ mod export {
         mode: Option<MatchModeOperator>,
         invert_match: Option<bool>,
         comment: Option<String>,
+        disable: Option<bool>,
         delete: Option<Vec<DeleteableMatcherProperty>>,
         digest: Option<&str>,
     ) -> Result<(), HttpError> {
@@ -452,6 +469,7 @@ mod export {
                 mode,
                 invert_match,
                 comment,
+                disable,
             },
             delete.as_deref(),
             digest.as_deref(),
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-perl-rs 20/52] notify: support 'origin' paramter
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (18 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 19/52] notify: add 'disable' parameter Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-cluster 21/52] notify: adapt to matcher based notification system Lukas Wagner
                   ` (34 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This parameter shows the origin of a config entry (builtin,
user-created, modified-builtin)

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 common/src/notify.rs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/common/src/notify.rs b/common/src/notify.rs
index a5ab754..8f9f38f 100644
--- a/common/src/notify.rs
+++ b/common/src/notify.rs
@@ -161,6 +161,8 @@ mod export {
                 author,
                 comment,
                 disable,
+                filter: None,
+                origin: None,
             },
         )
     }
@@ -242,6 +244,7 @@ mod export {
                 comment,
                 disable,
                 filter: None,
+                origin: None,
             },
             &GotifyPrivateConfig { name, token },
         )
@@ -334,6 +337,7 @@ mod export {
                 author,
                 comment,
                 disable,
+                origin: None,
             },
             &SmtpPrivateConfig { name, password },
         )
@@ -435,6 +439,7 @@ mod export {
                 invert_match,
                 comment,
                 disable,
+                origin: None,
             },
         )
     }
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-cluster 21/52] notify: adapt to matcher based notification system
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (19 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 20/52] notify: support 'origin' paramter Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-guest-common 22/52] vzdump: deprecate mailto/mailnotification/notification-{target, policy} Lukas Wagner
                   ` (33 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This commit removes the target paramters from all notify calls. Also,
the default 'mail-to-root' target is not added automatically any more
- this target will be added by an dpkg hook in the future.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/PVE/Notify.pm | 101 +++++++++++++++++++++-------------------------
 1 file changed, 47 insertions(+), 54 deletions(-)

diff --git a/src/PVE/Notify.pm b/src/PVE/Notify.pm
index 419bf6d..872eb25 100644
--- a/src/PVE/Notify.pm
+++ b/src/PVE/Notify.pm
@@ -18,8 +18,6 @@ cfs_register_file(
     \&write_notification_config,
 );
 
-my $mail_to_root_target = 'mail-to-root';
-
 sub parse_notification_config {
     my ($filename, $raw) = @_;
 
@@ -48,86 +46,81 @@ sub read_config {
 
     my $notification_config = Proxmox::RS::Notify->parse_config($config, $priv_config);
 
-    eval {
-	# This target should always be available...
-	$notification_config->add_sendmail_endpoint(
-	    $mail_to_root_target,
-	    undef,
-	    ['root@pam'],
-	    undef,
-	    undef,
-	    'Send mail to root@pam\'s email address'
-	);
-    };
-
     return $notification_config;
 }
 
 sub write_config {
     my ($notification_config) = @_;
 
-    eval {
-	# ... but don't persist it to the config.
-	# Rationale: If it is in the config, the user might think
-	# that it can be changed by editing the configuration there.
-	# However, since we always add it in `read_config`, any changes
-	# will be implicitly overridden by the default.
-
-	# If users want's to change the configuration, they are supposed to
-	# create a new sendmail endpoint.
-	$notification_config->delete_sendmail_endpoint($mail_to_root_target);
-    };
-
     my ($config, $priv_config) = $notification_config->write_config();
     cfs_write_file('notifications.cfg', $config, 1);
     cfs_write_file('priv/notifications.cfg', $priv_config, 1);
 }
 
-sub default_target {
-    return $mail_to_root_target;
-}
-
 my $send_notification = sub {
-    my ($target, $severity, $title, $message, $properties, $config) = @_;
+    my ($severity, $title, $message, $template_data, $fields, $config) = @_;
     $config = read_config() if !defined($config);
-    my ($module, $file, $line) = caller(1);
-
-    # Augment properties with the source code location of the notify call
-    my $props_with_source = {
-	%$properties,
-	source => {
-	    module => $module,
-	    file => $file,
-	    line => $line,
-	}
-    };
-
-    $config->send($target, $severity, $title, $message, $props_with_source);
+    $config->send($severity, $title, $message, $template_data, $fields);
 };
 
 sub notify {
-    my ($target, $severity, $title, $message, $properties, $config) = @_;
-    $send_notification->($target, $severity, $title, $message, $properties, $config);
+    my ($severity, $title, $message, $template_data, $fields, $config) = @_;
+    $send_notification->(
+        $severity,
+        $title,
+        $message,
+        $template_data,
+        $fields,
+        $config
+    );
 }
 
 sub info {
-    my ($target, $title, $message, $properties, $config) = @_;
-    $send_notification->($target, 'info', $title, $message, $properties, $config);
+    my ($title, $message, $template_data, $fields, $config) = @_;
+    $send_notification->(
+        'info',
+        $title,
+        $message,
+        $template_data,
+        $fields,
+        $config
+    );
 }
 
 sub notice {
-    my ($target, $title, $message, $properties, $config) = @_;
-    $send_notification->($target, 'notice', $title, $message, $properties, $config);
+    my ($title, $message, $template_data, $fields, $config) = @_;
+    $send_notification->(
+        'notice',
+        $title,
+        $message,
+        $template_data,
+        $fields,
+        $config
+    );
 }
 
 sub warning {
-    my ($target, $title, $message, $properties, $config) = @_;
-    $send_notification->($target, 'warning', $title, $message, $properties, $config);
+    my ($title, $message, $template_data, $fields, $config) = @_;
+    $send_notification->(
+        'warning',
+        $title,
+        $message,
+        $template_data,
+        $fields,
+        $config
+    );
 }
 
 sub error {
-    my ($target, $title, $message, $properties, $config) = @_;
-    $send_notification->($target, 'error', $title, $message, $properties, $config);
+    my ($title, $message, $template_data, $fields, $config) = @_;
+    $send_notification->(
+        'error',
+        $title,
+        $message,
+        $template_data,
+        $fields,
+        $config
+    );
 }
 
 sub check_may_use_target {
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-guest-common 22/52] vzdump: deprecate mailto/mailnotification/notification-{target, policy}
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (20 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-cluster 21/52] notify: adapt to matcher based notification system Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-ha-manager 23/52] env: switch to matcher-based notification system Lukas Wagner
                   ` (32 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

The first two will be migrated to the notification system, the second
were part for the first attempt for the new notification system.
The first attempt only ever hit pvetest, so we simply tell the user
to not use the two params.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/PVE/VZDump/Common.pm | 16 +++++++---------
 1 file changed, 7 insertions(+), 9 deletions(-)

diff --git a/src/PVE/VZDump/Common.pm b/src/PVE/VZDump/Common.pm
index be605af..b93ad86 100644
--- a/src/PVE/VZDump/Common.pm
+++ b/src/PVE/VZDump/Common.pm
@@ -175,21 +175,22 @@ my $confdesc = {
     mailto => {
 	type => 'string',
 	format => 'email-or-username-list',
-	description => "Comma-separated list of email addresses or users that should" .
-	    " receive email notifications. Has no effect if the 'notification-target' option " .
-	    " is set at the same time.",
+	description => "Deprecated: Use notification targets/matchers instead." .
+	    " Comma-separated list of email addresses or users that should" .
+	    " receive email notifications.",
 	optional => 1,
     },
     mailnotification => {
 	type => 'string',
-	description => "Deprecated: use 'notification-policy' instead.",
+	description => "Deprecated: use notification targets/matchers instead." .
+	    " Specify when to send a notification mail",
 	optional => 1,
 	enum => [ 'always', 'failure' ],
 	default => 'always',
     },
     'notification-policy' => {
 	type => 'string',
-	description => "Specify when to send a notification",
+	description => "Deprecated: Do not use",
 	optional => 1,
 	enum => [ 'always', 'failure', 'never'],
 	default => 'always',
@@ -197,10 +198,7 @@ my $confdesc = {
     'notification-target' => {
 	type => 'string',
 	format => 'pve-configid',
-	description => "Determine the target to which notifications should be sent." .
-	    " Can either be a notification endpoint or a notification group." .
-	    " This option takes precedence over 'mailto', meaning that if both are " .
-	    " set, the 'mailto' option will be ignored.",
+	description => "Deprecated: Do not use",
 	optional => 1,
     },
     tmpdir => {
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-ha-manager 23/52] env: switch to matcher-based notification system
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (21 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-guest-common 22/52] vzdump: deprecate mailto/mailnotification/notification-{target, policy} Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 24/52] api: notification: remove notification groups Lukas Wagner
                   ` (31 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/PVE/HA/Env/PVE2.pm   | 10 ++--------
 src/PVE/HA/NodeStatus.pm | 11 +++++++++--
 2 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/src/PVE/HA/Env/PVE2.pm b/src/PVE/HA/Env/PVE2.pm
index ea9e6e4..fcb60a9 100644
--- a/src/PVE/HA/Env/PVE2.pm
+++ b/src/PVE/HA/Env/PVE2.pm
@@ -221,16 +221,10 @@ sub log {
 }
 
 sub send_notification {
-    my ($self, $subject, $text, $properties) = @_;
+    my ($self, $subject, $text, $template_data, $metadata_fields) = @_;
 
     eval {
-	my $dc_config = PVE::Cluster::cfs_read_file('datacenter.cfg');
-	my $target = $dc_config->{notify}->{'target-fencing'} // PVE::Notify::default_target();
-	my $notify = $dc_config->{notify}->{fencing} // 'always';
-
-	if ($notify eq 'always') {
-	    PVE::Notify::error($target, $subject, $text, $properties);
-	}
+	PVE::Notify::error($subject, $text, $template_data, $metadata_fields);
     };
 
     $self->log("warning", "could not notify: $@") if $@;
diff --git a/src/PVE/HA/NodeStatus.pm b/src/PVE/HA/NodeStatus.pm
index b264a36..e053c55 100644
--- a/src/PVE/HA/NodeStatus.pm
+++ b/src/PVE/HA/NodeStatus.pm
@@ -212,7 +212,7 @@ my $send_fence_state_email = sub {
     my $haenv = $self->{haenv};
     my $status = $haenv->read_manager_status();
 
-    my $notification_properties = {
+    my $template_data = {
 	"status-data"    => {
 	    manager_status => $status,
 	    node_status    => $self->{status}
@@ -222,11 +222,18 @@ my $send_fence_state_email = sub {
 	"subject"        => $subject,
     };
 
+    my $metadata_fields = {
+	type => 'fencing',
+	hostname => $node,
+    };
+
     $haenv->send_notification(
 	$subject_template,
 	$body_template,
-	$notification_properties
+	$template_data,
+	$metadata_fields,
     );
+
 };
 
 
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 24/52] api: notification: remove notification groups
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (22 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-ha-manager 23/52] env: switch to matcher-based notification system Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 25/52] api: notification: add new matcher-based notification API Lukas Wagner
                   ` (30 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 PVE/API2/Cluster/Notifications.pm | 267 +-----------------------------
 1 file changed, 4 insertions(+), 263 deletions(-)

diff --git a/PVE/API2/Cluster/Notifications.pm b/PVE/API2/Cluster/Notifications.pm
index ec666903..b34802c8 100644
--- a/PVE/API2/Cluster/Notifications.pm
+++ b/PVE/API2/Cluster/Notifications.pm
@@ -121,7 +121,6 @@ __PACKAGE__->register_method ({
 	my $result = [
 	    { name => 'endpoints' },
 	    { name => 'filters' },
-	    { name => 'groups' },
 	    { name => 'targets' },
 	];
 
@@ -161,8 +160,7 @@ __PACKAGE__->register_method ({
     name => 'get_all_targets',
     path => 'targets',
     method => 'GET',
-    description => 'Returns a list of all entities that can be used as notification targets' .
-	' (endpoints and groups).',
+    description => 'Returns a list of all entities that can be used as notification targets.',
     permissions => {
 	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
 	    . " 'Mapping.Audit' permissions on '/mapping/notification/<name>'."
@@ -180,14 +178,14 @@ __PACKAGE__->register_method ({
 	    type => 'object',
 	    properties => {
 		name => {
-		    description => 'Name of the endpoint/group.',
+		    description => 'Name of the target.',
 		    type => 'string',
 		    format => 'pve-configid',
 		},
 		'type' => {
-		    description => 'Type of the endpoint or group.',
+		    description => 'Type of the target.',
 		    type  => 'string',
-		    enum => [qw(sendmail gotify group)],
+		    enum => [qw(sendmail gotify)],
 		},
 		'comment' => {
 		    description => 'Comment',
@@ -221,14 +219,6 @@ __PACKAGE__->register_method ({
 		};
 	    }
 
-	    for my $target (@{$config->get_groups()}) {
-		push @$result, {
-		    name => $target->{name},
-		    comment => $target->{comment},
-		    type => 'group',
-		};
-	    }
-
 	    $result
 	};
 
@@ -290,255 +280,6 @@ __PACKAGE__->register_method ({
     }
 });
 
-my $group_properties = {
-    name => {
-	description => 'Name of the group.',
-	type => 'string',
-	format => 'pve-configid',
-    },
-    'endpoint' => {
-	type => 'array',
-	items => {
-	    type => 'string',
-	    format => 'pve-configid',
-	},
-	description => 'List of included endpoints',
-    },
-    'comment' => {
-	description => 'Comment',
-	type => 'string',
-	optional => 1,
-    },
-    filter => {
-	description => 'Name of the filter that should be applied.',
-	type => 'string',
-	format => 'pve-configid',
-	optional => 1,
-    },
-};
-
-__PACKAGE__->register_method ({
-    name => 'get_groups',
-    path => 'groups',
-    method => 'GET',
-    description => 'Returns a list of all groups',
-    protected => 1,
-    permissions => {
-	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
-	    . " 'Mapping.Audit' permissions on '/mapping/notification/<name>'.",
-	user => 'all',
-    },
-    parameters => {
-	additionalProperties => 0,
-	properties => {},
-    },
-    returns => {
-	type => 'array',
-	items => {
-	    type => 'object',
-	    properties => $group_properties,
-	},
-	links => [ { rel => 'child', href => '{name}' } ],
-    },
-    code => sub {
-	my $config = PVE::Notify::read_config();
-	my $rpcenv = PVE::RPCEnvironment::get();
-
-	my $entities = eval {
-	    $config->get_groups();
-	};
-	raise_api_error($@) if $@;
-
-	return filter_entities_by_privs($rpcenv, $entities);
-    }
-});
-
-__PACKAGE__->register_method ({
-    name => 'get_group',
-    path => 'groups/{name}',
-    method => 'GET',
-    description => 'Return a specific group',
-    protected => 1,
-    permissions => {
-	check => ['or',
-	    ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
-	    ['perm', '/mapping/notification/{name}', ['Mapping.Audit']],
-	],
-    },
-    parameters => {
-	additionalProperties => 0,
-	properties => {
-	    name => {
-		type => 'string',
-		format => 'pve-configid',
-	    },
-	}
-    },
-    returns => {
-	type => 'object',
-	properties => {
-	    %$group_properties,
-	    digest => get_standard_option('pve-config-digest'),
-	},
-    },
-    code => sub {
-	my ($param) = @_;
-	my $name = extract_param($param, 'name');
-
-	my $config = PVE::Notify::read_config();
-
-	my $group = eval {
-	    $config->get_group($name)
-	};
-
-	raise_api_error($@) if $@;
-	$group->{digest} = $config->digest();
-
-	return $group;
-    }
-});
-
-__PACKAGE__->register_method ({
-    name => 'create_group',
-    path => 'groups',
-    protected => 1,
-    method => 'POST',
-    description => 'Create a new group',
-    permissions => {
-	check => ['perm', '/mapping/notification', ['Mapping.Modify']],
-    },
-    parameters => {
-	additionalProperties => 0,
-	properties => $group_properties,
-    },
-    returns => { type => 'null' },
-    code => sub {
-	my ($param) = @_;
-
-	my $name = extract_param($param, 'name');
-	my $endpoint = extract_param($param, 'endpoint');
-	my $comment = extract_param($param, 'comment');
-	my $filter = extract_param($param, 'filter');
-
-	eval {
-	    PVE::Notify::lock_config(sub {
-		my $config = PVE::Notify::read_config();
-
-		$config->add_group(
-		    $name,
-		    $endpoint,
-		    $comment,
-		    $filter,
-		);
-
-		PVE::Notify::write_config($config);
-	    });
-	};
-
-	raise_api_error($@) if $@;
-	return;
-    }
-});
-
-__PACKAGE__->register_method ({
-    name => 'update_group',
-    path => 'groups/{name}',
-    protected => 1,
-    method => 'PUT',
-    description => 'Update existing group',
-    permissions => {
-	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
-    },
-    parameters => {
-	additionalProperties => 0,
-	properties => {
-	    %{ make_properties_optional($group_properties) },
-	    delete => {
-		type => 'array',
-		items => {
-		    type => 'string',
-		    format => 'pve-configid',
-		},
-		optional => 1,
-		description => 'A list of settings you want to delete.',
-	    },
-	    digest => get_standard_option('pve-config-digest'),
-	},
-    },
-    returns => { type => 'null' },
-    code => sub {
-	my ($param) = @_;
-
-	my $name = extract_param($param, 'name');
-	my $endpoint = extract_param($param, 'endpoint');
-	my $comment = extract_param($param, 'comment');
-	my $filter = extract_param($param, 'filter');
-	my $digest = extract_param($param, 'digest');
-	my $delete = extract_param($param, 'delete');
-
-	eval {
-	    PVE::Notify::lock_config(sub {
-		my $config = PVE::Notify::read_config();
-
-		$config->update_group(
-		    $name,
-		    $endpoint,
-		    $comment,
-		    $filter,
-		    $delete,
-		    $digest,
-		);
-
-		PVE::Notify::write_config($config);
-	    });
-	};
-
-	raise_api_error($@) if $@;
-	return;
-    }
-});
-
-__PACKAGE__->register_method ({
-    name => 'delete_group',
-    protected => 1,
-    path => 'groups/{name}',
-    method => 'DELETE',
-    description => 'Remove group',
-    permissions => {
-	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
-    },
-    parameters => {
-	additionalProperties => 0,
-	properties => {
-	    name => {
-		type => 'string',
-		format => 'pve-configid',
-	    },
-	}
-    },
-    returns => { type => 'null' },
-    code => sub {
-	my ($param) = @_;
-	my $name = extract_param($param, 'name');
-
-	my $used_by = target_used_by($name);
-	if ($used_by) {
-	    raise_param_exc({'name' => "Cannot remove $name, used by: $used_by"});
-	}
-
-	eval {
-	    PVE::Notify::lock_config(sub {
-		my $config = PVE::Notify::read_config();
-		$config->delete_group($name);
-		PVE::Notify::write_config($config);
-	    });
-	};
-
-	raise_api_error($@) if $@;
-	return;
-    }
-});
-
 my $sendmail_properties = {
     name => {
 	description => 'The name of the endpoint.',
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 25/52] api: notification: add new matcher-based notification API
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (23 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 24/52] api: notification: remove notification groups Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 26/52] ui: dc: remove unneeded notification events panel Lukas Wagner
                   ` (29 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This renames filters -> matchers and adds new configuration options
needed by matchers (e.g. match-field, match-calendar, etc.)

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 PVE/API2/Cluster/Notifications.pm | 195 ++++++++++++++----------------
 1 file changed, 88 insertions(+), 107 deletions(-)

diff --git a/PVE/API2/Cluster/Notifications.pm b/PVE/API2/Cluster/Notifications.pm
index b34802c8..8f716f26 100644
--- a/PVE/API2/Cluster/Notifications.pm
+++ b/PVE/API2/Cluster/Notifications.pm
@@ -68,37 +68,12 @@ sub filter_entities_by_privs {
 	    "/mapping/notification/$_->{name}",
 	    $can_see_mapping_privs,
 	    1
-	) || $_->{name} eq PVE::Notify::default_target();
+	);
     } @$entities];
 
     return $filtered;
 }
 
-sub target_used_by {
-    my ($target) = @_;
-
-    my $used_by = [];
-
-    # Check keys in datacenter.cfg
-    my $dc_conf = PVE::Cluster::cfs_read_file('datacenter.cfg');
-    for my $key (qw(target-package-updates target-replication target-fencing)) {
-	if ($dc_conf->{notify} && $dc_conf->{notify}->{$key} eq $target) {
-	    push @$used_by, $key;
-	}
-    }
-
-    # Check backup jobs
-    my $jobs_conf = PVE::Cluster::cfs_read_file('jobs.cfg');
-    for my $key (keys %{$jobs_conf->{ids}}) {
-	my $job = $jobs_conf->{ids}->{$key};
-	if ($job->{'notification-target'} eq $target) {
-	    push @$used_by, $key;
-	}
-    }
-
-    return join(', ', @$used_by);
-}
-
 __PACKAGE__->register_method ({
     name => 'index',
     path => '',
@@ -120,7 +95,7 @@ __PACKAGE__->register_method ({
     code => sub {
 	my $result = [
 	    { name => 'endpoints' },
-	    { name => 'filters' },
+	    { name => 'matchers' },
 	    { name => 'targets' },
 	];
 
@@ -259,15 +234,11 @@ __PACKAGE__->register_method ({
 
 	my $privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
 
-	if ($name ne PVE::Notify::default_target()) {
-	    # Due to backwards compatibility reasons the 'mail-to-root'
-	    # target must be accessible for any user
-	    $rpcenv->check_any(
-		$authuser,
-		"/mapping/notification/$name",
-		$privs,
-	    );
-	}
+	$rpcenv->check_any(
+	    $authuser,
+	    "/mapping/notification/$name",
+	    $privs,
+	);
 
 	eval {
 	    my $config = PVE::Notify::read_config();
@@ -319,12 +290,6 @@ my $sendmail_properties = {
 	type        => 'string',
 	optional    => 1,
     },
-    filter => {
-	description => 'Name of the filter that should be applied.',
-	type => 'string',
-	format => 'pve-configid',
-	optional => 1,
-    },
 };
 
 __PACKAGE__->register_method ({
@@ -431,7 +396,6 @@ __PACKAGE__->register_method ({
 	my $from_address = extract_param($param, 'from-address');
 	my $author = extract_param($param, 'author');
 	my $comment = extract_param($param, 'comment');
-	my $filter = extract_param($param, 'filter');
 
 	eval {
 	    PVE::Notify::lock_config(sub {
@@ -444,7 +408,6 @@ __PACKAGE__->register_method ({
 		    $from_address,
 		    $author,
 		    $comment,
-		    $filter
 		);
 
 		PVE::Notify::write_config($config);
@@ -492,7 +455,6 @@ __PACKAGE__->register_method ({
 	my $from_address = extract_param($param, 'from-address');
 	my $author = extract_param($param, 'author');
 	my $comment = extract_param($param, 'comment');
-	my $filter = extract_param($param, 'filter');
 
 	my $delete = extract_param($param, 'delete');
 	my $digest = extract_param($param, 'digest');
@@ -508,7 +470,6 @@ __PACKAGE__->register_method ({
 		    $from_address,
 		    $author,
 		    $comment,
-		    $filter,
 		    $delete,
 		    $digest,
 		);
@@ -545,11 +506,6 @@ __PACKAGE__->register_method ({
 	my ($param) = @_;
 	my $name = extract_param($param, 'name');
 
-	my $used_by = target_used_by($name);
-	if ($used_by) {
-	    raise_param_exc({'name' => "Cannot remove $name, used by: $used_by"});
-	}
-
 	eval {
 	    PVE::Notify::lock_config(sub {
 		my $config = PVE::Notify::read_config();
@@ -582,12 +538,6 @@ my $gotify_properties = {
 	type        => 'string',
 	optional    => 1,
     },
-    'filter' => {
-	description => 'Name of the filter that should be applied.',
-	type => 'string',
-	format => 'pve-configid',
-	optional => 1,
-    }
 };
 
 __PACKAGE__->register_method ({
@@ -692,7 +642,6 @@ __PACKAGE__->register_method ({
 	my $server = extract_param($param, 'server');
 	my $token = extract_param($param, 'token');
 	my $comment = extract_param($param, 'comment');
-	my $filter = extract_param($param, 'filter');
 
 	eval {
 	    PVE::Notify::lock_config(sub {
@@ -703,7 +652,6 @@ __PACKAGE__->register_method ({
 		    $server,
 		    $token,
 		    $comment,
-		    $filter
 		);
 
 		PVE::Notify::write_config($config);
@@ -748,7 +696,6 @@ __PACKAGE__->register_method ({
 	my $server = extract_param($param, 'server');
 	my $token = extract_param($param, 'token');
 	my $comment = extract_param($param, 'comment');
-	my $filter = extract_param($param, 'filter');
 
 	my $delete = extract_param($param, 'delete');
 	my $digest = extract_param($param, 'digest');
@@ -762,7 +709,6 @@ __PACKAGE__->register_method ({
 		    $server,
 		    $token,
 		    $comment,
-		    $filter,
 		    $delete,
 		    $digest,
 		);
@@ -799,11 +745,6 @@ __PACKAGE__->register_method ({
 	my ($param) = @_;
 	my $name = extract_param($param, 'name');
 
-	my $used_by = target_used_by($name);
-	if ($used_by) {
-	    raise_param_exc({'name' => "Cannot remove $name, used by: $used_by"});
-	}
-
 	eval {
 	    PVE::Notify::lock_config(sub {
 		my $config = PVE::Notify::read_config();
@@ -817,28 +758,56 @@ __PACKAGE__->register_method ({
     }
 });
 
-my $filter_properties = {
+my $matcher_properties = {
     name => {
-	description => 'Name of the endpoint.',
+	description => 'Name of the matcher.',
 	type => 'string',
 	format => 'pve-configid',
     },
-    'min-severity' => {
-	type => 'string',
-	description => 'Minimum severity to match',
+    'match-field' => {
+	type => 'array',
+	items => {
+	    type => 'string',
+	},
+	optional => 1,
+	description => 'Metadata fields to match (regex or exact match).'
+	    . ' Must be in the form (regex|exact):<field>=<value>',
+    },
+    'match-severity' => {
+	type => 'array',
+	items => {
+	    type => 'string',
+	},
+	optional => 1,
+	description => 'Notification severities to match',
+    },
+    'match-calendar' => {
+	type => 'array',
+	items => {
+	    type => 'string',
+	},
+	optional => 1,
+	description => 'Match notification timestamp',
+    },
+    'target' => {
+	type => 'array',
+	items => {
+	    type => 'string',
+	    format => 'pve-configid',
+	},
 	optional => 1,
-	enum => [qw(info notice warning error)],
+	description => 'Targets to notify on match',
     },
     mode => {
 	type => 'string',
-	description => "Choose between 'and' and 'or' for when multiple properties are specified",
+	description => "Choose between 'all' and 'any' for when multiple properties are specified",
 	optional => 1,
-	enum => [qw(and or)],
-	default => 'and',
+	enum => [qw(all any)],
+	default => 'all',
     },
     'invert-match' => {
 	type => 'boolean',
-	description => 'Invert match of the whole filter',
+	description => 'Invert match of the whole matcher',
 	optional => 1,
     },
     'comment' => {
@@ -849,10 +818,10 @@ my $filter_properties = {
 };
 
 __PACKAGE__->register_method ({
-    name => 'get_filters',
-    path => 'filters',
+    name => 'get_matchers',
+    path => 'matchers',
     method => 'GET',
-    description => 'Returns a list of all filters',
+    description => 'Returns a list of all matchers',
     protected => 1,
     permissions => {
 	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
@@ -867,7 +836,7 @@ __PACKAGE__->register_method ({
 	type => 'array',
 	items => {
 	    type => 'object',
-	    properties => $filter_properties,
+	    properties => $matcher_properties,
 	},
 	links => [ { rel => 'child', href => '{name}' } ],
     },
@@ -876,7 +845,7 @@ __PACKAGE__->register_method ({
 	my $rpcenv = PVE::RPCEnvironment::get();
 
 	my $entities = eval {
-	    $config->get_filters();
+	    $config->get_matchers();
 	};
 	raise_api_error($@) if $@;
 
@@ -885,10 +854,10 @@ __PACKAGE__->register_method ({
 });
 
 __PACKAGE__->register_method ({
-    name => 'get_filter',
-    path => 'filters/{name}',
+    name => 'get_matcher',
+    path => 'matchers/{name}',
     method => 'GET',
-    description => 'Return a specific filter',
+    description => 'Return a specific matcher',
     protected => 1,
     permissions => {
 	check => ['or',
@@ -908,7 +877,7 @@ __PACKAGE__->register_method ({
     returns => {
 	type => 'object',
 	properties => {
-	    %$filter_properties,
+	    %$matcher_properties,
 	    digest => get_standard_option('pve-config-digest'),
 	},
     },
@@ -918,37 +887,40 @@ __PACKAGE__->register_method ({
 
 	my $config = PVE::Notify::read_config();
 
-	my $filter = eval {
-	    $config->get_filter($name)
+	my $matcher = eval {
+	    $config->get_matcher($name)
 	};
 
 	raise_api_error($@) if $@;
-	$filter->{digest} = $config->digest();
+	$matcher->{digest} = $config->digest();
 
-	return $filter;
+	return $matcher;
     }
 });
 
 __PACKAGE__->register_method ({
-    name => 'create_filter',
-    path => 'filters',
+    name => 'create_matcher',
+    path => 'matchers',
     protected => 1,
     method => 'POST',
-    description => 'Create a new filter',
+    description => 'Create a new matcher',
     protected => 1,
     permissions => {
 	check => ['perm', '/mapping/notification', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
-	properties => $filter_properties,
+	properties => $matcher_properties,
     },
     returns => { type => 'null' },
     code => sub {
 	my ($param) = @_;
 
 	my $name = extract_param($param, 'name');
-	my $min_severity = extract_param($param, 'min-severity');
+	my $match_severity = extract_param($param, 'match-severity');
+	my $match_field = extract_param($param, 'match-field');
+	my $match_calendar = extract_param($param, 'match-calendar');
+	my $target = extract_param($param, 'target');
 	my $mode = extract_param($param, 'mode');
 	my $invert_match = extract_param($param, 'invert-match');
 	my $comment = extract_param($param, 'comment');
@@ -957,9 +929,12 @@ __PACKAGE__->register_method ({
 	    PVE::Notify::lock_config(sub {
 		my $config = PVE::Notify::read_config();
 
-		$config->add_filter(
+		$config->add_matcher(
 		    $name,
-		    $min_severity,
+		    $target,
+		    $match_severity,
+		    $match_field,
+		    $match_calendar,
 		    $mode,
 		    $invert_match,
 		    $comment,
@@ -975,18 +950,18 @@ __PACKAGE__->register_method ({
 });
 
 __PACKAGE__->register_method ({
-    name => 'update_filter',
-    path => 'filters/{name}',
+    name => 'update_matcher',
+    path => 'matchers/{name}',
     protected => 1,
     method => 'PUT',
-    description => 'Update existing filter',
+    description => 'Update existing matcher',
     permissions => {
 	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
 	properties => {
-	    %{ make_properties_optional($filter_properties) },
+	    %{ make_properties_optional($matcher_properties) },
 	    delete => {
 		type => 'array',
 		items => {
@@ -1004,7 +979,10 @@ __PACKAGE__->register_method ({
 	my ($param) = @_;
 
 	my $name = extract_param($param, 'name');
-	my $min_severity = extract_param($param, 'min-severity');
+	my $match_severity = extract_param($param, 'match-severity');
+	my $match_field = extract_param($param, 'match-field');
+	my $match_calendar = extract_param($param, 'match-calendar');
+	my $target = extract_param($param, 'target');
 	my $mode = extract_param($param, 'mode');
 	my $invert_match = extract_param($param, 'invert-match');
 	my $comment = extract_param($param, 'comment');
@@ -1015,9 +993,12 @@ __PACKAGE__->register_method ({
 	    PVE::Notify::lock_config(sub {
 		my $config = PVE::Notify::read_config();
 
-		$config->update_filter(
+		$config->update_matcher(
 		    $name,
-		    $min_severity,
+		    $target,
+		    $match_severity,
+		    $match_field,
+		    $match_calendar,
 		    $mode,
 		    $invert_match,
 		    $comment,
@@ -1035,11 +1016,11 @@ __PACKAGE__->register_method ({
 });
 
 __PACKAGE__->register_method ({
-    name => 'delete_filter',
+    name => 'delete_matcher',
     protected => 1,
-    path => 'filters/{name}',
+    path => 'matchers/{name}',
     method => 'DELETE',
-    description => 'Remove filter',
+    description => 'Remove matcher',
     permissions => {
 	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
     },
@@ -1060,7 +1041,7 @@ __PACKAGE__->register_method ({
 	eval {
 	    PVE::Notify::lock_config(sub {
 		my $config = PVE::Notify::read_config();
-		$config->delete_filter($name);
+		$config->delete_matcher($name);
 		PVE::Notify::write_config($config);
 	    });
 	};
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 26/52] ui: dc: remove unneeded notification events panel
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (24 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 25/52] api: notification: add new matcher-based notification API Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 27/52] vzdump: adapt to new matcher based notification system Lukas Wagner
                   ` (28 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

The notification event settings are replaced by notification matchers,
which will combine the notification routing and filtering into a
single concept.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 www/manager6/Makefile                 |   4 -
 www/manager6/dc/Config.js             |  17 +-
 www/manager6/dc/NotificationEvents.js | 276 --------------------------
 3 files changed, 2 insertions(+), 295 deletions(-)
 delete mode 100644 www/manager6/dc/NotificationEvents.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 57e1b48f..18baa024 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -159,7 +159,6 @@ JSSRC= 							\
 	dc/Health.js					\
 	dc/Log.js					\
 	dc/NodeView.js					\
-	dc/NotificationEvents.js			\
 	dc/OptionView.js				\
 	dc/PermissionView.js				\
 	dc/PoolEdit.js					\
@@ -346,6 +345,3 @@ install: pvemanagerlib.js
 .PHONY: clean
 clean:
 	rm -rf pvemanagerlib.js OnlineHelpInfo.js .lint-incremental
-
-
-
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 7d01da5f..0dea1c67 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -319,18 +319,6 @@ Ext.define('PVE.dc.Config', {
 
 	// this is being reworked, but we need to release newer manager versions already..
 	let notification_enabled = false;
-	if (notification_enabled && caps.dc['Sys.Audit']) {
-	    me.items.push(
-		{
-		    xtype: 'pveNotificationEvents',
-		    title: gettext('Notifications'),
-		    onlineHelp: 'notification_events',
-		    iconCls: 'fa fa-bell-o',
-		    itemId: 'notifications',
-		},
-	    );
-	}
-
 	if (notification_enabled && (
 		caps.mapping['Mapping.Audit'] ||
 		caps.mapping['Mapping.Use'] ||
@@ -340,12 +328,11 @@ Ext.define('PVE.dc.Config', {
 	    me.items.push(
 		{
 		    xtype: 'pmxNotificationConfigView',
-		    title: gettext('Notification Targets'),
+		    title: gettext('Notifications'),
 		    onlineHelp: 'notification_targets',
 		    itemId: 'notification-targets',
-		    iconCls: 'fa fa-dot-circle-o',
+		    iconCls: 'fa fa-bell-o',
 		    baseUrl: '/cluster/notifications',
-		    groups: ['notifications'],
 		},
 	    );
 	}
diff --git a/www/manager6/dc/NotificationEvents.js b/www/manager6/dc/NotificationEvents.js
deleted file mode 100644
index 18816290..00000000
--- a/www/manager6/dc/NotificationEvents.js
+++ /dev/null
@@ -1,276 +0,0 @@
-Ext.define('PVE.dc.NotificationEventsPolicySelector', {
-    alias: ['widget.pveNotificationEventsPolicySelector'],
-    extend: 'Proxmox.form.KVComboBox',
-    deleteEmpty: false,
-    value: '__default__',
-
-    config: {
-	warningRef: null,
-	warnIfValIs: null,
-    },
-
-    listeners: {
-	change: function(field, newValue) {
-	    let me = this;
-	    if (!me.warningRef && !me.warnIfValIs) {
-		return;
-	    }
-
-	    let warningField = field.nextSibling(
-		`displayfield[reference=${me.warningRef}]`,
-	    );
-	    warningField.setVisible(newValue === me.warnIfValIs);
-	},
-    },
-});
-
-Ext.define('PVE.dc.NotificationEventDisabledWarning', {
-    alias: ['widget.pveNotificationEventDisabledWarning'],
-    extend: 'Ext.form.field.Display',
-    userCls: 'pmx-hint',
-    hidden: true,
-    value: gettext('Disabling notifications is not recommended for production systems!'),
-});
-
-Ext.define('PVE.dc.NotificationEventsTargetSelector', {
-    alias: ['widget.pveNotificationEventsTargetSelector'],
-    extend: 'PVE.form.NotificationTargetSelector',
-    fieldLabel: gettext('Notification Target'),
-    allowBlank: true,
-    editable: true,
-    autoSelect: false,
-    deleteEmpty: false,
-    emptyText: `${Proxmox.Utils.defaultText} (mail-to-root)`,
-});
-
-Ext.define('PVE.dc.NotificationEvents', {
-    extend: 'Proxmox.grid.ObjectGrid',
-    alias: ['widget.pveNotificationEvents'],
-
-    // Taken from OptionView.js, but adapted slightly.
-    // The modified version allows us to have multiple rows in the ObjectGrid
-    // for the same underlying property (notify).
-    // Every setting is eventually stored as a property string in the
-    // notify key of datacenter.cfg.
-    // When updating 'notify', all properties that were already set
-    // also have to be submitted, even if they were not modified.
-    // This means that we need to save the old value somewhere.
-    addInputPanelRow: function(name, propertyName, text, opts) {
-	let me = this;
-
-	opts = opts || {};
-	me.rows = me.rows || {};
-
-	me.rows[name] = {
-	    required: true,
-	    defaultValue: opts.defaultValue,
-	    header: text,
-	    renderer: opts.renderer,
-	    name: propertyName,
-	    editor: {
-		xtype: 'proxmoxWindowEdit',
-		width: opts.width || 400,
-		subject: text,
-		onlineHelp: opts.onlineHelp,
-		fieldDefaults: {
-		    labelWidth: opts.labelWidth || 150,
-		},
-		setValues: function(values) {
-		    let value = values[propertyName];
-
-		    if (opts.parseBeforeSet) {
-			value = PVE.Parser.parsePropertyString(value);
-		    }
-
-		    Ext.Array.each(this.query('inputpanel'), function(panel) {
-			panel.setValues(value);
-
-			// Save the original value
-			panel.originalValue = {
-			    ...value,
-			};
-		    });
-		},
-		url: opts.url,
-		items: [{
-		    xtype: 'inputpanel',
-		    onGetValues: function(values) {
-			let fields = this.config.items.map(field => field.name).filter(n => n);
-
-			// Restore old, unchanged values
-			for (const [key, value] of Object.entries(this.originalValue)) {
-			    if (!fields.includes(key)) {
-				values[key] = value;
-			    }
-			}
-
-			let value = {};
-			if (Object.keys(values).length > 0) {
-			    value[propertyName] = PVE.Parser.printPropertyString(values);
-			} else {
-			    Proxmox.Utils.assemble_field_data(value, { 'delete': propertyName });
-			}
-
-			return value;
-		    },
-		    items: opts.items,
-		}],
-	    },
-	};
-    },
-
-    initComponent: function() {
-	let me = this;
-
-	// Helper function for rendering the property
-	// Needed since the actual value is always stored in the 'notify' property
-	let render_value = (store, target_key, mode_key, default_val) => {
-	    let value = store.getById('notify')?.get('value') ?? {};
-	    let target = value[target_key] ?? 'mail-to-root';
-	    let template;
-
-	    switch (value[mode_key]) {
-		case 'always':
-		    template = gettext('Always, notify via target \'{0}\'');
-		    break;
-		case 'never':
-		    template = gettext('Never');
-		    break;
-		case 'auto':
-		    template = gettext('Automatically, notify via target \'{0}\'');
-		    break;
-		default:
-		    template = gettext('{1} ({2}), notify via target \'{0}\'');
-		    break;
-	    }
-
-	    return Ext.String.format(template, target, Proxmox.Utils.defaultText, default_val);
-	};
-
-	me.addInputPanelRow('fencing', 'notify', gettext('Node Fencing'), {
-	    renderer: (value, metaData, record, rowIndex, colIndex, store) =>
-		render_value(store, 'target-fencing', 'fencing', gettext('Always')),
-	    url: "/api2/extjs/cluster/options",
-	    items: [
-		{
-		    xtype: 'pveNotificationEventsPolicySelector',
-		    name: 'fencing',
-		    fieldLabel: gettext('Notify'),
-		    comboItems: [
-			['__default__', `${Proxmox.Utils.defaultText} (${gettext('Always')})`],
-			['always', gettext('Always')],
-			['never', gettext('Never')],
-		    ],
-		    warningRef: 'warning',
-		    warnIfValIs: 'never',
-		},
-		{
-		    xtype: 'pveNotificationEventsTargetSelector',
-		    name: 'target-fencing',
-		},
-		{
-		    xtype: 'pveNotificationEventDisabledWarning',
-		    reference: 'warning',
-		},
-	    ],
-	});
-
-	me.addInputPanelRow('replication', 'notify', gettext('Replication'), {
-	    renderer: (value, metaData, record, rowIndex, colIndex, store) =>
-		render_value(store, 'target-replication', 'replication', gettext('Always')),
-	    url: "/api2/extjs/cluster/options",
-	    items: [
-		{
-		    xtype: 'pveNotificationEventsPolicySelector',
-		    name: 'replication',
-		    fieldLabel: gettext('Notify'),
-		    comboItems: [
-			['__default__', `${Proxmox.Utils.defaultText} (${gettext('Always')})`],
-			['always', gettext('Always')],
-			['never', gettext('Never')],
-		    ],
-		    warningRef: 'warning',
-		    warnIfValIs: 'never',
-		},
-		{
-		    xtype: 'pveNotificationEventsTargetSelector',
-		    name: 'target-replication',
-		},
-		{
-		    xtype: 'pveNotificationEventDisabledWarning',
-		    reference: 'warning',
-		},
-	    ],
-	});
-
-	me.addInputPanelRow('updates', 'notify', gettext('Package Updates'), {
-	    renderer: (value, metaData, record, rowIndex, colIndex, store) =>
-		render_value(
-		    store,
-		    'target-package-updates',
-		    'package-updates',
-		    gettext('Automatically'),
-		),
-	    url: "/api2/extjs/cluster/options",
-	    items: [
-		{
-		    xtype: 'pveNotificationEventsPolicySelector',
-		    name: 'package-updates',
-		    fieldLabel: gettext('Notify'),
-		    comboItems: [
-			[
-			    '__default__',
-			    `${Proxmox.Utils.defaultText} (${gettext('Automatically')})`,
-			],
-			['auto', gettext('Automatically')],
-			['always', gettext('Always')],
-			['never', gettext('Never')],
-		    ],
-		    warningRef: 'warning',
-		    warnIfValIs: 'never',
-		},
-		{
-		    xtype: 'pveNotificationEventsTargetSelector',
-		    name: 'target-package-updates',
-		},
-		{
-		    xtype: 'pveNotificationEventDisabledWarning',
-		    reference: 'warning',
-		},
-	    ],
-	});
-
-	// Hack: Also load the notify property to make it accessible
-	// for our render functions.
-	me.rows.notify = {
-	    visible: false,
-	};
-
-	me.selModel = Ext.create('Ext.selection.RowModel', {});
-
-	Ext.apply(me, {
-	    tbar: [{
-		text: gettext('Edit'),
-		xtype: 'proxmoxButton',
-		disabled: true,
-		handler: () => me.run_editor(),
-		selModel: me.selModel,
-	    }],
-	    url: "/api2/json/cluster/options",
-	    editorConfig: {
-		url: "/api2/extjs/cluster/options",
-	    },
-	    interval: 5000,
-	    cwidth1: 200,
-	    listeners: {
-		itemdblclick: me.run_editor,
-	    },
-	});
-
-	me.callParent();
-
-	me.on('activate', me.rstore.startUpdate);
-	me.on('destroy', me.rstore.stopUpdate);
-	me.on('deactivate', me.rstore.stopUpdate);
-    },
-});
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 27/52] vzdump: adapt to new matcher based notification system
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (25 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 26/52] ui: dc: remove unneeded notification events panel Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 28/52] api: apt: adapt to matcher-based notifications Lukas Wagner
                   ` (27 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

To ease the migration from old-style mailto/mailnotification paramters
for backup jobs, the code will add a ephemeral sendmail endpoint and
a matcher.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 PVE/API2/VZDump.pm |  8 +-------
 PVE/VZDump.pm      | 40 +++++++++++++++++++---------------------
 2 files changed, 20 insertions(+), 28 deletions(-)

diff --git a/PVE/API2/VZDump.pm b/PVE/API2/VZDump.pm
index 3886772e..f66fc740 100644
--- a/PVE/API2/VZDump.pm
+++ b/PVE/API2/VZDump.pm
@@ -44,9 +44,7 @@ __PACKAGE__->register_method ({
 	    ."'Datastore.AllocateSpace' on the backup storage. The 'tmpdir', 'dumpdir' and "
 	    ."'script' parameters are restricted to the 'root\@pam' user. The 'maxfiles' and "
 	    ."'prune-backups' settings require 'Datastore.Allocate' on the backup storage. The "
-	    ."'bwlimit', 'performance' and 'ionice' parameters require 'Sys.Modify' on '/'. "
-	    ."If 'notification-target' is set, then the 'Mapping.Use' permission is needed on "
-	    ."'/mapping/notification/<target>'.",
+	    ."'bwlimit', 'performance' and 'ionice' parameters require 'Sys.Modify' on '/'. ",
 	user => 'all',
     },
     protected => 1,
@@ -115,10 +113,6 @@ __PACKAGE__->register_method ({
 	    $rpcenv->check($user, "/storage/$storeid", [ 'Datastore.AllocateSpace' ]);
 	}
 
-	if (my $target = $param->{'notification-target'}) {
-	    PVE::Notify::check_may_use_target($target, $rpcenv);
-	}
-
 	my $worker = sub {
 	    my $upid = shift;
 
diff --git a/PVE/VZDump.pm b/PVE/VZDump.pm
index 454ab494..b0574d41 100644
--- a/PVE/VZDump.pm
+++ b/PVE/VZDump.pm
@@ -452,20 +452,18 @@ sub send_notification {
     my $opts = $self->{opts};
     my $mailto = $opts->{mailto};
     my $cmdline = $self->{cmdline};
-    my $target = $opts->{"notification-target"};
-    # Fall back to 'mailnotification' if 'notification-policy' is not set.
-    # If both are set, 'notification-policy' takes precedence
-    my $policy = $opts->{"notification-policy"} // $opts->{mailnotification} // 'always';
+    # Old-style notification policy. This parameter will influce
+    # if an ad-hoc notification target/matcher will be created.
+    my $policy = $opts->{"notification-policy"} //
+	$opts->{mailnotification} //
+	'always';
 
-    return if ($policy eq 'never');
 
     sanitize_task_list($tasklist);
     my $error_count = count_failed_tasks($tasklist);
 
     my $failed = ($error_count || $err);
 
-    return if (!$failed && ($policy eq 'failure'));
-
     my $status_text = $failed ? 'backup failed' : 'backup successful';
 
     if ($err) {
@@ -489,8 +487,10 @@ sub send_notification {
 	    "See Task History for details!\n";
     };
 
+    my $hostname = get_hostname();
+
     my $notification_props = {
-	"hostname"      => get_hostname(),
+	"hostname"      => $hostname,
 	"error-message" => $err,
 	"guest-table"   => build_guest_table($tasklist),
 	"logs"          => $text_log_part,
@@ -498,9 +498,16 @@ sub send_notification {
 	"total-time"    => $total_time,
     };
 
+    my $fields = {
+	type => "vzdump",
+	hostname => $hostname,
+    };
+
     my $notification_config = PVE::Notify::read_config();
 
-    if ($mailto && scalar(@$mailto)) {
+    my $legacy_sendmail = $policy eq "always" || ($policy eq "failure" && $failed);
+
+    if ($mailto && scalar(@$mailto) && $legacy_sendmail) {
 	# <, >, @ are not allowed in endpoint names, but that is only
 	# verified once the config is serialized. That means that
 	# we can rely on that fact that no other endpoint with this name exists.
@@ -514,29 +521,20 @@ sub send_notification {
 
 	my $endpoints = [$endpoint_name];
 
-	# Create an anonymous group containing the sendmail endpoint and the
-	# $target endpoint, if specified
-	if ($target) {
-	    push @$endpoints, $target;
-	}
-
-	$target = "<group-$endpoint_name>";
-	$notification_config->add_group(
-	    $target,
+	$notification_config->add_matcher(
+	    "<matcher-$endpoint_name>",
 	    $endpoints,
 	);
     }
 
-    return if (!$target);
-
     my $severity = $failed ? "error" : "info";
 
     PVE::Notify::notify(
-	$target,
 	$severity,
 	$subject_template,
 	$body_template,
 	$notification_props,
+	$fields,
 	$notification_config
     );
 };
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 28/52] api: apt: adapt to matcher-based notifications
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (26 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 27/52] vzdump: adapt to new matcher based notification system Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 29/52] api: replication: adapt to matcher-based notification system Lukas Wagner
                   ` (26 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 PVE/API2/APT.pm | 27 +++++++++++----------------
 1 file changed, 11 insertions(+), 16 deletions(-)

diff --git a/PVE/API2/APT.pm b/PVE/API2/APT.pm
index a213fc59..da75a4dc 100644
--- a/PVE/API2/APT.pm
+++ b/PVE/API2/APT.pm
@@ -286,8 +286,6 @@ __PACKAGE__->register_method({
     description => "This is used to resynchronize the package index files from their sources (apt-get update).",
     permissions => {
 	check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
-	description => "If 'notify: target-package-updates' is set, then the user must have the "
-	    . "'Mapping.Use' permission on '/mapping/notification/<target>'",
     },
     protected => 1,
     proxyto => 'node',
@@ -297,7 +295,7 @@ __PACKAGE__->register_method({
 	    node => get_standard_option('pve-node'),
 	    notify => {
 		type => 'boolean',
-		description => "Send notification mail about new packages (to email address specified for user 'root\@pam').",
+		description => "Send notification about new packages.",
 		optional => 1,
 		default => 0,
 	    },
@@ -317,16 +315,6 @@ __PACKAGE__->register_method({
 
 	my $rpcenv = PVE::RPCEnvironment::get();
 	my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
-	my $target = $dcconf->{notify}->{'target-package-updates'} //
-	    PVE::Notify::default_target();
-
-	if ($param->{notify} && $target ne PVE::Notify::default_target()) {
-	    # If we notify via anything other than the default target (mail to root),
-	    # then the user must have the proper permissions for the target.
-	    # The mail-to-root target does not require these, as otherwise
-	    # we would break compatibility.
-	    PVE::Notify::check_may_use_target($target, $rpcenv);
-	}
 
 	my $authuser = $rpcenv->get_user();
 
@@ -392,16 +380,23 @@ __PACKAGE__->register_method({
 
 		return if !$count;
 
-		my $properties = {
+		my $template_data = {
 		    updates  => $updates_table,
 		    hostname => $hostname,
 		};
 
+		# Additional metadata fields that can be used in notification
+		# matchers.
+		my $metadata_fields = {
+		    type => 'package-updates',
+		    hostname => $hostname,
+		};
+
 		PVE::Notify::info(
-		    $target,
 		    $updates_available_subject_template,
 		    $updates_available_body_template,
-		    $properties,
+		    $template_data,
+		    $metadata_fields,
 		);
 
 		foreach my $pi (@$pkglist) {
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 29/52] api: replication: adapt to matcher-based notification system
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (27 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 28/52] api: apt: adapt to matcher-based notifications Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 30/52] test: fix vzdump notification test Lukas Wagner
                   ` (25 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 PVE/API2/Replication.pm | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/PVE/API2/Replication.pm b/PVE/API2/Replication.pm
index d61518ba..0dc944c9 100644
--- a/PVE/API2/Replication.pm
+++ b/PVE/API2/Replication.pm
@@ -129,7 +129,7 @@ my sub _handle_job_err {
     # The replication job is run every 15 mins if no schedule is set.
     my $schedule = $job->{schedule} // '*/15';
 
-    my $properties = {
+    my $template_data = {
 	"failure-count" => $fail_count,
 	"last-sync"     => $jobstate->{last_sync},
 	"next-sync"     => $next_sync,
@@ -139,19 +139,18 @@ my sub _handle_job_err {
 	"error"         => $err,
     };
 
+    my $metadata_fields = {
+	# TODO: Add job-id?
+	type => "replication",
+    };
+
     eval {
-	my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
-	my $target = $dcconf->{notify}->{'target-replication'} // PVE::Notify::default_target();
-	my $notify = $dcconf->{notify}->{'replication'} // 'always';
-
-	if ($notify eq 'always') {
-	    PVE::Notify::error(
-		$target,
-		$replication_error_subject_template,
-		$replication_error_body_template,
-		$properties
-	    );
-	}
+	PVE::Notify::error(
+	    $replication_error_subject_template,
+	    $replication_error_body_template,
+	    $template_data,
+	    $metadata_fields
+	);
 
     };
     warn ": $@" if $@;
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 30/52] test: fix vzdump notification test
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (28 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 29/52] api: replication: adapt to matcher-based notification system Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 31/52] ui: vzdump: remove left-overs from target/policy based notifications Lukas Wagner
                   ` (24 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

The signature of the PVE::Notify functions have changed, this commit
adapts the mocked functions so that the tests work again.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 test/vzdump_notification_test.pl | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/vzdump_notification_test.pl b/test/vzdump_notification_test.pl
index 21c31651..631606bb 100755
--- a/test/vzdump_notification_test.pl
+++ b/test/vzdump_notification_test.pl
@@ -38,14 +38,14 @@ my $result_properties;
 
 my $mock_notification_module = Test::MockModule->new('PVE::Notify');
 my $mocked_notify = sub {
-    my ($channel, $severity, $title, $text, $properties) = @_;
+    my ($severity, $title, $text, $properties, $metadata) = @_;
 
     $result_text = $text;
     $result_properties = $properties;
 };
 my $mocked_notify_short = sub {
-    my ($channel, @rest) = @_;
-    return $mocked_notify->($channel, '<some severity>', @rest);
+    my (@params) = @_;
+    return $mocked_notify->('<some severity>', @params);
 };
 
 $mock_notification_module->mock(
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 31/52] ui: vzdump: remove left-overs from target/policy based notifications
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (29 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 30/52] test: fix vzdump notification test Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-16 11:52   ` [pve-devel] [PATCH manager] ui: fix backup job create Dominik Csapak
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 32/52] ui: dc: config: show notification panel again Lukas Wagner
                   ` (23 subsequent siblings)
  54 siblings, 1 reply; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 www/manager6/dc/Backup.js                     | 81 ++++---------------
 .../form/NotificationPolicySelector.js        |  1 -
 www/manager6/window/Backup.js                 | 35 +-------
 3 files changed, 15 insertions(+), 102 deletions(-)

diff --git a/www/manager6/dc/Backup.js b/www/manager6/dc/Backup.js
index 0c8d2d4f..e1c76a1d 100644
--- a/www/manager6/dc/Backup.js
+++ b/www/manager6/dc/Backup.js
@@ -36,29 +36,11 @@ Ext.define('PVE.dc.BackupEdit', {
 		delete values.node;
 	    }
 
-	    if (!isCreate) {
-		// 'mailnotification' is deprecated in favor of 'notification-policy'
-		// -> Migration to the new parameter happens in init, so we are
-		//    safe to remove the old parameter here.
-		Proxmox.Utils.assemble_field_data(values, { 'delete': 'mailnotification' });
-
-		// If sending notifications via mail, remove the current value of
-		// 'notification-target'
-		if (values['notification-mode'] === "mailto") {
-		    Proxmox.Utils.assemble_field_data(
-			values,
-			{ 'delete': 'notification-target' },
-		    );
-		} else {
-		    // and vice versa...
-		    Proxmox.Utils.assemble_field_data(
-			values,
-			{ 'delete': 'mailto' },
-		    );
-		}
-	    }
-
-	    delete values['notification-mode'];
+	    // Get rid of new-old parameters for notification settings.
+	    // These should only be set for those selected few who ran
+	    // pve-manager from pvetest.
+	    Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-policy' });
+	    Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-target' });
 
 	    if (!values.id && isCreate) {
 		values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
@@ -170,20 +152,14 @@ Ext.define('PVE.dc.BackupEdit', {
 		    success: function(response, _options) {
 			let data = response.result.data;
 
-			// 'mailnotification' is deprecated. Let's automatically
-			// migrate to the compatible 'notification-policy' parameter
-			if (data.mailnotification) {
-			    if (!data["notification-policy"]) {
-				data["notification-policy"] = data.mailnotification;
-			    }
-
-			    delete data.mailnotification;
-			}
-
-			if (data['notification-target']) {
-			    data['notification-mode'] = 'notification-target';
-			} else if (data.mailto) {
-			    data['notification-mode'] = 'mailto';
+			// Migrate 'new'-old notification-policy back to
+			// old-old mailnotification. Only should affect
+			// users who used pve-manager from pvetest.
+			// This was a remnant of notifications before the
+			// overhaul.
+			let policy = data['notification-policy'];
+			if (policy === 'always' || policy === 'failure') {
+			    data.mailnotification = policy;
 			}
 
 			if (data.exclude) {
@@ -228,7 +204,6 @@ Ext.define('PVE.dc.BackupEdit', {
     viewModel: {
 	data: {
 	    selMode: 'include',
-	    notificationMode: 'notification-target',
 	},
 
 	formulas: {
@@ -327,44 +302,16 @@ Ext.define('PVE.dc.BackupEdit', {
 				{
 				    xtype: 'pveEmailNotificationSelector',
 				    fieldLabel: gettext('Notify'),
-				    name: 'notification-policy',
+				    name: 'mailnotification',
 				    cbind: {
 					value: (get) => get('isCreate') ? 'always' : '',
 					deleteEmpty: '{!isCreate}',
 				    },
 				},
-				{
-				    xtype: 'pveNotificationModeSelector',
-				    fieldLabel: gettext('Notify via'),
-				    name: 'notification-mode',
-				    bind: {
-					value: '{notificationMode}',
-				    },
-				},
-				{
-				    xtype: 'pveNotificationTargetSelector',
-				    fieldLabel: gettext('Notification Target'),
-				    name: 'notification-target',
-				    allowBlank: true,
-				    editable: true,
-				    autoSelect: false,
-				    bind: {
-					hidden: '{mailNotificationSelected}',
-					disabled: '{mailNotificationSelected}',
-				    },
-				    cbind: {
-					deleteEmpty: '{!isCreate}',
-				    },
-				},
 				{
 				    xtype: 'textfield',
 				    fieldLabel: gettext('Send email to'),
 				    name: 'mailto',
-				    hidden: true,
-				    bind: {
-					hidden: '{!mailNotificationSelected}',
-					disabled: '{!mailNotificationSelected}',
-				    },
 				},
 				{
 				    xtype: 'pveBackupCompressionSelector',
diff --git a/www/manager6/form/NotificationPolicySelector.js b/www/manager6/form/NotificationPolicySelector.js
index 68087275..f318ea18 100644
--- a/www/manager6/form/NotificationPolicySelector.js
+++ b/www/manager6/form/NotificationPolicySelector.js
@@ -4,6 +4,5 @@ Ext.define('PVE.form.EmailNotificationSelector', {
     comboItems: [
 	['always', gettext('Notify always')],
 	['failure', gettext('On failure only')],
-	['never', gettext('Notify never')],
     ],
 });
diff --git a/www/manager6/window/Backup.js b/www/manager6/window/Backup.js
index 8e6fa77e..8d8c9ff0 100644
--- a/www/manager6/window/Backup.js
+++ b/www/manager6/window/Backup.js
@@ -30,32 +30,12 @@ Ext.define('PVE.window.Backup', {
 	    name: 'mode',
 	});
 
-	let notificationTargetSelector = Ext.create('PVE.form.NotificationTargetSelector', {
-	    fieldLabel: gettext('Notification target'),
-	    name: 'notification-target',
-	    emptyText: Proxmox.Utils.noneText,
-	    hidden: true,
-	});
-
 	let mailtoField = Ext.create('Ext.form.field.Text', {
 	    fieldLabel: gettext('Send email to'),
 	    name: 'mailto',
 	    emptyText: Proxmox.Utils.noneText,
 	});
 
-	let notificationModeSelector = Ext.create('PVE.form.NotificationModeSelector', {
-	    fieldLabel: gettext('Notify via'),
-	    value: 'mailto',
-	    name: 'notification-mode',
-	    listeners: {
-		change: function(f, v) {
-		    let mailSelected = v === 'mailto';
-		    notificationTargetSelector.setHidden(mailSelected);
-		    mailtoField.setHidden(!mailSelected);
-		},
-	    },
-	});
-
 	const keepNames = [
 	    ['keep-last', gettext('Keep Last')],
 	    ['keep-hourly', gettext('Keep Hourly')],
@@ -127,12 +107,6 @@ Ext.define('PVE.window.Backup', {
 			success: function(response, opts) {
 			    const data = response.result.data;
 
-			    if (!initialDefaults && data['notification-mode'] !== undefined) {
-				notificationModeSelector.setValue(data['notification-mode']);
-			    }
-			    if (!initialDefaults && data['notification-channel'] !== undefined) {
-				notificationTargetSelector.setValue(data['notification-channel']);
-			    }
 			    if (!initialDefaults && data.mailto !== undefined) {
 				mailtoField.setValue(data.mailto);
 			    }
@@ -202,8 +176,6 @@ Ext.define('PVE.window.Backup', {
 	    ],
 	    column2: [
 		compressionSelector,
-		notificationModeSelector,
-		notificationTargetSelector,
 		mailtoField,
 		removeCheckbox,
 	    ],
@@ -280,15 +252,10 @@ Ext.define('PVE.window.Backup', {
 		    remove: values.remove,
 		};
 
-		if (values.mailto && values['notification-mode'] === 'mailto') {
+		if (values.mailto) {
 		    params.mailto = values.mailto;
 		}
 
-		if (values['notification-target'] &&
-		    values['notification-mode'] === 'notification-target') {
-		    params['notification-target'] = values['notification-target'];
-		}
-
 		if (values.compress) {
 		    params.compress = values.compress;
 		}
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 32/52] ui: dc: config: show notification panel again
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (30 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 31/52] ui: vzdump: remove left-overs from target/policy based notifications Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 33/52] notify: add API routes for smtp endpoints Lukas Wagner
                   ` (22 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Rework should be done now.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 www/manager6/dc/Config.js | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 0dea1c67..74a84e91 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -317,14 +317,9 @@ Ext.define('PVE.dc.Config', {
 	    );
 	}
 
-	// this is being reworked, but we need to release newer manager versions already..
-	let notification_enabled = false;
-	if (notification_enabled && (
-		caps.mapping['Mapping.Audit'] ||
-		caps.mapping['Mapping.Use'] ||
-		caps.mapping['Mapping.Modify']
-	    )
-	) {
+	if (caps.mapping['Mapping.Audit'] ||
+	    caps.mapping['Mapping.Use'] ||
+	    caps.mapping['Mapping.Modify']) {
 	    me.items.push(
 		{
 		    xtype: 'pmxNotificationConfigView',
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 33/52] notify: add API routes for smtp endpoints
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (31 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 32/52] ui: dc: config: show notification panel again Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 34/52] api: notification: add disable and origin params Lukas Wagner
                   ` (21 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

The Perl part of the API methods primarily defines the API schema,
checks for any needed privileges and then calls the actual Rust
implementation exposed via perlmod. Any errors returned by the Rust
code are translated into PVE::Exception, so that the API call fails
with the correct HTTP error code.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 PVE/API2/Cluster/Notifications.pm | 323 ++++++++++++++++++++++++++++++
 1 file changed, 323 insertions(+)

diff --git a/PVE/API2/Cluster/Notifications.pm b/PVE/API2/Cluster/Notifications.pm
index 8f716f26..42207aaa 100644
--- a/PVE/API2/Cluster/Notifications.pm
+++ b/PVE/API2/Cluster/Notifications.pm
@@ -194,6 +194,14 @@ __PACKAGE__->register_method ({
 		};
 	    }
 
+	    for my $target (@{$config->get_smtp_endpoints()}) {
+		push @$result, {
+		    name => $target->{name},
+		    comment => $target->{comment},
+		    type => 'smtp',
+		};
+	    }
+
 	    $result
 	};
 
@@ -758,6 +766,321 @@ __PACKAGE__->register_method ({
     }
 });
 
+my $smtp_properties= {
+    name => {
+	description => 'The name of the endpoint.',
+	type => 'string',
+	format => 'pve-configid',
+    },
+    server => {
+	description => 'The address of the SMTP server.',
+	type => 'string',
+    },
+    port => {
+	description => 'The port to be used. Defaults to 465 for TLS based connections,'
+	    . ' 587 for STARTTLS based connections and port 25 for insecure plain-text'
+	    . ' connections.',
+	type => 'integer',
+	optional => 1,
+    },
+    mode => {
+	description => 'Determine which encryption method shall be used for the connection.',
+	type => 'string',
+	enum => [ qw(insecure starttls tls) ],
+	default => 'tls',
+	optional => 1,
+    },
+    username => {
+	description => 'Username for SMTP authentication',
+	type => 'string',
+	optional => 1,
+    },
+    password => {
+	description => 'Password for SMTP authentication',
+	type => 'string',
+	optional => 1,
+    },
+    mailto => {
+	type => 'array',
+	items => {
+	    type => 'string',
+	    format => 'email-or-username',
+	},
+	description => 'List of email recipients',
+	optional => 1,
+    },
+    'mailto-user' => {
+	type => 'array',
+	items => {
+	    type => 'string',
+	    format => 'pve-userid',
+	},
+	description => 'List of users',
+	optional => 1,
+    },
+    'from-address' => {
+	description => '`From` address for the mail',
+	type => 'string',
+    },
+    author => {
+	description => 'Author of the mail. Defaults to \'Proxmox VE\'.',
+	type => 'string',
+	optional => 1,
+    },
+    'comment' => {
+	description => 'Comment',
+	type        => 'string',
+	optional    => 1,
+    },
+};
+
+__PACKAGE__->register_method ({
+    name => 'get_smtp_endpoints',
+    path => 'endpoints/smtp',
+    method => 'GET',
+    description => 'Returns a list of all smtp endpoints',
+    permissions => {
+	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
+	    . " 'Mapping.Audit' permissions on '/mapping/notification/targets/<name>'.",
+	user => 'all',
+    },
+    protected => 1,
+    parameters => {
+	additionalProperties => 0,
+	properties => {},
+    },
+    returns => {
+	type => 'array',
+	items => {
+	    type => 'object',
+	    properties => $smtp_properties,
+	},
+	links => [ { rel => 'child', href => '{name}' } ],
+    },
+    code => sub {
+	my $config = PVE::Notify::read_config();
+	my $rpcenv = PVE::RPCEnvironment::get();
+
+	my $entities = eval {
+	    $config->get_smtp_endpoints();
+	};
+	raise_api_error($@) if $@;
+
+	return filter_entities_by_privs($rpcenv, "targets", $entities);
+    }
+});
+
+__PACKAGE__->register_method ({
+    name => 'get_smtp_endpoint',
+    path => 'endpoints/smtp/{name}',
+    method => 'GET',
+    description => 'Return a specific smtp endpoint',
+    permissions => {
+	check => ['or',
+	    ['perm', '/mapping/notification/targets/{name}', ['Mapping.Modify']],
+	    ['perm', '/mapping/notification/targets/{name}', ['Mapping.Audit']],
+	],
+    },
+    protected => 1,
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    name => {
+		type => 'string',
+		format => 'pve-configid',
+	    },
+	}
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    %{ remove_protected_properties($smtp_properties, ['password']) },
+	    digest => get_standard_option('pve-config-digest'),
+	}
+
+    },
+    code => sub {
+	my ($param) = @_;
+	my $name = extract_param($param, 'name');
+
+	my $config = PVE::Notify::read_config();
+	my $endpoint = eval {
+	    $config->get_smtp_endpoint($name)
+	};
+
+	raise_api_error($@) if $@;
+	$endpoint->{digest} = $config->digest();
+
+	return $endpoint;
+    }
+});
+
+__PACKAGE__->register_method ({
+    name => 'create_smtp_endpoint',
+    path => 'endpoints/smtp',
+    protected => 1,
+    method => 'POST',
+    description => 'Create a new smtp endpoint',
+    permissions => {
+	check => ['perm', '/mapping/notification/targets', ['Mapping.Modify']],
+    },
+    parameters => {
+	additionalProperties => 0,
+	properties => $smtp_properties,
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	my $name = extract_param($param, 'name');
+	my $server = extract_param($param, 'server');
+	my $port = extract_param($param, 'port');
+	my $mode = extract_param($param, 'mode');
+	my $username = extract_param($param, 'username');
+	my $password = extract_param($param, 'password');
+	my $mailto = extract_param($param, 'mailto');
+	my $mailto_user = extract_param($param, 'mailto-user');
+	my $from_address = extract_param($param, 'from-address');
+	my $author = extract_param($param, 'author');
+	my $comment = extract_param($param, 'comment');
+
+	eval {
+	    PVE::Notify::lock_config(sub {
+		my $config = PVE::Notify::read_config();
+
+		$config->add_smtp_endpoint(
+		    $name,
+		    $server,
+		    $port,
+		    $mode,
+		    $username,
+		    $password,
+		    $mailto,
+		    $mailto_user,
+		    $from_address,
+		    $author,
+		    $comment,
+		);
+
+		PVE::Notify::write_config($config);
+	    });
+	};
+
+	raise_api_error($@) if $@;
+	return;
+    }
+});
+
+__PACKAGE__->register_method ({
+    name => 'update_smtp_endpoint',
+    path => 'endpoints/smtp/{name}',
+    protected => 1,
+    method => 'PUT',
+    description => 'Update existing smtp endpoint',
+    permissions => {
+	check => ['perm', '/mapping/notification/targets/{name}', ['Mapping.Modify']],
+    },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    %{ make_properties_optional($smtp_properties) },
+	    delete => {
+		type => 'array',
+		items => {
+		    type => 'string',
+		    format => 'pve-configid',
+		},
+		optional => 1,
+		description => 'A list of settings you want to delete.',
+	    },
+	    digest => get_standard_option('pve-config-digest'),
+
+	}
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	my $name = extract_param($param, 'name');
+	my $server = extract_param($param, 'server');
+	my $port = extract_param($param, 'port');
+	my $mode = extract_param($param, 'mode');
+	my $username = extract_param($param, 'username');
+	my $password = extract_param($param, 'password');
+	my $mailto = extract_param($param, 'mailto');
+	my $mailto_user = extract_param($param, 'mailto-user');
+	my $from_address = extract_param($param, 'from-address');
+	my $author = extract_param($param, 'author');
+	my $comment = extract_param($param, 'comment');
+
+	my $delete = extract_param($param, 'delete');
+	my $digest = extract_param($param, 'digest');
+
+	eval {
+	    PVE::Notify::lock_config(sub {
+		my $config = PVE::Notify::read_config();
+
+		$config->update_smtp_endpoint(
+		    $name,
+		    $server,
+		    $port,
+		    $mode,
+		    $username,
+		    $password,
+		    $mailto,
+		    $mailto_user,
+		    $from_address,
+		    $author,
+		    $comment,
+		    $delete,
+		    $digest,
+		);
+
+		PVE::Notify::write_config($config);
+	    });
+	};
+
+	raise_api_error($@) if $@;
+	return;
+    }
+});
+
+__PACKAGE__->register_method ({
+    name => 'delete_smtp_endpoint',
+    protected => 1,
+    path => 'endpoints/smtp/{name}',
+    method => 'DELETE',
+    description => 'Remove smtp endpoint',
+    permissions => {
+	check => ['perm', '/mapping/notification/targets/{name}', ['Mapping.Modify']],
+    },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    name => {
+		type => 'string',
+		format => 'pve-configid',
+	    },
+	}
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+	my $name = extract_param($param, 'name');
+
+	eval {
+	    PVE::Notify::lock_config(sub {
+		my $config = PVE::Notify::read_config();
+		$config->delete_smtp_endpoint($name);
+		PVE::Notify::write_config($config);
+	    });
+	};
+
+	raise_api_error($@) if ($@);
+	return;
+    }
+});
+
 my $matcher_properties = {
     name => {
 	description => 'Name of the matcher.',
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 34/52] api: notification: add disable and origin params
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (32 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 33/52] notify: add API routes for smtp endpoints Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 35/52] api: notification: simplify ACLs for notification Lukas Wagner
                   ` (20 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

'disable' can be set to disable a matcher/target.
'origin' signals whether the configuration entry
was created by the user or whether it was built-in/
built-in-and-modified.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 PVE/API2/Cluster/Notifications.pm | 113 ++++++++++++++++++++++++++----
 1 file changed, 99 insertions(+), 14 deletions(-)

diff --git a/PVE/API2/Cluster/Notifications.pm b/PVE/API2/Cluster/Notifications.pm
index 42207aaa..27e3a66d 100644
--- a/PVE/API2/Cluster/Notifications.pm
+++ b/PVE/API2/Cluster/Notifications.pm
@@ -164,8 +164,19 @@ __PACKAGE__->register_method ({
 		},
 		'comment' => {
 		    description => 'Comment',
-		    type        => 'string',
-		    optional    => 1,
+		    type => 'string',
+		    optional => 1,
+		},
+		'disable' => {
+		    description => 'Show if this target is disabled',
+		    type => 'boolean',
+		    optional => 1,
+		    default => 0,
+		},
+		'origin' => {
+		    description => 'Show if this entry was created by a user or was built-in',
+		    type  => 'string',
+		    enum => [qw(user-created builtin modified-builtin)],
 		},
 	    },
 	},
@@ -183,6 +194,8 @@ __PACKAGE__->register_method ({
 		    name => $target->{name},
 		    comment => $target->{comment},
 		    type => 'sendmail',
+		    disable => $target->{disable},
+		    origin => $target->{origin},
 		};
 	    }
 
@@ -191,6 +204,8 @@ __PACKAGE__->register_method ({
 		    name => $target->{name},
 		    comment => $target->{comment},
 		    type => 'gotify',
+		    disable => $target->{disable},
+		    origin => $target->{origin},
 		};
 	    }
 
@@ -199,6 +214,8 @@ __PACKAGE__->register_method ({
 		    name => $target->{name},
 		    comment => $target->{comment},
 		    type => 'smtp',
+		    disable => $target->{disable},
+		    origin => $target->{origin},
 		};
 	    }
 
@@ -295,8 +312,14 @@ my $sendmail_properties = {
     },
     'comment' => {
 	description => 'Comment',
-	type        => 'string',
-	optional    => 1,
+	type => 'string',
+	optional => 1,
+    },
+    'disable' => {
+	description => 'Disable this target',
+	type => 'boolean',
+	optional => 1,
+	default => 0,
     },
 };
 
@@ -319,7 +342,14 @@ __PACKAGE__->register_method ({
 	type => 'array',
 	items => {
 	    type => 'object',
-	    properties => $sendmail_properties,
+	    properties => {
+		%$sendmail_properties,
+		'origin' => {
+		    description => 'Show if this entry was created by a user or was built-in',
+		    type  => 'string',
+		    enum => [qw(user-created builtin modified-builtin)],
+		},
+	    },
 	},
 	links => [ { rel => 'child', href => '{name}' } ],
     },
@@ -404,6 +434,7 @@ __PACKAGE__->register_method ({
 	my $from_address = extract_param($param, 'from-address');
 	my $author = extract_param($param, 'author');
 	my $comment = extract_param($param, 'comment');
+	my $disable = extract_param($param, 'disable');
 
 	eval {
 	    PVE::Notify::lock_config(sub {
@@ -416,6 +447,7 @@ __PACKAGE__->register_method ({
 		    $from_address,
 		    $author,
 		    $comment,
+		    $disable,
 		);
 
 		PVE::Notify::write_config($config);
@@ -463,6 +495,7 @@ __PACKAGE__->register_method ({
 	my $from_address = extract_param($param, 'from-address');
 	my $author = extract_param($param, 'author');
 	my $comment = extract_param($param, 'comment');
+	my $disable = extract_param($param, 'disable');
 
 	my $delete = extract_param($param, 'delete');
 	my $digest = extract_param($param, 'digest');
@@ -478,6 +511,7 @@ __PACKAGE__->register_method ({
 		    $from_address,
 		    $author,
 		    $comment,
+		    $disable,
 		    $delete,
 		    $digest,
 		);
@@ -543,8 +577,14 @@ my $gotify_properties = {
     },
     'comment' => {
 	description => 'Comment',
-	type        => 'string',
-	optional    => 1,
+	type => 'string',
+	optional => 1,
+    },
+    'disable' => {
+	description => 'Disable this target',
+	type => 'boolean',
+	optional => 1,
+	default => 0,
     },
 };
 
@@ -567,7 +607,14 @@ __PACKAGE__->register_method ({
 	type => 'array',
 	items => {
 	    type => 'object',
-	    properties => remove_protected_properties($gotify_properties, ['token']),
+	    properties => {
+		% {remove_protected_properties($gotify_properties, ['token'])},
+		'origin' => {
+		    description => 'Show if this entry was created by a user or was built-in',
+		    type  => 'string',
+		    enum => [qw(user-created builtin modified-builtin)],
+		},
+	    },
 	},
 	links => [ { rel => 'child', href => '{name}' } ],
     },
@@ -650,6 +697,7 @@ __PACKAGE__->register_method ({
 	my $server = extract_param($param, 'server');
 	my $token = extract_param($param, 'token');
 	my $comment = extract_param($param, 'comment');
+	my $disable = extract_param($param, 'disable');
 
 	eval {
 	    PVE::Notify::lock_config(sub {
@@ -660,6 +708,7 @@ __PACKAGE__->register_method ({
 		    $server,
 		    $token,
 		    $comment,
+		    $disable,
 		);
 
 		PVE::Notify::write_config($config);
@@ -704,6 +753,7 @@ __PACKAGE__->register_method ({
 	my $server = extract_param($param, 'server');
 	my $token = extract_param($param, 'token');
 	my $comment = extract_param($param, 'comment');
+	my $disable = extract_param($param, 'disable');
 
 	my $delete = extract_param($param, 'delete');
 	my $digest = extract_param($param, 'digest');
@@ -717,6 +767,7 @@ __PACKAGE__->register_method ({
 		    $server,
 		    $token,
 		    $comment,
+		    $disable,
 		    $delete,
 		    $digest,
 		);
@@ -829,8 +880,14 @@ my $smtp_properties= {
     },
     'comment' => {
 	description => 'Comment',
-	type        => 'string',
-	optional    => 1,
+	type => 'string',
+	optional => 1,
+    },
+    'disable' => {
+	description => 'Disable this target',
+	type => 'boolean',
+	optional => 1,
+	default => 0,
     },
 };
 
@@ -853,7 +910,14 @@ __PACKAGE__->register_method ({
 	type => 'array',
 	items => {
 	    type => 'object',
-	    properties => $smtp_properties,
+	    properties => {
+		%{ remove_protected_properties($smtp_properties, ['password']) },
+		'origin' => {
+		    description => 'Show if this entry was created by a user or was built-in',
+		    type  => 'string',
+		    enum => [qw(user-created builtin modified-builtin)],
+		},
+	    },
 	},
 	links => [ { rel => 'child', href => '{name}' } ],
     },
@@ -943,6 +1007,7 @@ __PACKAGE__->register_method ({
 	my $from_address = extract_param($param, 'from-address');
 	my $author = extract_param($param, 'author');
 	my $comment = extract_param($param, 'comment');
+	my $disable = extract_param($param, 'disable');
 
 	eval {
 	    PVE::Notify::lock_config(sub {
@@ -960,6 +1025,7 @@ __PACKAGE__->register_method ({
 		    $from_address,
 		    $author,
 		    $comment,
+		    $disable,
 		);
 
 		PVE::Notify::write_config($config);
@@ -1012,6 +1078,7 @@ __PACKAGE__->register_method ({
 	my $from_address = extract_param($param, 'from-address');
 	my $author = extract_param($param, 'author');
 	my $comment = extract_param($param, 'comment');
+	my $disable = extract_param($param, 'disable');
 
 	my $delete = extract_param($param, 'delete');
 	my $digest = extract_param($param, 'digest');
@@ -1032,6 +1099,7 @@ __PACKAGE__->register_method ({
 		    $from_address,
 		    $author,
 		    $comment,
+		    $disable,
 		    $delete,
 		    $digest,
 		);
@@ -1135,8 +1203,14 @@ my $matcher_properties = {
     },
     'comment' => {
 	description => 'Comment',
-	type        => 'string',
-	optional    => 1,
+	type => 'string',
+	optional => 1,
+    },
+    'disable' => {
+	description => 'Disable this matcher',
+	type => 'boolean',
+	optional => 1,
+	default => 0,
     },
 };
 
@@ -1159,7 +1233,14 @@ __PACKAGE__->register_method ({
 	type => 'array',
 	items => {
 	    type => 'object',
-	    properties => $matcher_properties,
+	    properties => {
+		%$matcher_properties,
+		'origin' => {
+		    description => 'Show if this entry was created by a user or was built-in',
+		    type  => 'string',
+		    enum => [qw(user-created builtin modified-builtin)],
+		},
+	    }
 	},
 	links => [ { rel => 'child', href => '{name}' } ],
     },
@@ -1247,6 +1328,7 @@ __PACKAGE__->register_method ({
 	my $mode = extract_param($param, 'mode');
 	my $invert_match = extract_param($param, 'invert-match');
 	my $comment = extract_param($param, 'comment');
+	my $disable = extract_param($param, 'disable');
 
 	eval {
 	    PVE::Notify::lock_config(sub {
@@ -1261,6 +1343,7 @@ __PACKAGE__->register_method ({
 		    $mode,
 		    $invert_match,
 		    $comment,
+		    $disable,
 		);
 
 		PVE::Notify::write_config($config);
@@ -1309,6 +1392,7 @@ __PACKAGE__->register_method ({
 	my $mode = extract_param($param, 'mode');
 	my $invert_match = extract_param($param, 'invert-match');
 	my $comment = extract_param($param, 'comment');
+	my $disable = extract_param($param, 'disable');
 	my $digest = extract_param($param, 'digest');
 	my $delete = extract_param($param, 'delete');
 
@@ -1325,6 +1409,7 @@ __PACKAGE__->register_method ({
 		    $mode,
 		    $invert_match,
 		    $comment,
+		    $disable,
 		    $delete,
 		    $digest,
 		);
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-manager 35/52] api: notification: simplify ACLs for notification
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (33 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 34/52] api: notification: add disable and origin params Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 36/52] notification ui: add target selector for matcher Lukas Wagner
                   ` (19 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Use coarse-grained /mapping/notifications for now. We
can always extend later if we need to.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 PVE/API2/Cluster/Notifications.pm | 131 ++++++++++++------------------
 1 file changed, 54 insertions(+), 77 deletions(-)

diff --git a/PVE/API2/Cluster/Notifications.pm b/PVE/API2/Cluster/Notifications.pm
index 27e3a66d..7047f0b1 100644
--- a/PVE/API2/Cluster/Notifications.pm
+++ b/PVE/API2/Cluster/Notifications.pm
@@ -56,24 +56,6 @@ sub raise_api_error {
     die $exc;
 }
 
-sub filter_entities_by_privs {
-    my ($rpcenv, $entities) = @_;
-    my $authuser = $rpcenv->get_user();
-
-    my $can_see_mapping_privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
-
-    my $filtered = [grep {
-	$rpcenv->check_any(
-	    $authuser,
-	    "/mapping/notification/$_->{name}",
-	    $can_see_mapping_privs,
-	    1
-	);
-    } @$entities];
-
-    return $filtered;
-}
-
 __PACKAGE__->register_method ({
     name => 'index',
     path => '',
@@ -137,10 +119,11 @@ __PACKAGE__->register_method ({
     method => 'GET',
     description => 'Returns a list of all entities that can be used as notification targets.',
     permissions => {
-	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
-	    . " 'Mapping.Audit' permissions on '/mapping/notification/<name>'."
-	    . " The special 'mail-to-root' target is available to all users.",
-	user => 'all',
+	check => ['or',
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
+	    ['perm', '/mapping/notifications', ['Mapping.Use']],
+	],
     },
     protected => 1,
     parameters => {
@@ -184,7 +167,6 @@ __PACKAGE__->register_method ({
     },
     code => sub {
 	my $config = PVE::Notify::read_config();
-	my $rpcenv = PVE::RPCEnvironment::get();
 
 	my $targets = eval {
 	    my $result = [];
@@ -224,7 +206,7 @@ __PACKAGE__->register_method ({
 
 	raise_api_error($@) if $@;
 
-	return filter_entities_by_privs($rpcenv, $targets);
+	return $targets;
     }
 });
 
@@ -235,10 +217,11 @@ __PACKAGE__->register_method ({
     method => 'POST',
     description => 'Send a test notification to a provided target.',
     permissions => {
-	description => "The user requires 'Mapping.Modify', 'Mapping.Use' or"
-	    . " 'Mapping.Audit' permissions on '/mapping/notification/<name>'."
-	    . " The special 'mail-to-root' target can be accessed by all users.",
-	user => 'all',
+	check => ['or',
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
+	    ['perm', '/mapping/notifications', ['Mapping.Use']],
+	],
     },
     parameters => {
 	additionalProperties => 0,
@@ -254,16 +237,6 @@ __PACKAGE__->register_method ({
     code => sub {
 	my ($param) = @_;
 	my $name = extract_param($param, 'name');
-	my $rpcenv = PVE::RPCEnvironment::get();
-	my $authuser = $rpcenv->get_user();
-
-	my $privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
-
-	$rpcenv->check_any(
-	    $authuser,
-	    "/mapping/notification/$name",
-	    $privs,
-	);
 
 	eval {
 	    my $config = PVE::Notify::read_config();
@@ -329,9 +302,10 @@ __PACKAGE__->register_method ({
     method => 'GET',
     description => 'Returns a list of all sendmail endpoints',
     permissions => {
-	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
-	    . " 'Mapping.Audit' permissions on '/mapping/notification/<name>'.",
-	user => 'all',
+	check => ['or',
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
+	],
     },
     protected => 1,
     parameters => {
@@ -355,14 +329,13 @@ __PACKAGE__->register_method ({
     },
     code => sub {
 	my $config = PVE::Notify::read_config();
-	my $rpcenv = PVE::RPCEnvironment::get();
 
 	my $entities = eval {
 	    $config->get_sendmail_endpoints();
 	};
 	raise_api_error($@) if $@;
 
-	return filter_entities_by_privs($rpcenv, $entities);
+	return $entities;
     }
 });
 
@@ -373,8 +346,8 @@ __PACKAGE__->register_method ({
     description => 'Return a specific sendmail endpoint',
     permissions => {
 	check => ['or',
-	    ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
-	    ['perm', '/mapping/notification/{name}', ['Mapping.Audit']],
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
 	],
     },
     protected => 1,
@@ -418,7 +391,7 @@ __PACKAGE__->register_method ({
     method => 'POST',
     description => 'Create a new sendmail endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -466,7 +439,7 @@ __PACKAGE__->register_method ({
     method => 'PUT',
     description => 'Update existing sendmail endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -532,7 +505,7 @@ __PACKAGE__->register_method ({
     method => 'DELETE',
     description => 'Remove sendmail endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -595,9 +568,8 @@ __PACKAGE__->register_method ({
     description => 'Returns a list of all gotify endpoints',
     protected => 1,
     permissions => {
-	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
-	    . " 'Mapping.Audit' permissions on '/mapping/notification/<name>'.",
-	user => 'all',
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Audit']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -627,7 +599,7 @@ __PACKAGE__->register_method ({
 	};
 	raise_api_error($@) if $@;
 
-	return filter_entities_by_privs($rpcenv, $entities);
+	return $entities;
     }
 });
 
@@ -639,8 +611,8 @@ __PACKAGE__->register_method ({
     protected => 1,
     permissions => {
 	check => ['or',
-	    ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
-	    ['perm', '/mapping/notification/{name}', ['Mapping.Audit']],
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
 	],
     },
     parameters => {
@@ -683,7 +655,7 @@ __PACKAGE__->register_method ({
     method => 'POST',
     description => 'Create a new gotify endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -727,7 +699,7 @@ __PACKAGE__->register_method ({
     method => 'PUT',
     description => 'Update existing gotify endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -788,7 +760,7 @@ __PACKAGE__->register_method ({
     method => 'DELETE',
     description => 'Remove gotify endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -897,9 +869,10 @@ __PACKAGE__->register_method ({
     method => 'GET',
     description => 'Returns a list of all smtp endpoints',
     permissions => {
-	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
-	    . " 'Mapping.Audit' permissions on '/mapping/notification/targets/<name>'.",
-	user => 'all',
+	check => ['or',
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
+	],
     },
     protected => 1,
     parameters => {
@@ -923,14 +896,13 @@ __PACKAGE__->register_method ({
     },
     code => sub {
 	my $config = PVE::Notify::read_config();
-	my $rpcenv = PVE::RPCEnvironment::get();
 
 	my $entities = eval {
 	    $config->get_smtp_endpoints();
 	};
 	raise_api_error($@) if $@;
 
-	return filter_entities_by_privs($rpcenv, "targets", $entities);
+	return $entities;
     }
 });
 
@@ -941,8 +913,8 @@ __PACKAGE__->register_method ({
     description => 'Return a specific smtp endpoint',
     permissions => {
 	check => ['or',
-	    ['perm', '/mapping/notification/targets/{name}', ['Mapping.Modify']],
-	    ['perm', '/mapping/notification/targets/{name}', ['Mapping.Audit']],
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
 	],
     },
     protected => 1,
@@ -986,7 +958,9 @@ __PACKAGE__->register_method ({
     method => 'POST',
     description => 'Create a new smtp endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification/targets', ['Mapping.Modify']],
+	check => ['or',
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	],
     },
     parameters => {
 	additionalProperties => 0,
@@ -1044,7 +1018,9 @@ __PACKAGE__->register_method ({
     method => 'PUT',
     description => 'Update existing smtp endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification/targets/{name}', ['Mapping.Modify']],
+	check => ['or',
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	],
     },
     parameters => {
 	additionalProperties => 0,
@@ -1120,7 +1096,7 @@ __PACKAGE__->register_method ({
     method => 'DELETE',
     description => 'Remove smtp endpoint',
     permissions => {
-	check => ['perm', '/mapping/notification/targets/{name}', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -1221,9 +1197,11 @@ __PACKAGE__->register_method ({
     description => 'Returns a list of all matchers',
     protected => 1,
     permissions => {
-	description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
-	    . " 'Mapping.Audit' permissions on '/mapping/notification/<name>'.",
-	user => 'all',
+	check => ['or',
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
+	    ['perm', '/mapping/notifications', ['Mapping.Use']],
+	],
     },
     parameters => {
 	additionalProperties => 0,
@@ -1246,14 +1224,13 @@ __PACKAGE__->register_method ({
     },
     code => sub {
 	my $config = PVE::Notify::read_config();
-	my $rpcenv = PVE::RPCEnvironment::get();
 
 	my $entities = eval {
 	    $config->get_matchers();
 	};
 	raise_api_error($@) if $@;
 
-	return filter_entities_by_privs($rpcenv, $entities);
+	return $entities;
     }
 });
 
@@ -1265,8 +1242,8 @@ __PACKAGE__->register_method ({
     protected => 1,
     permissions => {
 	check => ['or',
-	    ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
-	    ['perm', '/mapping/notification/{name}', ['Mapping.Audit']],
+	    ['perm', '/mapping/notifications', ['Mapping.Modify']],
+	    ['perm', '/mapping/notifications', ['Mapping.Audit']],
 	],
     },
     parameters => {
@@ -1310,7 +1287,7 @@ __PACKAGE__->register_method ({
     description => 'Create a new matcher',
     protected => 1,
     permissions => {
-	check => ['perm', '/mapping/notification', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -1362,7 +1339,7 @@ __PACKAGE__->register_method ({
     method => 'PUT',
     description => 'Update existing matcher',
     permissions => {
-	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
@@ -1430,7 +1407,7 @@ __PACKAGE__->register_method ({
     method => 'DELETE',
     description => 'Remove matcher',
     permissions => {
-	check => ['perm', '/mapping/notification/{name}', ['Mapping.Modify']],
+	check => ['perm', '/mapping/notifications', ['Mapping.Modify']],
     },
     parameters => {
 	additionalProperties => 0,
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 36/52] notification ui: add target selector for matcher
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (34 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 35/52] api: notification: simplify ACLs for notification Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 37/52] notification ui: remove filter setting for targets Lukas Wagner
                   ` (18 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/window/NotificationFilterEdit.js | 145 +++++++++++++++++++++++++++
 1 file changed, 145 insertions(+)

diff --git a/src/window/NotificationFilterEdit.js b/src/window/NotificationFilterEdit.js
index 703a9e2..bcde4fa 100644
--- a/src/window/NotificationFilterEdit.js
+++ b/src/window/NotificationFilterEdit.js
@@ -49,6 +49,11 @@ Ext.define('Proxmox.panel.NotificationFilterEditPanel', {
 		deleteDefaultValue: '{!isCreate}',
 	    },
 	},
+	{
+	    xtype: 'pmxNotificationTargetSelector',
+	    name: 'target',
+	    allowBlank: false,
+	},
 	{
 	    xtype: 'proxmoxtextfield',
 	    name: 'comment',
@@ -107,3 +112,143 @@ Ext.define('Proxmox.window.NotificationFilterEdit', {
 	}
     },
 });
+
+Ext.define('Proxmox.form.NotificationTargetSelector', {
+    extend: 'Ext.grid.Panel',
+    alias: 'widget.pmxNotificationTargetSelector',
+
+    mixins: {
+	field: 'Ext.form.field.Field',
+    },
+
+    padding: '0 0 10 0',
+
+    allowBlank: true,
+    selectAll: false,
+    isFormField: true,
+
+    store: {
+	autoLoad: true,
+	model: 'proxmox-notification-endpoints',
+	sorters: 'name',
+    },
+
+    columns: [
+	{
+	    header: gettext('Target Name'),
+	    dataIndex: 'name',
+	    flex: 1,
+	},
+	{
+	    header: gettext('Type'),
+	    dataIndex: 'type',
+	    flex: 1,
+	},
+	{
+	    header: gettext('Comment'),
+	    dataIndex: 'comment',
+	    flex: 3,
+	},
+    ],
+
+    selModel: {
+	selType: 'checkboxmodel',
+	mode: 'SIMPLE',
+    },
+
+    checkChangeEvents: [
+	'selectionchange',
+	'change',
+    ],
+
+    listeners: {
+	selectionchange: function() {
+	    // to trigger validity and error checks
+	    this.checkChange();
+	},
+    },
+
+    getSubmitData: function() {
+	let me = this;
+	let res = {};
+	res[me.name] = me.getValue();
+	return res;
+    },
+
+    getValue: function() {
+	let me = this;
+	if (me.savedValue !== undefined) {
+	    return me.savedValue;
+	}
+	let sm = me.getSelectionModel();
+	return (sm.getSelection() ?? []).map(item => item.data.name);
+    },
+
+    setValueSelection: function(value) {
+	let me = this;
+
+	let store = me.getStore();
+
+	let notFound = [];
+	let selection = value.map(item => {
+	    let found = store.findRecord('name', item, 0, false, true, true);
+	    if (!found) {
+		notFound.push(item);
+	    }
+	    return found;
+	}).filter(r => r);
+
+	for (const name of notFound) {
+	    let rec = store.add({
+		name,
+		type: '-',
+		comment: gettext('Included target does not exist!'),
+	    });
+	    selection.push(rec[0]);
+	}
+
+	let sm = me.getSelectionModel();
+	if (selection.length) {
+	    sm.select(selection);
+	} else {
+	    sm.deselectAll();
+	}
+	// to correctly trigger invalid class
+	me.getErrors();
+    },
+
+    setValue: function(value) {
+	let me = this;
+
+	let store = me.getStore();
+	if (!store.isLoaded()) {
+	    me.savedValue = value;
+	    store.on('load', function() {
+		me.setValueSelection(value);
+		delete me.savedValue;
+	    }, { single: true });
+	} else {
+	    me.setValueSelection(value);
+	}
+	return me.mixins.field.setValue.call(me, value);
+    },
+
+    getErrors: function(value) {
+	let me = this;
+	if (!me.isDisabled() && me.allowBlank === false &&
+	    me.getSelectionModel().getCount() === 0) {
+	    me.addBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
+	    return [gettext('No target selected')];
+	}
+
+	me.removeBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
+	return [];
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.callParent();
+	me.initField();
+    },
+
+});
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 37/52] notification ui: remove filter setting for targets
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (35 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 36/52] notification ui: add target selector for matcher Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 38/52] notification ui: remove notification groups Lukas Wagner
                   ` (17 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/Makefile                            |  1 -
 src/form/NotificationFilterSelector.js  | 58 -------------------------
 src/panel/GotifyEditPanel.js            |  9 ----
 src/panel/NotificationGroupEditPanel.js |  9 ----
 src/panel/SendmailEditPanel.js          |  9 ----
 5 files changed, 86 deletions(-)
 delete mode 100644 src/form/NotificationFilterSelector.js

diff --git a/src/Makefile b/src/Makefile
index 21fbe76..85ecea4 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -44,7 +44,6 @@ JSSRC=					\
 	form/RoleSelector.js		\
 	form/DiskSelector.js		\
 	form/MultiDiskSelector.js	\
-	form/NotificationFilterSelector.js	\
 	form/TaskTypeSelector.js	\
 	form/ACME.js			\
 	form/UserSelector.js		\
diff --git a/src/form/NotificationFilterSelector.js b/src/form/NotificationFilterSelector.js
deleted file mode 100644
index d2ab8be..0000000
--- a/src/form/NotificationFilterSelector.js
+++ /dev/null
@@ -1,58 +0,0 @@
-Ext.define('Proxmox.form.NotificationFilterSelector', {
-    extend: 'Proxmox.form.ComboGrid',
-    alias: ['widget.pmxNotificationFilterSelector'],
-
-    // set default value to empty array, else it inits it with
-    // null and after the store load it is an empty array,
-    // triggering dirtychange
-    value: [],
-    valueField: 'name',
-    displayField: 'name',
-    deleteEmpty: true,
-    skipEmptyText: true,
-    allowBlank: true,
-    editable: false,
-    autoSelect: false,
-
-    listConfig: {
-	columns: [
-	    {
-		header: gettext('Filter'),
-		dataIndex: 'name',
-		sortable: true,
-		hideable: false,
-		flex: 1,
-	    },
-	    {
-		header: gettext('Comment'),
-		dataIndex: 'comment',
-		sortable: true,
-		hideable: false,
-		flex: 2,
-	    },
-	],
-    },
-
-    initComponent: function() {
-	let me = this;
-
-	Ext.apply(me, {
-	    store: {
-		fields: ['name', 'comment'],
-		proxy: {
-		    type: 'proxmox',
-		    url: `/api2/json/${me.baseUrl}/filters`,
-		},
-		sorters: [
-		    {
-			property: 'name',
-			direction: 'ASC',
-		    },
-		],
-		autoLoad: true,
-	    },
-	});
-
-	me.callParent();
-    },
-});
diff --git a/src/panel/GotifyEditPanel.js b/src/panel/GotifyEditPanel.js
index 3ddcc4d..5d814e5 100644
--- a/src/panel/GotifyEditPanel.js
+++ b/src/panel/GotifyEditPanel.js
@@ -32,15 +32,6 @@ Ext.define('Proxmox.panel.GotifyEditPanel', {
 		allowBlank: '{!isCreate}',
 	    },
 	},
-	{
-	    xtype: 'pmxNotificationFilterSelector',
-	    name: 'filter',
-	    fieldLabel: gettext('Filter'),
-	    cbind: {
-		deleteEmpty: '{!isCreate}',
-		baseUrl: '{baseUrl}',
-	    },
-	},
 	{
 	    xtype: 'proxmoxtextfield',
 	    name: 'comment',
diff --git a/src/panel/NotificationGroupEditPanel.js b/src/panel/NotificationGroupEditPanel.js
index aa76810..910d15a 100644
--- a/src/panel/NotificationGroupEditPanel.js
+++ b/src/panel/NotificationGroupEditPanel.js
@@ -21,15 +21,6 @@ Ext.define('Proxmox.panel.NotificationGroupEditPanel', {
 	    name: 'endpoint',
 	    allowBlank: false,
 	},
-	{
-	    xtype: 'pmxNotificationFilterSelector',
-	    name: 'filter',
-	    fieldLabel: gettext('Filter'),
-	    cbind: {
-		deleteEmpty: '{!isCreate}',
-		baseUrl: '{baseUrl}',
-	    },
-	},
 	{
 	    xtype: 'proxmoxtextfield',
 	    name: 'comment',
diff --git a/src/panel/SendmailEditPanel.js b/src/panel/SendmailEditPanel.js
index ace6129..16abebc 100644
--- a/src/panel/SendmailEditPanel.js
+++ b/src/panel/SendmailEditPanel.js
@@ -86,15 +86,6 @@ Ext.define('Proxmox.panel.SendmailEditPanel', {
 		return this.up('pmxSendmailEditPanel').mailValidator();
 	    },
 	},
-	{
-	    xtype: 'pmxNotificationFilterSelector',
-	    name: 'filter',
-	    fieldLabel: gettext('Filter'),
-	    cbind: {
-		deleteEmpty: '{!isCreate}',
-		baseUrl: '{baseUrl}',
-	    },
-	},
 	{
 	    xtype: 'proxmoxtextfield',
 	    name: 'comment',
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 38/52] notification ui: remove notification groups
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (36 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 37/52] notification ui: remove filter setting for targets Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 39/52] notification ui: rename filter to matcher Lukas Wagner
                   ` (16 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/Makefile                            |   1 -
 src/Schema.js                           |   5 -
 src/panel/NotificationConfigView.js     |   4 -
 src/panel/NotificationGroupEditPanel.js | 174 ------------------------
 4 files changed, 184 deletions(-)
 delete mode 100644 src/panel/NotificationGroupEditPanel.js

diff --git a/src/Makefile b/src/Makefile
index 85ecea4..e07f17c 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -61,7 +61,6 @@ JSSRC=					\
 	panel/LogView.js		\
 	panel/NodeInfoRepoStatus.js	\
 	panel/NotificationConfigView.js	\
-	panel/NotificationGroupEditPanel.js	\
 	panel/JournalView.js		\
 	panel/PermissionView.js		\
 	panel/PruneKeepPanel.js		\
diff --git a/src/Schema.js b/src/Schema.js
index a7ffdf8..37ecd88 100644
--- a/src/Schema.js
+++ b/src/Schema.js
@@ -48,11 +48,6 @@ Ext.define('Proxmox.Schema', { // a singleton
 	    ipanel: 'pmxGotifyEditPanel',
 	    iconCls: 'fa-bell-o',
 	},
-	group: {
-	    name: gettext('Notification Group'),
-	    ipanel: 'pmxNotificationGroupEditPanel',
-	    iconCls: 'fa-bell-o',
-	},
     },
 
     pxarFileTypes: {
diff --git a/src/panel/NotificationConfigView.js b/src/panel/NotificationConfigView.js
index ff9c512..ba98395 100644
--- a/src/panel/NotificationConfigView.js
+++ b/src/panel/NotificationConfigView.js
@@ -191,10 +191,6 @@ Ext.define('Proxmox.panel.NotificationEndpointView', {
 		    callback: 'reload',
 		    enableFn: rec => rec.data.name !== 'mail-to-root',
 		    getUrl: function(rec) {
-			if (rec.data.type === 'group') {
-			    return `${me.baseUrl}/groups/${rec.getId()}`;
-			}
-
 			return `${me.baseUrl}/endpoints/${rec.data.type}/${rec.getId()}`;
 		    },
 		},
diff --git a/src/panel/NotificationGroupEditPanel.js b/src/panel/NotificationGroupEditPanel.js
deleted file mode 100644
index 910d15a..0000000
--- a/src/panel/NotificationGroupEditPanel.js
+++ /dev/null
@@ -1,174 +0,0 @@
-Ext.define('Proxmox.panel.NotificationGroupEditPanel', {
-    extend: 'Proxmox.panel.InputPanel',
-    xtype: 'pmxNotificationGroupEditPanel',
-    mixins: ['Proxmox.Mixin.CBind'],
-
-    type: 'group',
-
-    items: [
-	{
-	    xtype: 'pmxDisplayEditField',
-	    name: 'name',
-	    cbind: {
-		value: '{name}',
-		editable: '{isCreate}',
-	    },
-	    fieldLabel: gettext('Group Name'),
-	    allowBlank: false,
-	},
-	{
-	    xtype: 'pmxNotificationEndpointSelector',
-	    name: 'endpoint',
-	    allowBlank: false,
-	},
-	{
-	    xtype: 'proxmoxtextfield',
-	    name: 'comment',
-	    fieldLabel: gettext('Comment'),
-	    cbind: {
-		deleteEmpty: '{!isCreate}',
-	    },
-	},
-    ],
-});
-
-Ext.define('Proxmox.form.NotificationEndpointSelector', {
-    extend: 'Ext.grid.Panel',
-    alias: 'widget.pmxNotificationEndpointSelector',
-
-    mixins: {
-	field: 'Ext.form.field.Field',
-    },
-
-    padding: '0 0 10 0',
-
-    allowBlank: true,
-    selectAll: false,
-    isFormField: true,
-
-    store: {
-	autoLoad: true,
-	model: 'proxmox-notification-endpoints',
-	sorters: 'name',
-	filters: item => item.data.type !== 'group',
-    },
-
-    columns: [
-	{
-	    header: gettext('Endpoint Name'),
-	    dataIndex: 'name',
-	    flex: 1,
-	},
-	{
-	    header: gettext('Type'),
-	    dataIndex: 'type',
-	    flex: 1,
-	},
-	{
-	    header: gettext('Comment'),
-	    dataIndex: 'comment',
-	    flex: 3,
-	},
-    ],
-
-    selModel: {
-	selType: 'checkboxmodel',
-	mode: 'SIMPLE',
-    },
-
-    checkChangeEvents: [
-	'selectionchange',
-	'change',
-    ],
-
-    listeners: {
-	selectionchange: function() {
-	    // to trigger validity and error checks
-	    this.checkChange();
-	},
-    },
-
-    getSubmitData: function() {
-	let me = this;
-	let res = {};
-	res[me.name] = me.getValue();
-	return res;
-    },
-
-    getValue: function() {
-	let me = this;
-	if (me.savedValue !== undefined) {
-	    return me.savedValue;
-	}
-	let sm = me.getSelectionModel();
-	return (sm.getSelection() ?? []).map(item => item.data.name);
-    },
-
-    setValueSelection: function(value) {
-	let me = this;
-
-	let store = me.getStore();
-
-	let notFound = [];
-	let selection = value.map(item => {
-	    let found = store.findRecord('name', item, 0, false, true, true);
-	    if (!found) {
-		notFound.push(item);
-	    }
-	    return found;
-	}).filter(r => r);
-
-	for (const name of notFound) {
-	    let rec = store.add({
-		name,
-		type: '-',
-		comment: gettext('Included endpoint does not exist!'),
-	    });
-	    selection.push(rec[0]);
-	}
-
-	let sm = me.getSelectionModel();
-	if (selection.length) {
-	    sm.select(selection);
-	} else {
-	    sm.deselectAll();
-	}
-	// to correctly trigger invalid class
-	me.getErrors();
-    },
-
-    setValue: function(value) {
-	let me = this;
-
-	let store = me.getStore();
-	if (!store.isLoaded()) {
-	    me.savedValue = value;
-	    store.on('load', function() {
-		me.setValueSelection(value);
-		delete me.savedValue;
-	    }, { single: true });
-	} else {
-	    me.setValueSelection(value);
-	}
-	return me.mixins.field.setValue.call(me, value);
-    },
-
-    getErrors: function(value) {
-	let me = this;
-	if (!me.isDisabled() && me.allowBlank === false &&
-	    me.getSelectionModel().getCount() === 0) {
-	    me.addBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
-	    return [gettext('No endpoint selected')];
-	}
-
-	me.removeBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
-	return [];
-    },
-
-    initComponent: function() {
-	let me = this;
-	me.callParent();
-	me.initField();
-    },
-
-});
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 39/52] notification ui: rename filter to matcher
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (37 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 38/52] notification ui: remove notification groups Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 40/52] notification: matcher: add UI for matcher editing Lukas Wagner
                   ` (15 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/Makefile                                  |  2 +-
 src/data/model/NotificationConfig.js          |  2 +-
 src/panel/NotificationConfigView.js           | 26 +++++++++----------
 ...lterEdit.js => NotificationMatcherEdit.js} | 14 +++++-----
 4 files changed, 22 insertions(+), 22 deletions(-)
 rename src/window/{NotificationFilterEdit.js => NotificationMatcherEdit.js} (92%)

diff --git a/src/Makefile b/src/Makefile
index e07f17c..c6d31c3 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -88,7 +88,7 @@ JSSRC=					\
 	window/ACMEPluginEdit.js	\
 	window/ACMEDomains.js		\
 	window/EndpointEditBase.js	\
-	window/NotificationFilterEdit.js \
+	window/NotificationMatcherEdit.js \
 	window/FileBrowser.js		\
 	window/AuthEditBase.js		\
 	window/AuthEditOpenId.js	\
diff --git a/src/data/model/NotificationConfig.js b/src/data/model/NotificationConfig.js
index bb4ef85..f447db4 100644
--- a/src/data/model/NotificationConfig.js
+++ b/src/data/model/NotificationConfig.js
@@ -7,7 +7,7 @@ Ext.define('proxmox-notification-endpoints', {
     idProperty: 'name',
 });
 
-Ext.define('proxmox-notification-filters', {
+Ext.define('proxmox-notification-matchers', {
     extend: 'Ext.data.Model',
     fields: ['name', 'comment'],
     proxy: {
diff --git a/src/panel/NotificationConfigView.js b/src/panel/NotificationConfigView.js
index ba98395..ecf764d 100644
--- a/src/panel/NotificationConfigView.js
+++ b/src/panel/NotificationConfigView.js
@@ -21,7 +21,7 @@ Ext.define('Proxmox.panel.NotificationConfigView', {
 	    border: false,
 	    collapsible: true,
 	    animCollapse: false,
-	    xtype: 'pmxNotificationFilterView',
+	    xtype: 'pmxNotificationMatcherView',
 	    cbind: {
 		baseUrl: '{baseUrl}',
 	    },
@@ -209,21 +209,21 @@ Ext.define('Proxmox.panel.NotificationEndpointView', {
     },
 });
 
-Ext.define('Proxmox.panel.NotificationFilterView', {
+Ext.define('Proxmox.panel.NotificationMatcherView', {
     extend: 'Ext.grid.Panel',
-    alias: 'widget.pmxNotificationFilterView',
+    alias: 'widget.pmxNotificationMatcherView',
 
-    title: gettext('Notification Filters'),
+    title: gettext('Notification Matchers'),
 
     controller: {
 	xclass: 'Ext.app.ViewController',
 
-	openEditWindow: function(filter) {
+	openEditWindow: function(matcher) {
 	    let me = this;
 
-	    Ext.create('Proxmox.window.NotificationFilterEdit', {
+	    Ext.create('Proxmox.window.NotificationMatcherEdit', {
 		baseUrl: me.getView().baseUrl,
-		name: filter,
+		name: matcher,
 		autoShow: true,
 		listeners: {
 		    destroy: () => me.reload(),
@@ -253,12 +253,12 @@ Ext.define('Proxmox.panel.NotificationFilterView', {
 	activate: 'reload',
     },
 
-    emptyText: gettext('No notification filters configured'),
+    emptyText: gettext('No notification matchers configured'),
 
     columns: [
 	{
 	    dataIndex: 'name',
-	    text: gettext('Filter Name'),
+	    text: gettext('Matcher Name'),
 	    renderer: Ext.String.htmlEncode,
 	    flex: 1,
 	},
@@ -276,8 +276,8 @@ Ext.define('Proxmox.panel.NotificationFilterView', {
 	autoDestroyRstore: true,
 	rstore: {
 	    type: 'update',
-	    storeid: 'proxmox-notification-filters',
-	    model: 'proxmox-notification-filters',
+	    storeid: 'proxmox-notification-matchers',
+	    model: 'proxmox-notification-matchers',
 	    autoStart: true,
 	},
 	sorters: 'name',
@@ -307,12 +307,12 @@ Ext.define('Proxmox.panel.NotificationFilterView', {
 		{
 		    xtype: 'proxmoxStdRemoveButton',
 		    callback: 'reload',
-		    baseurl: `${me.baseUrl}/filters`,
+		    baseurl: `${me.baseUrl}/matchers`,
 		},
 	    ],
 	});
 
 	me.callParent();
-	me.store.rstore.proxy.setUrl(`/api2/json/${me.baseUrl}/filters`);
+	me.store.rstore.proxy.setUrl(`/api2/json/${me.baseUrl}/matchers`);
     },
 });
diff --git a/src/window/NotificationFilterEdit.js b/src/window/NotificationMatcherEdit.js
similarity index 92%
rename from src/window/NotificationFilterEdit.js
rename to src/window/NotificationMatcherEdit.js
index bcde4fa..a014f3e 100644
--- a/src/window/NotificationFilterEdit.js
+++ b/src/window/NotificationMatcherEdit.js
@@ -1,6 +1,6 @@
-Ext.define('Proxmox.panel.NotificationFilterEditPanel', {
+Ext.define('Proxmox.panel.NotificationMatcherEditPanel', {
     extend: 'Proxmox.panel.InputPanel',
-    xtype: 'pmxNotificationFilterEditPanel',
+    xtype: 'pmxNotificationMatcherEditPanel',
     mixins: ['Proxmox.Mixin.CBind'],
 
     items: [
@@ -11,7 +11,7 @@ Ext.define('Proxmox.panel.NotificationFilterEditPanel', {
 		value: '{name}',
 		editable: '{isCreate}',
 	    },
-	    fieldLabel: gettext('Filter Name'),
+	    fieldLabel: gettext('Matcher Name'),
 	    allowBlank: false,
 	},
 	{
@@ -65,7 +65,7 @@ Ext.define('Proxmox.panel.NotificationFilterEditPanel', {
     ],
 });
 
-Ext.define('Proxmox.window.NotificationFilterEdit', {
+Ext.define('Proxmox.window.NotificationMatcherEdit', {
     extend: 'Proxmox.window.Edit',
 
     isAdd: true,
@@ -85,7 +85,7 @@ Ext.define('Proxmox.window.NotificationFilterEdit', {
 	    throw "baseUrl not set";
 	}
 
-	me.url = `/api2/extjs${me.baseUrl}/filters`;
+	me.url = `/api2/extjs${me.baseUrl}/matchers`;
 
 	if (me.isCreate) {
 	    me.method = 'POST';
@@ -94,12 +94,12 @@ Ext.define('Proxmox.window.NotificationFilterEdit', {
 	    me.method = 'PUT';
 	}
 
-	me.subject = gettext('Notification Filter');
+	me.subject = gettext('Notification Matcher');
 
 	Ext.apply(me, {
 	    items: [{
 		name: me.name,
-		xtype: 'pmxNotificationFilterEditPanel',
+		xtype: 'pmxNotificationMatcherEditPanel',
 		isCreate: me.isCreate,
 		baseUrl: me.baseUrl,
 	    }],
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 40/52] notification: matcher: add UI for matcher editing
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (38 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 39/52] notification ui: rename filter to matcher Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 41/52] notification ui: unprotected mailto-root target Lukas Wagner
                   ` (14 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This modifies the old filter edit window in the following ways:
  - Split content into multiple panels
    - Name and comment in the first tab
    - Match rules in a tree-structure in the second tab
    - Targets to notify in the third tab

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    The code binding the match rule tree structure to the editable fields
    could definitely be a bit cleaner. I think this is the first time that
    we have used such a pattern, so there there was much experimentation
    needed to get this working.
    I plan to revisit it and clean up a bit later, I wanted to get
    the notification system changes on the list ASAP.

 src/window/NotificationMatcherEdit.js | 867 ++++++++++++++++++++++++--
 1 file changed, 820 insertions(+), 47 deletions(-)

diff --git a/src/window/NotificationMatcherEdit.js b/src/window/NotificationMatcherEdit.js
index a014f3e..c6f0726 100644
--- a/src/window/NotificationMatcherEdit.js
+++ b/src/window/NotificationMatcherEdit.js
@@ -1,6 +1,6 @@
-Ext.define('Proxmox.panel.NotificationMatcherEditPanel', {
+Ext.define('Proxmox.panel.NotificationMatcherGeneralPanel', {
     extend: 'Proxmox.panel.InputPanel',
-    xtype: 'pmxNotificationMatcherEditPanel',
+    xtype: 'pmxNotificationMatcherGeneralPanel',
     mixins: ['Proxmox.Mixin.CBind'],
 
     items: [
@@ -15,53 +15,27 @@ Ext.define('Proxmox.panel.NotificationMatcherEditPanel', {
 	    allowBlank: false,
 	},
 	{
-	    xtype: 'proxmoxKVComboBox',
-	    name: 'min-severity',
-	    fieldLabel: gettext('Minimum Severity'),
-	    value: null,
+	    xtype: 'proxmoxtextfield',
+	    name: 'comment',
+	    fieldLabel: gettext('Comment'),
 	    cbind: {
 		deleteEmpty: '{!isCreate}',
 	    },
-	    comboItems: [
-		['info', 'info'],
-		['notice', 'notice'],
-		['warning', 'warning'],
-		['error', 'error'],
-	    ],
-	    triggers: {
-		clear: {
-		    cls: 'pmx-clear-trigger',
-		    weight: -1,
-		    hidden: false,
-		    handler: function() {
-			this.setValue('');
-		    },
-		},
-	    },
-	},
-	{
-	    xtype: 'proxmoxcheckbox',
-	    fieldLabel: gettext('Invert match'),
-	    name: 'invert-match',
-	    uncheckedValue: 0,
-	    defaultValue: 0,
-	    cbind: {
-		deleteDefaultValue: '{!isCreate}',
-	    },
 	},
+    ],
+});
+
+Ext.define('Proxmox.panel.NotificationMatcherTargetPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    xtype: 'pmxNotificationMatcherTargetPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    items: [
 	{
 	    xtype: 'pmxNotificationTargetSelector',
 	    name: 'target',
 	    allowBlank: false,
 	},
-	{
-	    xtype: 'proxmoxtextfield',
-	    name: 'comment',
-	    fieldLabel: gettext('Comment'),
-	    cbind: {
-		deleteEmpty: '{!isCreate}',
-	    },
-	},
     ],
 });
 
@@ -74,7 +48,7 @@ Ext.define('Proxmox.window.NotificationMatcherEdit', {
 	labelWidth: 120,
     },
 
-    width: 500,
+    width: 700,
 
     initComponent: function() {
 	let me = this;
@@ -97,12 +71,38 @@ Ext.define('Proxmox.window.NotificationMatcherEdit', {
 	me.subject = gettext('Notification Matcher');
 
 	Ext.apply(me, {
-	    items: [{
-		name: me.name,
-		xtype: 'pmxNotificationMatcherEditPanel',
-		isCreate: me.isCreate,
-		baseUrl: me.baseUrl,
-	    }],
+	    bodyPadding: 0,
+	    items: [
+		{
+		    xtype: 'tabpanel',
+		    region: 'center',
+		    layout: 'fit',
+		    bodyPadding: 10,
+		    items: [
+			{
+			    name: me.name,
+			    title: gettext('General'),
+			    xtype: 'pmxNotificationMatcherGeneralPanel',
+			    isCreate: me.isCreate,
+			    baseUrl: me.baseUrl,
+			},
+			{
+			    name: me.name,
+			    title: gettext('Match Rules'),
+			    xtype: 'pmxNotificationMatchRulesEditPanel',
+			    isCreate: me.isCreate,
+			    baseUrl: me.baseUrl,
+			},
+			{
+			    name: me.name,
+			    title: gettext('Targets to notify'),
+			    xtype: 'pmxNotificationMatcherTargetPanel',
+			    isCreate: me.isCreate,
+			    baseUrl: me.baseUrl,
+			},
+		    ],
+		},
+	    ],
 	});
 
 	me.callParent();
@@ -252,3 +252,776 @@ Ext.define('Proxmox.form.NotificationTargetSelector', {
     },
 
 });
+
+Ext.define('Proxmox.panel.NotificationRulesEditPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    xtype: 'pmxNotificationMatchRulesEditPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    viewModel: {
+	data: {
+	    selectedRecord: null,
+	    matchFieldType: 'exact',
+	    matchFieldField: '',
+	    matchFieldValue: '',
+	    rootMode: 'all',
+	},
+
+	formulas: {
+	    nodeType: {
+		get: function(get) {
+		    let record = get('selectedRecord');
+		    return record?.get('type');
+		},
+		set: function(value) {
+		    let me = this;
+		    let record = me.get('selectedRecord');
+
+		    let data;
+
+		    switch (value) {
+			case 'match-severity':
+			    data = {
+				value: ['info', 'notice', 'warning', 'error'],
+			    };
+			    break;
+			case 'match-field':
+			    data = {
+				type: 'exact',
+				field: '',
+				value: '',
+			    };
+			    break;
+			case 'match-calendar':
+			    data = {
+				value: '',
+			    };
+			    break;
+		    }
+
+		    let node = {
+			type: value,
+			data,
+		    };
+		    record.set(node);
+		},
+	    },
+	    showMatchingMode: function(get) {
+		let record = get('selectedRecord');
+		if (!record) {
+		    return false;
+		}
+		return record.isRoot();
+	    },
+	    showMatcherType: function(get) {
+		let record = get('selectedRecord');
+		if (!record) {
+		    return false;
+		}
+		return !record.isRoot();
+	    },
+	    typeIsMatchField: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		get: function(record) {
+		    return record?.get('type') === 'match-field';
+		},
+	    },
+	    typeIsMatchSeverity: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		get: function(record) {
+		    return record?.get('type') === 'match-severity';
+		},
+	    },
+	    typeIsMatchCalendar: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		get: function(record) {
+		    return record?.get('type') === 'match-calendar';
+		},
+	    },
+	    matchFieldType: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		set: function(value) {
+		    let me = this;
+		    let record = me.get('selectedRecord');
+		    let currentData = record.get('data');
+		    record.set({
+			data: {
+			    ...currentData,
+			    type: value,
+			},
+		    });
+		},
+		get: function(record) {
+		    return record?.get('data')?.type;
+		},
+	    },
+	    matchFieldField: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		set: function(value) {
+		    let me = this;
+		    let record = me.get('selectedRecord');
+		    let currentData = record.get('data');
+
+		    record.set({
+			data: {
+			    ...currentData,
+			    field: value,
+			},
+		    });
+		},
+		get: function(record) {
+		    return record?.get('data')?.field;
+		},
+	    },
+	    matchFieldValue: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		set: function(value) {
+		    let me = this;
+		    let record = me.get('selectedRecord');
+		    let currentData = record.get('data');
+		    record.set({
+			data: {
+			    ...currentData,
+			    value: value,
+			},
+		    });
+		},
+		get: function(record) {
+		    return record?.get('data')?.value;
+		},
+	    },
+	    matchSeverityValue: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		set: function(value) {
+		    let me = this;
+		    let record = me.get('selectedRecord');
+		    let currentData = record.get('data');
+		    record.set({
+			data: {
+			    ...currentData,
+			    value: value,
+			},
+		    });
+		},
+		get: function(record) {
+		    return record?.get('data')?.value;
+		},
+	    },
+	    matchCalendarValue: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		set: function(value) {
+		    let me = this;
+		    let record = me.get('selectedRecord');
+		    let currentData = record.get('data');
+		    record.set({
+			data: {
+			    ...currentData,
+			    value: value,
+			},
+		    });
+		},
+		get: function(record) {
+		    return record?.get('data')?.value;
+		},
+	    },
+	    rootMode: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		set: function(value) {
+		    let me = this;
+		    let record = me.get('selectedRecord');
+		    let currentData = record.get('data');
+		    record.set({
+			data: {
+			    ...currentData,
+			    value,
+			},
+		    });
+		},
+		get: function(record) {
+		    return record?.get('data')?.value;
+		},
+	    },
+	    invertMatch: {
+		bind: {
+		    bindTo: '{selectedRecord}',
+		    deep: true,
+		},
+		set: function(value) {
+		    let me = this;
+		    let record = me.get('selectedRecord');
+		    let currentData = record.get('data');
+		    record.set({
+			data: {
+			    ...currentData,
+			    invert: value,
+			},
+		    });
+		},
+		get: function(record) {
+		    return record?.get('data')?.invert;
+		},
+	    },
+	},
+    },
+
+    column1: [
+	{
+	    xtype: 'pmxNotificationMatchRuleTree',
+	    cbind: {
+		isCreate: '{isCreate}',
+	    },
+	},
+    ],
+    column2: [
+	{
+	    xtype: 'pmxNotificationMatchRuleSettings',
+	},
+
+    ],
+
+    onGetValues: function(values) {
+	let me = this;
+
+	let deleteArrayIfEmtpy = (field) => {
+	    if (Ext.isArray(values[field])) {
+		if (values[field].length === 0) {
+		    delete values[field];
+		    if (!me.isCreate) {
+			Proxmox.Utils.assemble_field_data(values, { 'delete': field });
+		    }
+		}
+	    }
+	};
+	deleteArrayIfEmtpy('match-field');
+	deleteArrayIfEmtpy('match-severity');
+	deleteArrayIfEmtpy('match-calendar');
+
+	return values;
+    },
+});
+
+Ext.define('Proxmox.panel.NotificationMatchRuleTree', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'pmxNotificationMatchRuleTree',
+    mixins: ['Proxmox.Mixin.CBind'],
+    border: false,
+
+    getNodeTextAndIcon: function(type, data) {
+	let text;
+	let iconCls;
+
+	switch (type) {
+	    case 'match-severity': {
+		let v = data.value.join(', ');
+		text = Ext.String.format(gettext("Match severity: {0}"), v);
+		iconCls = 'fa fa-exclamation';
+	    } break;
+	    case 'match-field': {
+		let field = data.field;
+		let value = data.value;
+		text = Ext.String.format(gettext("Match field: {0}={1}"), field, value);
+		iconCls = 'fa fa-cube';
+	    } break;
+	    case 'match-calendar': {
+		let v = data.value;
+		text = Ext.String.format(gettext("Match calendar: {0}"), v);
+		iconCls = 'fa fa-calendar-o';
+	    } break;
+	    case 'mode':
+		if (data.value === 'all') {
+		    text = gettext("All");
+		} else if (data.value === 'any') {
+		    text = gettext("Any");
+		}
+		if (data.invert) {
+		    text = `!${text}`;
+		}
+		iconCls = 'fa fa-filter';
+
+		break;
+	}
+
+	return [text, iconCls];
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	let treeStore = Ext.create('Ext.data.TreeStore', {
+	    root: {
+		expanded: true,
+		expandable: false,
+		text: '',
+		type: 'mode',
+		data: {
+		    value: 'all',
+		    invert: false,
+		},
+		children: [],
+		iconCls: 'fa fa-filter',
+	    },
+	});
+
+	let realMatchFields = Ext.create({
+	    xtype: 'hiddenfield',
+	    setValue: function(value) {
+		this.value = value;
+		this.checkChange();
+	    },
+	    getValue: function() {
+		return this.value;
+	    },
+	    getSubmitValue: function() {
+		let value = this.value;
+		if (!value) {
+		    value = [];
+		}
+		return value;
+	    },
+	    name: 'match-field',
+	});
+
+	let realMatchSeverity = Ext.create({
+	    xtype: 'hiddenfield',
+	    setValue: function(value) {
+		this.value = value;
+		this.checkChange();
+	    },
+	    getValue: function() {
+		return this.value;
+	    },
+	    getSubmitValue: function() {
+		let value = this.value;
+		if (!value) {
+		    value = [];
+		}
+		return value;
+	    },
+	    name: 'match-severity',
+	});
+
+	let realMode = Ext.create({
+	    xtype: 'hiddenfield',
+	    name: 'mode',
+	    setValue: function(value) {
+		this.value = value;
+		this.checkChange();
+	    },
+	    getValue: function() {
+		return this.value;
+	    },
+	    getSubmitValue: function() {
+		let value = this.value;
+		return value;
+	    },
+	});
+
+	let realMatchCalendar = Ext.create({
+	    xtype: 'hiddenfield',
+	    name: 'match-calendar',
+
+	    setValue: function(value) {
+		this.value = value;
+		this.checkChange();
+	    },
+	    getValue: function() {
+		return this.value;
+	    },
+	    getSubmitValue: function() {
+		let value = this.value;
+		return value;
+	    },
+	});
+
+	let realInvertMatch = Ext.create({
+	    xtype: 'proxmoxcheckbox',
+	    name: 'invert-match',
+	    hidden: true,
+	    deleteEmpty: !me.isCreate,
+	});
+
+	let storeChanged = function(store) {
+	    store.suspendEvent('datachanged');
+
+	    let matchFieldStmts = [];
+	    let matchSeverityStmts = [];
+	    let matchCalendarStmts = [];
+	    let modeStmt = 'all';
+	    let invertMatchStmt = false;
+
+	    store.each(function(model) {
+		let type = model.get('type');
+		let data = model.get('data');
+
+		switch (type) {
+		    case 'match-field':
+			matchFieldStmts.push(`${data.type}:${data.field}=${data.value}`);
+			break;
+		    case 'match-severity':
+			matchSeverityStmts.push(data.value.join(','));
+			break;
+		    case 'match-calendar':
+			matchCalendarStmts.push(data.value);
+			break;
+		    case 'mode':
+			modeStmt = data.value;
+			invertMatchStmt = data.invert;
+			break;
+		}
+
+		let [text, iconCls] = me.getNodeTextAndIcon(type, data);
+		model.set({
+		    text,
+		    iconCls,
+		});
+	    });
+
+	    realMatchFields.suspendEvent('change');
+	    realMatchFields.setValue(matchFieldStmts);
+	    realMatchFields.resumeEvent('change');
+
+	    realMatchCalendar.suspendEvent('change');
+	    realMatchCalendar.setValue(matchCalendarStmts);
+	    realMatchCalendar.resumeEvent('change');
+
+	    realMode.suspendEvent('change');
+	    realMode.setValue(modeStmt);
+	    realMode.resumeEvent('change');
+
+	    realInvertMatch.suspendEvent('change');
+	    realInvertMatch.setValue(invertMatchStmt);
+	    realInvertMatch.resumeEvent('change');
+
+	    realMatchSeverity.suspendEvent('change');
+	    realMatchSeverity.setValue(matchSeverityStmts);
+	    realMatchSeverity.resumeEvent('change');
+
+	    store.resumeEvent('datachanged');
+	};
+
+	realMatchFields.addListener('change', function(field, value) {
+	    let parseMatchField = function(filter) {
+		let [, type, matchedField, matchedValue] =
+		    filter.match(/^(?:(regex|exact):)?([A-Za-z0-9_][A-Za-z0-9._-]*)=(.+)$/);
+		if (type === undefined) {
+		    type = "exact";
+		}
+		return {
+		    type: 'match-field',
+		    data: {
+			type,
+			field: matchedField,
+			value: matchedValue,
+		    },
+		    leaf: true,
+		};
+	    };
+
+	    for (let node of treeStore.queryBy(
+		record => record.get('type') === 'match-field',
+	    ).getRange()) {
+		node.remove(true);
+	    }
+
+	    let records = value.map(parseMatchField);
+
+	    let rootNode = treeStore.getRootNode();
+
+	    for (let record of records) {
+		rootNode.appendChild(record);
+	    }
+	});
+
+	realMatchSeverity.addListener('change', function(field, value) {
+	    let parseSeverity = function(severities) {
+		return {
+		    type: 'match-severity',
+		    data: {
+			value: severities.split(','),
+		    },
+		    leaf: true,
+		};
+	    };
+
+	    for (let node of treeStore.queryBy(
+		record => record.get('type') === 'match-severity').getRange()) {
+		node.remove(true);
+	    }
+
+	    let records = value.map(parseSeverity);
+	    let rootNode = treeStore.getRootNode();
+
+	    for (let record of records) {
+		rootNode.appendChild(record);
+	    }
+	});
+
+	realMatchCalendar.addListener('change', function(field, value) {
+	    let parseCalendar = function(timespan) {
+		return {
+		    type: 'match-calendar',
+		    data: {
+			value: timespan,
+		    },
+		    leaf: true,
+		};
+	    };
+
+	    for (let node of treeStore.queryBy(
+		record => record.get('type') === 'match-calendar').getRange()) {
+		node.remove(true);
+	    }
+
+	    let records = value.map(parseCalendar);
+	    let rootNode = treeStore.getRootNode();
+
+	    for (let record of records) {
+		rootNode.appendChild(record);
+	    }
+	});
+
+	realMode.addListener('change', function(field, value) {
+	    let data = treeStore.getRootNode().get('data');
+	    treeStore.getRootNode().set('data', {
+		...data,
+		value,
+	    });
+	});
+
+	realInvertMatch.addListener('change', function(field, value) {
+	    let data = treeStore.getRootNode().get('data');
+	    treeStore.getRootNode().set('data', {
+		...data,
+		invert: value,
+	    });
+	});
+
+	treeStore.addListener('datachanged', storeChanged);
+
+	let treePanel = Ext.create({
+	    xtype: 'treepanel',
+	    store: treeStore,
+	    minHeight: 300,
+	    maxHeight: 300,
+	    scrollable: true,
+
+	    bind: {
+		selection: '{selectedRecord}',
+	    },
+	});
+
+	let addNode = function() {
+	    let node = {
+		type: 'match-field',
+		data: {
+		    type: 'exact',
+		    field: '',
+		    value: '',
+		},
+		leaf: true,
+	    };
+	    treeStore.getRootNode().appendChild(node);
+	    treePanel.setSelection(treeStore.getRootNode().lastChild);
+	};
+
+	let deleteNode = function() {
+	    let selection = treePanel.getSelection();
+	    for (let selected of selection) {
+		if (!selected.isRoot()) {
+		    selected.remove(true);
+		}
+	    }
+	};
+
+	Ext.apply(me, {
+	    items: [
+		realMatchFields,
+		realMode,
+		realMatchSeverity,
+		realInvertMatch,
+		realMatchCalendar,
+		treePanel,
+		{
+		    xtype: 'button',
+		    margin: '5 5 5 0',
+		    text: gettext('Add'),
+		    iconCls: 'fa fa-plus-circle',
+		    handler: addNode,
+		},
+		{
+		    xtype: 'button',
+		    margin: '5 5 5 0',
+		    text: gettext('Remove'),
+		    iconCls: 'fa fa-minus-circle',
+		    handler: deleteNode,
+		},
+	    ],
+	});
+	me.callParent();
+    },
+});
+
+Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'pmxNotificationMatchRuleSettings',
+    border: false,
+
+    items: [
+	{
+	    xtype: 'proxmoxKVComboBox',
+	    name: 'mode',
+	    fieldLabel: gettext('Match if'),
+	    allowBlank: false,
+	    isFormField: false,
+
+	    comboItems: [
+		['all', gettext('All rules match')],
+		['any', gettext('Any rule matches')],
+	    ],
+	    bind: {
+		hidden: '{!showMatchingMode}',
+		disabled: '{!showMatchingMode}',
+		value: '{rootMode}',
+	    },
+	},
+	{
+	    xtype: 'proxmoxcheckbox',
+	    fieldLabel: gettext('Invert match'),
+	    isFormField: false,
+	    uncheckedValue: 0,
+	    defaultValue: 0,
+	    bind: {
+		hidden: '{!showMatchingMode}',
+		disabled: '{!showMatchingMode}',
+		value: '{invertMatch}',
+	    },
+
+	},
+	{
+	    xtype: 'proxmoxKVComboBox',
+	    fieldLabel: gettext('Node type'),
+	    isFormField: false,
+	    allowBlank: false,
+
+	    bind: {
+		value: '{nodeType}',
+		hidden: '{!showMatcherType}',
+		disabled: '{!showMatcherType}',
+	    },
+
+	    comboItems: [
+		['match-field', gettext('Match Field')],
+		['match-severity', gettext('Match Severity')],
+		['match-calendar', gettext('Match Calendar')],
+	    ],
+	},
+	{
+	    fieldLabel: 'Match Type',
+	    xtype: 'proxmoxKVComboBox',
+	    reference: 'type',
+	    isFormField: false,
+	    allowBlank: false,
+	    submitValue: false,
+
+	    bind: {
+		hidden: '{!typeIsMatchField}',
+		disabled: '{!typeIsMatchField}',
+		value: '{matchFieldType}',
+	    },
+
+	    comboItems: [
+		['exact', gettext('Exact')],
+		['regex', gettext('Regex')],
+	    ],
+	},
+	{
+	    fieldLabel: gettext('Field'),
+	    xtype: 'textfield',
+	    isFormField: false,
+	    submitValue: false,
+	    bind: {
+		hidden: '{!typeIsMatchField}',
+		disabled: '{!typeIsMatchField}',
+		value: '{matchFieldField}',
+	    },
+	},
+	{
+	    fieldLabel: gettext('Value'),
+	    xtype: 'textfield',
+	    isFormField: false,
+	    submitValue: false,
+	    allowBlank: false,
+	    bind: {
+		hidden: '{!typeIsMatchField}',
+		disabled: '{!typeIsMatchField}',
+		value: '{matchFieldValue}',
+	    },
+	},
+	{
+	    xtype: 'proxmoxKVComboBox',
+	    fieldLabel: gettext('Severities to match'),
+	    isFormField: false,
+	    allowBlank: true,
+	    multiSelect: true,
+
+	    bind: {
+		value: '{matchSeverityValue}',
+		hidden: '{!typeIsMatchSeverity}',
+		disabled: '{!typeIsMatchSeverity}',
+	    },
+
+	    comboItems: [
+		['info', gettext('Info')],
+		['notice', gettext('Notice')],
+		['warning', gettext('Warning')],
+		['error', gettext('Error')],
+	    ],
+	},
+	{
+	    xtype: 'proxmoxKVComboBox',
+	    fieldLabel: gettext('Timespan to match'),
+	    isFormField: false,
+	    allowBlank: false,
+	    editable: true,
+	    displayField: 'key',
+
+	    bind: {
+		value: '{matchCalendarValue}',
+		hidden: '{!typeIsMatchCalendar}',
+		disabled: '{!typeIsMatchCalender}',
+	    },
+
+	    comboItems: [
+		['mon 8-12', ''],
+		['tue..fri,sun 0:00-23:59', ''],
+	    ],
+	},
+    ],
+});
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 41/52] notification ui: unprotected mailto-root target
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (39 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 40/52] notification: matcher: add UI for matcher editing Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 42/52] noficiation: matcher edit: make 'field' an editable combobox Lukas Wagner
                   ` (13 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

A default notification config will now be created in pve-manager's
postinst hook - which is not magic in any way and can be modified
and deleted as desired.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/panel/NotificationConfigView.js | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/panel/NotificationConfigView.js b/src/panel/NotificationConfigView.js
index ecf764d..6a9bc20 100644
--- a/src/panel/NotificationConfigView.js
+++ b/src/panel/NotificationConfigView.js
@@ -41,10 +41,6 @@ Ext.define('Proxmox.panel.NotificationEndpointView', {
 	openEditWindow: function(endpointType, endpoint) {
 	    let me = this;
 
-	    if (endpoint === 'mail-to-root') {
-		return;
-	    }
-
 	    Ext.create('Proxmox.window.EndpointEditBase', {
 		baseUrl: me.getView().baseUrl,
 		type: endpointType,
@@ -183,13 +179,11 @@ Ext.define('Proxmox.panel.NotificationEndpointView', {
 		    xtype: 'proxmoxButton',
 		    text: gettext('Modify'),
 		    handler: 'openEditForSelectedItem',
-		    enableFn: rec => rec.data.name !== 'mail-to-root',
 		    disabled: true,
 		},
 		{
 		    xtype: 'proxmoxStdRemoveButton',
 		    callback: 'reload',
-		    enableFn: rec => rec.data.name !== 'mail-to-root',
 		    getUrl: function(rec) {
 			return `${me.baseUrl}/endpoints/${rec.data.type}/${rec.getId()}`;
 		    },
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 42/52] noficiation: matcher edit: make 'field' an editable combobox
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (40 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 41/52] notification ui: unprotected mailto-root target Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 43/52] panel: notification: add gui for SMTP endpoints Lukas Wagner
                   ` (12 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

For now with fixed options that are shared between most notification
events - later, once we have a notification registry, this should be
filled dynamically.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/window/NotificationMatcherEdit.js | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/window/NotificationMatcherEdit.js b/src/window/NotificationMatcherEdit.js
index c6f0726..fb55e17 100644
--- a/src/window/NotificationMatcherEdit.js
+++ b/src/window/NotificationMatcherEdit.js
@@ -963,14 +963,23 @@ Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
 	},
 	{
 	    fieldLabel: gettext('Field'),
-	    xtype: 'textfield',
+	    xtype: 'proxmoxKVComboBox',
 	    isFormField: false,
 	    submitValue: false,
+	    allowBlank: false,
+	    editable: true,
+	    displayField: 'key',
 	    bind: {
 		hidden: '{!typeIsMatchField}',
 		disabled: '{!typeIsMatchField}',
 		value: '{matchFieldField}',
 	    },
+	    // TODO: Once we have a 'notification registry', we should
+	    // retrive those via an API call.
+	    comboItems: [
+		['type', ''],
+		['hostname', ''],
+	    ],
 	},
 	{
 	    fieldLabel: gettext('Value'),
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 43/52] panel: notification: add gui for SMTP endpoints
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (41 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 42/52] noficiation: matcher edit: make 'field' an editable combobox Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 44/52] notification ui: add enable checkbox for targets/matchers Lukas Wagner
                   ` (11 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This new endpoint configuration panel is embedded in the existing
EndpointEditBase dialog window. This commit also factors out some of
the non-trivial common form elements that are shared between the new
panel and the already existing SendmailEditPanel into a separate panel
EmailRecipientPanel.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/Makefile                     |   2 +
 src/Schema.js                    |   5 +
 src/panel/EmailRecipientPanel.js |  88 +++++++++++++++
 src/panel/SendmailEditPanel.js   |  58 +---------
 src/panel/SmtpEditPanel.js       | 183 +++++++++++++++++++++++++++++++
 5 files changed, 281 insertions(+), 55 deletions(-)
 create mode 100644 src/panel/EmailRecipientPanel.js
 create mode 100644 src/panel/SmtpEditPanel.js

diff --git a/src/Makefile b/src/Makefile
index c6d31c3..01145b1 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -71,7 +71,9 @@ JSSRC=					\
 	panel/ACMEAccount.js		\
 	panel/ACMEPlugin.js		\
 	panel/ACMEDomains.js		\
+	panel/EmailRecipientPanel.js	\
 	panel/SendmailEditPanel.js	\
+	panel/SmtpEditPanel.js	\
 	panel/StatusView.js		\
 	panel/TfaView.js		\
 	panel/NotesView.js		\
diff --git a/src/Schema.js b/src/Schema.js
index 37ecd88..28e1037 100644
--- a/src/Schema.js
+++ b/src/Schema.js
@@ -43,6 +43,11 @@ Ext.define('Proxmox.Schema', { // a singleton
 	    ipanel: 'pmxSendmailEditPanel',
 	    iconCls: 'fa-envelope-o',
 	},
+	smtp: {
+	    name: gettext('SMTP'),
+	    ipanel: 'pmxSmtpEditPanel',
+	    iconCls: 'fa-envelope-o',
+	},
 	gotify: {
 	    name: gettext('Gotify'),
 	    ipanel: 'pmxGotifyEditPanel',
diff --git a/src/panel/EmailRecipientPanel.js b/src/panel/EmailRecipientPanel.js
new file mode 100644
index 0000000..b2bc03c
--- /dev/null
+++ b/src/panel/EmailRecipientPanel.js
@@ -0,0 +1,88 @@
+Ext.define('Proxmox.panel.EmailRecipientPanel', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'pmxEmailRecipientPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+    border: false,
+
+    mailValidator: function() {
+	let mailto_user = this.down(`[name=mailto-user]`);
+	let mailto = this.down(`[name=mailto]`);
+
+	if (!mailto_user.getValue()?.length && !mailto.getValue()) {
+	    return gettext('Either mailto or mailto-user must be set');
+	}
+
+	return true;
+    },
+
+    items: [
+	{
+	    layout: 'anchor',
+	    border: false,
+	    items: [
+		{
+		    xtype: 'pmxUserSelector',
+		    name: 'mailto-user',
+		    multiSelect: true,
+		    allowBlank: true,
+		    editable: false,
+		    skipEmptyText: true,
+		    fieldLabel: gettext('Recipient(s)'),
+		    cbind: {
+			deleteEmpty: '{!isCreate}',
+		    },
+		    validator: function() {
+			return this.up('pmxEmailRecipientPanel').mailValidator();
+		    },
+		    autoEl: {
+			tag: 'div',
+			'data-qtip': gettext('The notification will be sent to the user\'s configured mail address'),
+		    },
+		    listConfig: {
+			width: 600,
+			columns: [
+			    {
+				header: gettext('User'),
+				sortable: true,
+				dataIndex: 'userid',
+				renderer: Ext.String.htmlEncode,
+				flex: 1,
+			    },
+			    {
+				header: gettext('E-Mail'),
+				sortable: true,
+				dataIndex: 'email',
+				renderer: Ext.String.htmlEncode,
+				flex: 1,
+			    },
+			    {
+				header: gettext('Comment'),
+				sortable: false,
+				dataIndex: 'comment',
+				renderer: Ext.String.htmlEncode,
+				flex: 1,
+			    },
+			],
+		    },
+		},
+		{
+		    xtype: 'proxmoxtextfield',
+		    fieldLabel: gettext('Additional Recipient(s)'),
+		    name: 'mailto',
+		    allowBlank: true,
+		    emptyText: 'user@example.com, ...',
+		    cbind: {
+			deleteEmpty: '{!isCreate}',
+		    },
+		    autoEl: {
+			tag: 'div',
+			'data-qtip': gettext('Multiple recipients must be separated by spaces, commas or semicolons'),
+		    },
+		    validator: function() {
+			return this.up('pmxEmailRecipientPanel').mailValidator();
+		    },
+		},
+	    ],
+	},
+    ],
+});
diff --git a/src/panel/SendmailEditPanel.js b/src/panel/SendmailEditPanel.js
index 16abebc..17f2d4f 100644
--- a/src/panel/SendmailEditPanel.js
+++ b/src/panel/SendmailEditPanel.js
@@ -28,62 +28,10 @@ Ext.define('Proxmox.panel.SendmailEditPanel', {
 	    allowBlank: false,
 	},
 	{
-	    xtype: 'pmxUserSelector',
-	    name: 'mailto-user',
-	    reference: 'mailto-user',
-	    multiSelect: true,
-	    allowBlank: true,
-	    editable: false,
-	    skipEmptyText: true,
-	    fieldLabel: gettext('User(s)'),
+	    // provides 'mailto' and 'mailto-user' fields
+	    xtype: 'pmxEmailRecipientPanel',
 	    cbind: {
-		deleteEmpty: '{!isCreate}',
-	    },
-	    validator: function() {
-		return this.up('pmxSendmailEditPanel').mailValidator();
-	    },
-	    listConfig: {
-		width: 600,
-		columns: [
-		    {
-			header: gettext('User'),
-			sortable: true,
-			dataIndex: 'userid',
-			renderer: Ext.String.htmlEncode,
-			flex: 1,
-		    },
-		    {
-			header: gettext('E-Mail'),
-			sortable: true,
-			dataIndex: 'email',
-			renderer: Ext.String.htmlEncode,
-			flex: 1,
-		    },
-		    {
-			header: gettext('Comment'),
-			sortable: false,
-			dataIndex: 'comment',
-			renderer: Ext.String.htmlEncode,
-			flex: 1,
-		    },
-		],
-	    },
-	},
-	{
-	    xtype: 'proxmoxtextfield',
-	    fieldLabel: gettext('Additional Recipient(s)'),
-	    name: 'mailto',
-	    reference: 'mailto',
-	    allowBlank: true,
-	    cbind: {
-		deleteEmpty: '{!isCreate}',
-	    },
-	    autoEl: {
-		tag: 'div',
-		'data-qtip': gettext('Multiple recipients must be separated by spaces, commas or semicolons'),
-	    },
-	    validator: function() {
-		return this.up('pmxSendmailEditPanel').mailValidator();
+		isCreate: '{isCreate}',
 	    },
 	},
 	{
diff --git a/src/panel/SmtpEditPanel.js b/src/panel/SmtpEditPanel.js
new file mode 100644
index 0000000..ab21217
--- /dev/null
+++ b/src/panel/SmtpEditPanel.js
@@ -0,0 +1,183 @@
+Ext.define('Proxmox.panel.SmtpEditPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    xtype: 'pmxSmtpEditPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    type: 'smtp',
+
+    viewModel: {
+	xtype: 'viewmodel',
+	cbind: {
+	    isCreate: "{isCreate}",
+	},
+	data: {
+	    mode: 'tls',
+	    authentication: true,
+	},
+	formulas: {
+	    portEmptyText: function(get) {
+		let port;
+
+		switch (get('mode')) {
+		    case 'insecure':
+			port = 25;
+			break;
+		    case 'starttls':
+			port = 587;
+			break;
+		    case 'tls':
+			port = 465;
+			break;
+		}
+		return `${Proxmox.Utils.defaultText} (${port})`;
+	    },
+	    passwordEmptyText: function(get) {
+		let isCreate = this.isCreate;
+		return get('authentication') && !isCreate ? gettext('Unchanged') : '';
+	    },
+	},
+    },
+
+    columnT: [
+	{
+	    xtype: 'pmxDisplayEditField',
+	    name: 'name',
+	    cbind: {
+		value: '{name}',
+		editable: '{isCreate}',
+	    },
+	    fieldLabel: gettext('Endpoint Name'),
+	    allowBlank: false,
+	},
+    ],
+
+    column1: [
+	{
+	    xtype: 'proxmoxtextfield',
+	    fieldLabel: gettext('Server'),
+	    name: 'server',
+	    allowBlank: false,
+	    emptyText: gettext('mail.example.com'),
+	},
+	{
+	    xtype: 'proxmoxKVComboBox',
+	    name: 'mode',
+	    fieldLabel: gettext('Encryption'),
+	    editable: false,
+	    comboItems: [
+		['insecure', Proxmox.Utils.noneText + ' (' + gettext('insecure') + ')'],
+		['starttls', 'STARTTLS'],
+		['tls', 'TLS'],
+	    ],
+	    bind: "{mode}",
+	    cbind: {
+		deleteEmpty: '{!isCreate}',
+	    },
+	},
+	{
+	    xtype: 'proxmoxintegerfield',
+	    name: 'port',
+	    fieldLabel: gettext('Port'),
+	    minValue: 1,
+	    maxValue: 65535,
+	    bind: {
+		emptyText: "{portEmptyText}",
+	    },
+	    submitEmptyText: false,
+	    cbind: {
+		deleteEmpty: '{!isCreate}',
+	    },
+	},
+    ],
+    column2: [
+	{
+	    xtype: 'proxmoxcheckbox',
+	    fieldLabel: gettext('Authenticate'),
+	    name: 'authentication',
+	    bind: {
+		value: '{authentication}',
+	    },
+	},
+	{
+	    xtype: 'proxmoxtextfield',
+	    fieldLabel: gettext('Username'),
+	    name: 'username',
+	    allowBlank: false,
+	    cbind: {
+		deleteEmpty: '{!isCreate}',
+	    },
+	    bind: {
+		disabled: '{!authentication}',
+	    },
+	},
+	{
+	    xtype: 'proxmoxtextfield',
+	    inputType: 'password',
+	    fieldLabel: gettext('Password'),
+	    name: 'password',
+	    allowBlank: true,
+	    bind: {
+		disabled: '{!authentication}',
+		emptyText: '{passwordEmptyText}',
+	    },
+	},
+    ],
+    columnB: [
+	{
+	    xtype: 'proxmoxtextfield',
+	    fieldLabel: gettext('From Address'),
+	    name: 'from-address',
+	    allowBlank: false,
+	    emptyText: gettext('user@example.com'),
+	},
+	{
+	    // provides 'mailto' and 'mailto-user' fields
+	    xtype: 'pmxEmailRecipientPanel',
+	    cbind: {
+		isCreate: '{isCreate}',
+	    },
+	},
+	{
+	    xtype: 'proxmoxtextfield',
+	    name: 'comment',
+	    fieldLabel: gettext('Comment'),
+	    cbind: {
+		deleteEmpty: '{!isCreate}',
+	    },
+	},
+    ],
+
+    advancedColumnB: [
+	{
+	    xtype: 'proxmoxtextfield',
+	    fieldLabel: gettext('Author'),
+	    name: 'author',
+	    allowBlank: true,
+	    emptyText: gettext('Proxmox VE'),
+	    cbind: {
+		deleteEmpty: '{!isCreate}',
+	    },
+	},
+    ],
+
+    onGetValues: function(values) {
+	if (values.mailto) {
+	    values.mailto = values.mailto.split(/[\s,;]+/);
+	}
+
+	if (!values.authentication && !this.isCreate) {
+	    Proxmox.Utils.assemble_field_data(values, { 'delete': 'username' });
+	    Proxmox.Utils.assemble_field_data(values, { 'delete': 'password' });
+	}
+
+	delete values.authentication;
+
+	return values;
+    },
+
+    onSetValues: function(values) {
+	values.authentication = !!values.username;
+
+	return values;
+    },
+});
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 44/52] notification ui: add enable checkbox for targets/matchers
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (42 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 43/52] panel: notification: add gui for SMTP endpoints Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 45/52] notification ui: add column for 'origin' Lukas Wagner
                   ` (10 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Add a 'enable' checkbox for targets and matchers in their edit
windows. Also show a new 'enable' column in the overview panel.
The parameter in the config is actually called 'disable', so
the UI needs to invert the setting in the appropriate
on{Get,Set}Values hooks.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/data/model/NotificationConfig.js  |  4 ++--
 src/panel/GotifyEditPanel.js          | 30 +++++++++++++++++++++++++++
 src/panel/NotificationConfigView.js   | 12 +++++++++++
 src/panel/SendmailEditPanel.js        | 28 ++++++++++++++++++++++++-
 src/panel/SmtpEditPanel.js            | 23 +++++++++++++++++++-
 src/window/NotificationMatcherEdit.js | 30 +++++++++++++++++++++++++++
 6 files changed, 123 insertions(+), 4 deletions(-)

diff --git a/src/data/model/NotificationConfig.js b/src/data/model/NotificationConfig.js
index f447db4..9171515 100644
--- a/src/data/model/NotificationConfig.js
+++ b/src/data/model/NotificationConfig.js
@@ -1,6 +1,6 @@
 Ext.define('proxmox-notification-endpoints', {
     extend: 'Ext.data.Model',
-    fields: ['name', 'type', 'comment'],
+    fields: ['name', 'type', 'comment', 'disable'],
     proxy: {
         type: 'proxmox',
     },
@@ -9,7 +9,7 @@ Ext.define('proxmox-notification-endpoints', {
 
 Ext.define('proxmox-notification-matchers', {
     extend: 'Ext.data.Model',
-    fields: ['name', 'comment'],
+    fields: ['name', 'comment', 'disable'],
     proxy: {
         type: 'proxmox',
     },
diff --git a/src/panel/GotifyEditPanel.js b/src/panel/GotifyEditPanel.js
index 5d814e5..7e6ecd8 100644
--- a/src/panel/GotifyEditPanel.js
+++ b/src/panel/GotifyEditPanel.js
@@ -16,6 +16,13 @@ Ext.define('Proxmox.panel.GotifyEditPanel', {
 	    fieldLabel: gettext('Endpoint Name'),
 	    allowBlank: false,
 	},
+	{
+	    xtype: 'proxmoxcheckbox',
+	    name: 'enable',
+	    fieldLabel: gettext('Enable'),
+	    allowBlank: false,
+	    checked: true,
+	},
 	{
 	    xtype: 'proxmoxtextfield',
 	    fieldLabel: gettext('Server URL'),
@@ -41,4 +48,27 @@ Ext.define('Proxmox.panel.GotifyEditPanel', {
 	    },
 	},
     ],
+
+    onSetValues: (values) => {
+	values.enable = !values.disable;
+
+	delete values.disable;
+	return values;
+    },
+
+    onGetValues: function(values) {
+	let me = this;
+
+	if (values.enable) {
+	    if (!me.isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
+	    }
+	} else {
+	    values.disable = 1;
+	}
+
+	delete values.enable;
+
+	return values;
+    },
 });
diff --git a/src/panel/NotificationConfigView.js b/src/panel/NotificationConfigView.js
index 6a9bc20..ba69298 100644
--- a/src/panel/NotificationConfigView.js
+++ b/src/panel/NotificationConfigView.js
@@ -119,6 +119,12 @@ Ext.define('Proxmox.panel.NotificationEndpointView', {
     emptyText: gettext('No notification targets configured'),
 
     columns: [
+	{
+	    dataIndex: 'disable',
+	    text: gettext('Enable'),
+	    renderer: (disable) => Proxmox.Utils.renderEnabledIcon(!disable),
+	    align: 'center',
+	},
 	{
 	    dataIndex: 'name',
 	    text: gettext('Target Name'),
@@ -250,6 +256,12 @@ Ext.define('Proxmox.panel.NotificationMatcherView', {
     emptyText: gettext('No notification matchers configured'),
 
     columns: [
+	{
+	    dataIndex: 'disable',
+	    text: gettext('Enable'),
+	    renderer: (disable) => Proxmox.Utils.renderEnabledIcon(!disable),
+	    align: 'center',
+	},
 	{
 	    dataIndex: 'name',
 	    text: gettext('Matcher Name'),
diff --git a/src/panel/SendmailEditPanel.js b/src/panel/SendmailEditPanel.js
index 17f2d4f..930c4db 100644
--- a/src/panel/SendmailEditPanel.js
+++ b/src/panel/SendmailEditPanel.js
@@ -27,6 +27,13 @@ Ext.define('Proxmox.panel.SendmailEditPanel', {
 	    fieldLabel: gettext('Endpoint Name'),
 	    allowBlank: false,
 	},
+	{
+	    xtype: 'proxmoxcheckbox',
+	    name: 'enable',
+	    fieldLabel: gettext('Enable'),
+	    allowBlank: false,
+	    checked: true,
+	},
 	{
 	    // provides 'mailto' and 'mailto-user' fields
 	    xtype: 'pmxEmailRecipientPanel',
@@ -67,7 +74,26 @@ Ext.define('Proxmox.panel.SendmailEditPanel', {
 	},
     ],
 
-    onGetValues: (values) => {
+    onSetValues: (values) => {
+	values.enable = !values.disable;
+
+	delete values.disable;
+	return values;
+    },
+
+    onGetValues: function(values) {
+	let me = this;
+
+	if (values.enable) {
+	    if (!me.isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
+	    }
+	} else {
+	    values.disable = 1;
+	}
+
+	delete values.enable;
+
 	if (values.mailto) {
 	    values.mailto = values.mailto.split(/[\s,;]+/);
 	}
diff --git a/src/panel/SmtpEditPanel.js b/src/panel/SmtpEditPanel.js
index ab21217..218485d 100644
--- a/src/panel/SmtpEditPanel.js
+++ b/src/panel/SmtpEditPanel.js
@@ -49,6 +49,13 @@ Ext.define('Proxmox.panel.SmtpEditPanel', {
 	    fieldLabel: gettext('Endpoint Name'),
 	    allowBlank: false,
 	},
+	{
+	    xtype: 'proxmoxcheckbox',
+	    name: 'enable',
+	    fieldLabel: gettext('Enable'),
+	    allowBlank: false,
+	    checked: true,
+	},
     ],
 
     column1: [
@@ -161,15 +168,27 @@ Ext.define('Proxmox.panel.SmtpEditPanel', {
     ],
 
     onGetValues: function(values) {
+	let me = this;
+
 	if (values.mailto) {
 	    values.mailto = values.mailto.split(/[\s,;]+/);
 	}
 
-	if (!values.authentication && !this.isCreate) {
+	if (!values.authentication && !me.isCreate) {
 	    Proxmox.Utils.assemble_field_data(values, { 'delete': 'username' });
 	    Proxmox.Utils.assemble_field_data(values, { 'delete': 'password' });
 	}
 
+	if (values.enable) {
+	    if (!me.isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
+	    }
+	} else {
+	    values.disable = 1;
+	}
+
+	delete values.enable;
+
 	delete values.authentication;
 
 	return values;
@@ -177,6 +196,8 @@ Ext.define('Proxmox.panel.SmtpEditPanel', {
 
     onSetValues: function(values) {
 	values.authentication = !!values.username;
+	values.enable = !values.disable;
+	delete values.disable;
 
 	return values;
     },
diff --git a/src/window/NotificationMatcherEdit.js b/src/window/NotificationMatcherEdit.js
index fb55e17..c0dfa34 100644
--- a/src/window/NotificationMatcherEdit.js
+++ b/src/window/NotificationMatcherEdit.js
@@ -14,6 +14,13 @@ Ext.define('Proxmox.panel.NotificationMatcherGeneralPanel', {
 	    fieldLabel: gettext('Matcher Name'),
 	    allowBlank: false,
 	},
+	{
+	    xtype: 'proxmoxcheckbox',
+	    name: 'enable',
+	    fieldLabel: gettext('Enable'),
+	    allowBlank: false,
+	    checked: true,
+	},
 	{
 	    xtype: 'proxmoxtextfield',
 	    name: 'comment',
@@ -23,6 +30,29 @@ Ext.define('Proxmox.panel.NotificationMatcherGeneralPanel', {
 	    },
 	},
     ],
+
+
+    onSetValues: function(values) {
+	values.enable = !values.disable;
+
+	delete values.disable;
+	return values;
+    },
+
+    onGetValues: function(values) {
+	let me = this;
+
+	if (values.enable) {
+	    if (!me.isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
+	    }
+	} else {
+	    values.disable = 1;
+	}
+	delete values.enable;
+
+	return values;
+    },
 });
 
 Ext.define('Proxmox.panel.NotificationMatcherTargetPanel', {
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-widget-toolkit 45/52] notification ui: add column for 'origin'
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (43 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 44/52] notification ui: add enable checkbox for targets/matchers Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 46/52] notifications: update docs to for matcher-based notifications Lukas Wagner
                   ` (9 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This column shows whether a matcher/target was provided as a built-in
default config or if it was created by the user. For built-ins, it
also shows whether the built-in settings have been changed.

To reset a built-in entry to its defaults, one can simply delete it.
For best UX, the 'delete' button should change its text to 'reset
defaults' when a built-in target/matcher is selected. This will be
added in another patch.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/data/model/NotificationConfig.js |  4 ++--
 src/panel/NotificationConfigView.js  | 32 ++++++++++++++++++++++++++--
 2 files changed, 32 insertions(+), 4 deletions(-)

diff --git a/src/data/model/NotificationConfig.js b/src/data/model/NotificationConfig.js
index 9171515..e8ebf28 100644
--- a/src/data/model/NotificationConfig.js
+++ b/src/data/model/NotificationConfig.js
@@ -1,6 +1,6 @@
 Ext.define('proxmox-notification-endpoints', {
     extend: 'Ext.data.Model',
-    fields: ['name', 'type', 'comment', 'disable'],
+    fields: ['name', 'type', 'comment', 'disable', 'origin'],
     proxy: {
         type: 'proxmox',
     },
@@ -9,7 +9,7 @@ Ext.define('proxmox-notification-endpoints', {
 
 Ext.define('proxmox-notification-matchers', {
     extend: 'Ext.data.Model',
-    fields: ['name', 'comment', 'disable'],
+    fields: ['name', 'comment', 'disable', 'origin'],
     proxy: {
         type: 'proxmox',
     },
diff --git a/src/panel/NotificationConfigView.js b/src/panel/NotificationConfigView.js
index ba69298..4695da5 100644
--- a/src/panel/NotificationConfigView.js
+++ b/src/panel/NotificationConfigView.js
@@ -129,7 +129,7 @@ Ext.define('Proxmox.panel.NotificationEndpointView', {
 	    dataIndex: 'name',
 	    text: gettext('Target Name'),
 	    renderer: Ext.String.htmlEncode,
-	    flex: 1,
+	    flex: 2,
 	},
 	{
 	    dataIndex: 'type',
@@ -141,7 +141,21 @@ Ext.define('Proxmox.panel.NotificationEndpointView', {
 	    dataIndex: 'comment',
 	    text: gettext('Comment'),
 	    renderer: Ext.String.htmlEncode,
-	    flex: 1,
+	    flex: 3,
+	},
+	{
+	    dataIndex: 'origin',
+	    text: gettext('Origin'),
+	    renderer: (origin) => {
+		switch (origin) {
+		    case 'user-created': return gettext('Custom');
+		    case 'modified-builtin': return gettext('Built-In (modified)');
+		    case 'builtin': return gettext('Built-In');
+		}
+
+		// Should not happen...
+		return 'unknown';
+	    },
 	},
     ],
 
@@ -274,6 +288,20 @@ Ext.define('Proxmox.panel.NotificationMatcherView', {
 	    renderer: Ext.String.htmlEncode,
 	    flex: 2,
 	},
+	{
+	    dataIndex: 'origin',
+	    text: gettext('Origin'),
+	    renderer: (origin) => {
+		switch (origin) {
+		    case 'user-created': return gettext('Custom');
+		    case 'modified-builtin': return gettext('Built-In (modified)');
+		    case 'builtin': return gettext('Built-In');
+		}
+
+		// Should not happen...
+		return 'unknown';
+	    },
+	},
     ],
 
     store: {
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-docs 46/52] notifications: update docs to for matcher-based notifications
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (44 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 45/52] notification ui: add column for 'origin' Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 47/52] notifications: document SMTP endpoints Lukas Wagner
                   ` (8 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Target groups and filters have been replaced by notification matchers.
The matcher can match on certain notification properties and route
the notification to a target in case of a match.

This patch updates the docs to reflect these changes.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 notifications.adoc | 254 +++++++++++++++++++++++++++++++--------------
 1 file changed, 174 insertions(+), 80 deletions(-)

diff --git a/notifications.adoc b/notifications.adoc
index c4d2931..764ec72 100644
--- a/notifications.adoc
+++ b/notifications.adoc
@@ -5,45 +5,40 @@ ifndef::manvolnum[]
 :pve-toplevel:
 endif::manvolnum[]
 
-[[notification_events]]
-Notification Events
--------------------
-
-{pve} will attempt to notify system administrators in case of certain events,
-such as:
-
-[width="100%",options="header"]
-|===========================================================================
-| Event name        | Description                             | Severity
-| `package-updates` | System updates are available            | `info`
-| `fencing`         | The {pve} HA manager has fenced a node  | `error`
-| `replication`     | A storage replication job has failed    | `error`
-| `vzdump`          | vzdump backup finished                  | `info` (`error` on failure)
-|===========================================================================
-
-In the 'Notification' panel of the datacenter view, the system's behavior can be
-configured for all events except backup jobs. For backup jobs,
-the settings can be found in the respective backup job configuration.
-For every notification event there is an option to configure the notification
-behavior (*when* to send a notification) and the notification target (*where* to
-send the notification).
-
-
-See also:
-
-* xref:datacenter_configuration_file[Datacenter Configuration]
-* xref:datacenter_configuration_file[vzdump]
+Overview
+--------
+
+{pve} will send notifications if case of noteworthy events in the system.
+
+There are a number of different xref:notification_events[notification events],
+each with their own set of metadata fields that can be used in
+notification matchers.
+
+A xref:notification_matchers[notification matcher] determines
+_which_ notifications shall be sent _where_.
+A matcher has _match rules_, that can be used to
+match on certain notification properties (e.g. timestamp, severity,
+metadata fields).
+If a matcher matches a notification, the notification will be routed
+to a configurable set of notification targets.
+
+A xref:notification_targets[notification target] is an abstraction for a
+destination where a notification should be sent to - for instance,
+a Gotify server instance, or a set of email addresses.
+There are multiple types of notification targets, including
+`sendmail`, which uses the system's sendmail command to send emails,
+or `gotify`, which sends a notification to a Gotify instance.
+
+The notification system can be configured in the GUI under
+Datacenter -> Notifications. The configuration is stored in
+`/etc/pve/notifications.cfg` and `/etc/pve/priv/notifications.cfg` -
+the latter contains sensitive configuration options such as
+passwords or authentication tokens for notification targets.
 
 [[notification_targets]]
 Notification Targets
 --------------------
 
-Notification targets can be configured in the 'Notification Targets' panel.
-
-NOTE: The `mail-to-root` target is always available and cannot be modified or
-removed. It sends a mail the `root@pam` user by using the `sendmail` command and
-serves as a fallback target if no other target is configured for an event.
-
 Sendmail
 ~~~~~~~~
 The sendmail binary is a program commonly found on Unix-like operating systems
@@ -73,7 +68,15 @@ set, the plugin will fall back to the `email_from` setting from
 `datacenter.cfg`. If that is also not set, the plugin will default to
 `root@$hostname`, where `$hostname` is the hostname of the node.
 
-* `filter`: The name of the filter to use for this target.
+Example configuration (`/etc/pve/notifications.cfg`):
+----
+sendmail: example
+        mailto-user root@pam
+        mailto-user admin@pve
+        mailto max@example.com
+        from-address pve1@example.com
+        comment Send to multiple users/addresses
+----
 
 Gotify
 ~~~~~~
@@ -88,72 +91,163 @@ The configuration for Gotify target plugins has the following options:
 * `server`: The base URL of the Gotify server, e.g. `http://<ip>:8888`
 * `token`: The authentication token. Tokens can be generated within the Gotify
 web interface.
-* `filter`: The name of the filter to use for this target.
 
 NOTE: The Gotify target plugin will respect the HTTP proxy settings from the
  xref:datacenter_configuration_file[datacenter configuration]
 
-Group
-~~~~~
+Example configuration (`/etc/pve/notifications.cfg`):
+----
+gotify: example
+        server http://gotify.example.com:8888
+        comment Send to multiple users/addresses
+----
 
-One can only select a single target for notification events.
-To notify via multiple targets at the same time, a group can be created.
-A group can reference multiple targets. If a group is used as a target,
-the notification will be sent to all referenced targets. Groups can reference
-all targets except other groups.
+The matching entry in `/etc/pve/priv/notifications.cfg`, containing the
+secret token:
+----
+gotify: example
+        token somesecrettoken
+----
 
+[[notification_matchers]]
+Notification Matchers
+---------------------
+Notification matchers route notifications to notification targets based
+on their matching rules. These rules can match of certain properties of
+a notification, such as the timestamp (`match-calendar`), the severity of
+the notificaiton (`match-severity`) or metadata fiels (`match-field`).
+If a matcher matches a notification, all targets configured for the matcher
+will receive the notification.
+
+An arbitrary number of matchers can be created, each with with their own
+matching rules and targets to notify.
+Every target is notified at most once for every notification, even if
+the target is used in multiple matchers.
+
+A matcher without any matching rules is always true; the configured targets
+will always be notified.
+----
+matcher: always-matches
+        target admin
+        comment This matcher always matches
+----
 
-Notification Filters
---------------------
-A notification target can be configured to use a *notification filter*.
-If a notification is sent to a target with a filter, the
-filter will determine if the notification will be actually sent or not.
+Matcher Options
+~~~~~~~~~~~~~~~
 
-The following matchers are available:
+* `target`: Determine which target should be notified if the matcher matches.
+can be used multiple times to notify multiple targets.
+* `invert-match`: Inverts the result of the whole matcher
+* `mode`: Determines how the individual match rules are evaluated to compute
+the result for the whole matcher. If set to `all`, all matching rules must
+match. If set to `any`, at least one rule must match.
+a matcher must be true. Defaults to `all`.
+* `match-calendar`: Match the notification's timestamp against a schedule
+* `match-field`: Match the notification's metadata fields
+* `match-severity`: Match the notification's severity
 
-* `min-severity`: Matches notifications with equal or higher severity
+Calendar Matching Rules
+~~~~~~~~~~~~~~~~~~~~~~~
+A calendar matcher matches the time when a notification is sent agaist a
+configurable schedule.
 
-It is also possible to configure the evaluation of the individual matchers:
+* `match-calendar 8-12`
+* `match-calendar 8:00-15:30`
+* `match-calendar mon-fri 9:00-17:00`
+* `match-calendar sun,tue-wed,fri 9-17`
 
-* `invert-match`: Inverts the result of the whole filter
-* `mode`: Sets the logical operator used to connect the individual matchers to
-`and` or `or`. Defaults to `and`.
+Field Matching Rules
+~~~~~~~~~~~~~~~~~~~~
+Notifications have a selection of metadata fields that can be matched.
 
-The `mode` option also influences the evaluation of filters without any
-matchers. If set to `or`, an empty filter evaluates to `false` (do not notify).
-If set to `and`, the result is `true` (send a notification).
-----
-filter: always-matches
-    mode and
+* `match-field exact:type=vzdump` Only match notifications about backups.
+* `match-field regex:hostname=^.+\.example\.com$` Match the hostname of
+the node.
 
-filter: never-matches
-    mode or
-----
+If a matched metadata field does not exist, the notification will not be
+matched.
+For instance, a `match-field regex:hostname=.*` directive will only match
+notifications that have an arbitraty `hostname` metadata field, but will
+not match if the field does not exist.
 
-Permissions
------------
+Severity Matching Rules
+~~~~~~~~~~~~~~~~~~~~~~~
+A notification has a associated severity that can be matched.
 
-For every target or filter, there exists a corresponding ACL path
-`/mapping/notification/<name>`.
-If an operation can be triggered by a user (e.g. via the GUI or API) and if
-that operation is configured to notify via a given target, then
-the user must have the `Mapping.Use` permission on the corresponding
-node in the ACL tree.
-`Mapping.Modify` and `Mapping.Audit` are needed for
-writing/reading the configuration of a target or filter.
+* `match-severity error`: Only match errors
+* `match-severity warning,error`: Match warnings and error
 
-NOTE: For backwards-compatibility, the special `mail-to-root` target
-does not require `Mapping.Use`.
+The following severities are in use:
+`info`, `notice`, `warning`, `error`.
 
-NOTE: When sending notifications via a group target,
-the user must have the `Mapping.Use` permission for every single endpoint
-included in the group. If a group/endpoint is configured to
-use a filter, the user must have the `Mapping.Use` permission for the filter
-as well.
 
+Examples
+~~~~~~~~
+----
+matcher: workday
+        match-calendar mon-fri 9-17
+        target admin
+        comment Notify admins during working hours
+
+matcher: night-and-weekend
+        match-calendar mon-fri 9-17
+        invert-match true
+        target on-call-admins
+        comment Separate target for non-working hours
+----
 
+----
+matcher: backup-failures
+        match-field exact:type=vzdump
+        match-severity error
+        target backup-admins
+        comment Send notifications about backup failures to one group of admins
+
+matcher: cluster-failures
+        match-field exact:type=replication
+        match-field exact:type=fencing
+        mode any
+        target cluster-admins
+        comment Send cluster-related notifications to other group of admins
+----
 
+The last matcher could also be rewritten using a field matcher with a regular
+expression:
+----
+matcher: cluster-failures
+        match-field regex:type=^(replication|fencing)$
+        target cluster-admins
+        comment Send cluster-related notifications to other group of admins
+----
 
+[[notification_events]]
+Notification Events
+-------------------
 
+[width="100%",options="header"]
+|===========================================================================
+| Event                        | `type`            | Severity | Metadata fields (in addition to `type`)
+| System updates available     |`package-updates`  | `info`   | `hostname`
+| Cluster node fenced          |`fencing`          | `error`  | `hostname`
+| Storage replication failed   |`replication`      | `error`  | -
+| Backup finished              |`vzdump`           | `info` (`error` on failure) | `hostname`
+|===========================================================================
 
+[width="100%",options="header"]
+|=======================================================================
+| Field name | Description
+| `type`     | Type of the notifcation
+| `hostname` | Hostname, including domain (e.g. `pve1.example.com`)
+|=======================================================================
 
+Permissions
+-----------
+
+For every target, there exists a corresponding ACL path
+`/mapping/notification/targets/<name>`. Matchers use
+a seperate namespace in the ACL tree: `/mapping/notification/matchers/<name>`.
+
+To test a target, a user must have the `Mapping.Use` permission on the corresponding
+node in the ACL tree.
+`Mapping.Modify` and `Mapping.Audit` are needed to read/modify the
+configuration of a target or matcher.
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-docs 47/52] notifications: document SMTP endpoints
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (45 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 46/52] notifications: update docs to for matcher-based notifications Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 48/52] notifications: document 'comment' option for targets/matchers Lukas Wagner
                   ` (7 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 notifications.adoc | 47 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/notifications.adoc b/notifications.adoc
index 764ec72..acbdfae 100644
--- a/notifications.adoc
+++ b/notifications.adoc
@@ -67,6 +67,7 @@ accomodate multiple recipients.
 set, the plugin will fall back to the `email_from` setting from
 `datacenter.cfg`. If that is also not set, the plugin will default to
 `root@$hostname`, where `$hostname` is the hostname of the node.
+The `From` header in the email will be set to `$author <$from-address>`.
 
 Example configuration (`/etc/pve/notifications.cfg`):
 ----
@@ -78,6 +79,52 @@ sendmail: example
         comment Send to multiple users/addresses
 ----
 
+SMTP
+~~~~
+
+SMTP notification targets can send emails directly to an SMTP mail relay.
+
+The configuration for SMTP target plugins has the following options:
+
+* `mailto`: E-Mail address to which the notification shall be sent to. Can be
+set multiple times to accomodate multiple recipients.
+* `mailto-user`: Users to which emails shall be sent to. The user's email
+address will be looked up in `users.cfg`. Can be set multiple times to
+accomodate multiple recipients.
+* `author`: Sets the author of the E-Mail. Defaults to `Proxmox VE`.
+* `from-address`: Sets the From-addresss of the email. SMTP relays might require
+that this address is owned by the user in order to avoid spoofing.
+The `From` header in the email will be set to `$author <$from-address>`.
+* `username`: Username to use during authentication. If no username is set,
+no authentication will be performed. The PLAIN and LOGIN authentication methods
+are supported.
+* `password`: Password to use when authenticating.
+* `mode`: Sets the encryption mode (`insecure`, `starttls` or `tls`). Defaults
+to `tls`.
+* `server`: Address/IP of the SMTP relay
+* `port`: The port to connect to. If not set, the used port
+defaults to 25 (`insecure`), 465 (`tls`) or 587 (`starttls`), depending on the
+value of `mode`.
+* `comment`: Comment for this target
+
+Example configuration (`/etc/pve/notifications.cfg`):
+----
+smtp: example
+        mailto-user root@pam
+        mailto-user admin@pve
+        mailto max@example.com
+        from-address pve1@example.com
+        username pve1
+        server mail.example.com
+        mode starttls
+----
+The matching entry in `/etc/pve/priv/notifications.cfg`, containing the
+secret token:
+----
+smtp: example
+        password somepassword
+----
+
 Gotify
 ~~~~~~
 
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-docs 48/52] notifications: document 'comment' option for targets/matchers
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (46 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 47/52] notifications: document SMTP endpoints Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 49/52] notifications: add documentation for system mail forwarding Lukas Wagner
                   ` (6 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 notifications.adoc | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/notifications.adoc b/notifications.adoc
index acbdfae..e8ed51b 100644
--- a/notifications.adoc
+++ b/notifications.adoc
@@ -67,6 +67,7 @@ accomodate multiple recipients.
 set, the plugin will fall back to the `email_from` setting from
 `datacenter.cfg`. If that is also not set, the plugin will default to
 `root@$hostname`, where `$hostname` is the hostname of the node.
+* `comment`: Comment for this target
 The `From` header in the email will be set to `$author <$from-address>`.
 
 Example configuration (`/etc/pve/notifications.cfg`):
@@ -138,6 +139,7 @@ The configuration for Gotify target plugins has the following options:
 * `server`: The base URL of the Gotify server, e.g. `http://<ip>:8888`
 * `token`: The authentication token. Tokens can be generated within the Gotify
 web interface.
+* `comment`: Comment for this target
 
 NOTE: The Gotify target plugin will respect the HTTP proxy settings from the
  xref:datacenter_configuration_file[datacenter configuration]
@@ -192,6 +194,7 @@ a matcher must be true. Defaults to `all`.
 * `match-calendar`: Match the notification's timestamp against a schedule
 * `match-field`: Match the notification's metadata fields
 * `match-severity`: Match the notification's severity
+* `comment`: Comment for this matcher
 
 Calendar Matching Rules
 ~~~~~~~~~~~~~~~~~~~~~~~
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-docs 49/52] notifications: add documentation for system mail forwarding
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (47 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 48/52] notifications: document 'comment' option for targets/matchers Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 50/52] notifications: change to simplified ACL structure Lukas Wagner
                   ` (5 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    Changes v2 -> v3:
      - Dropped paragraph about target/policy, since we now do routing in
        matchers

 notifications.adoc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/notifications.adoc b/notifications.adoc
index e8ed51b..c7bdc5a 100644
--- a/notifications.adoc
+++ b/notifications.adoc
@@ -281,6 +281,7 @@ Notification Events
 | Cluster node fenced          |`fencing`          | `error`  | `hostname`
 | Storage replication failed   |`replication`      | `error`  | -
 | Backup finished              |`vzdump`           | `info` (`error` on failure) | `hostname`
+| Mail for root                |`system-mail`      | `unknown`| -
 |===========================================================================
 
 [width="100%",options="header"]
@@ -290,6 +291,21 @@ Notification Events
 | `hostname` | Hostname, including domain (e.g. `pve1.example.com`)
 |=======================================================================
 
+System Mail Forwarding
+---------------------
+
+Certain local system daemons, such as `smartd`, generate notification emails
+that are initially directed to the local `root` user. {pve} will
+feed these mails into the notification system as a notification of
+type `system-mail` and with severity `unknown`.
+
+When the forwarding process involves an email-based target
+(like `sendmail` or `smtp`), the email is forwarded exactly as received, with all
+original mail headers remaining intact. For all other targets,
+the system tries to extract both a subject line and the main text body
+from the email content. In instances where emails solely consist of HTML
+content, they will be transformed into plain text format during this process.
+
 Permissions
 -----------
 
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 pve-docs 50/52] notifications: change to simplified ACL structure.
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (48 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 49/52] notifications: add documentation for system mail forwarding Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-mail-forward 51/52] feed forwarded mails into proxmox_notify Lukas Wagner
                   ` (4 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

For now, we use a less deeply nested structure. We can always extend
it if we need to.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 notifications.adoc | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/notifications.adoc b/notifications.adoc
index c7bdc5a..74447e5 100644
--- a/notifications.adoc
+++ b/notifications.adoc
@@ -309,11 +309,9 @@ content, they will be transformed into plain text format during this process.
 Permissions
 -----------
 
-For every target, there exists a corresponding ACL path
-`/mapping/notification/targets/<name>`. Matchers use
-a seperate namespace in the ACL tree: `/mapping/notification/matchers/<name>`.
-
-To test a target, a user must have the `Mapping.Use` permission on the corresponding
-node in the ACL tree.
-`Mapping.Modify` and `Mapping.Audit` are needed to read/modify the
-configuration of a target or matcher.
+In order to modify/view the configuration for notification targets,
+the `Mapping.Modify/Mapping.Audit` permissions are required for the
+`/mapping/notifications` ACL node.
+
+Testing a target requires `Mapping.Use`, `Mapping.Audit` or `Mapping.Modify`
+permissions on `/mapping/notifications`
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-mail-forward 51/52] feed forwarded mails into proxmox_notify
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (49 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 50/52] notifications: change to simplified ACL structure Lukas Wagner
@ 2023-11-14 12:59 ` Lukas Wagner
  2023-11-14 13:00 ` [pve-devel] [PATCH v2 proxmox-mail-forward 52/52] update d/control Lukas Wagner
                   ` (3 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 12:59 UTC (permalink / raw)
  To: pve-devel

This allows us to send notifications for events from daemons that are
not under our control, e.g. zed, smartd, cron. etc...

For mail-based notification targets (sendmail, soon smtp) the mail is
forwarded as is, including all headers.
All other target types will try to parse the email to extra subject
and text body.

On PBS, where proxmox-notify is not yet fully integrated,
we simply add a default target/matcher to an empty config. That way
the behavior should be unchanged - mails will be forwarded to
root@pam.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    Changes v2 -> v3:
      - Update to matcher-based system - no need to read
        datacenter.cfg/node.cfg any more

 Cargo.toml  |   6 +-
 src/main.rs | 255 ++++++++++++++++++++++++----------------------------
 2 files changed, 121 insertions(+), 140 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index c68e802..a562f3e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ name = "proxmox-mail-forward"
 version = "0.2.0"
 authors = [
     "Fiona Ebner <f.ebner@proxmox.com>",
+    "Lukas Wagner <l.wagner@proxmox.com>",
     "Proxmox Support Team <support@proxmox.com>",
 ]
 edition = "2021"
@@ -16,10 +17,7 @@ exclude = [ "debian" ]
 anyhow = "1.0"
 log = "0.4.17"
 nix = "0.26"
-serde = { version = "1.0", features = ["derive"] }
-#serde_json = "1.0"
 syslog = "6.0"
 
-proxmox-schema = "1.3"
-proxmox-section-config = "1.0.2"
 proxmox-sys = "0.5"
+proxmox-notify = {version = "0.2", features = ["mail-forwarder", "pve-context", "pbs-context"] }
diff --git a/src/main.rs b/src/main.rs
index f3d4193..e56bc1e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,132 +1,124 @@
+//! A helper binary that forwards any mail passed via stdin to
+//! proxmox_notify.
+//!
+//! The binary's path is added to /root/.forward, which means that
+//! postfix will invoke it when the local root user receives an email message.
+//! The message is passed via stdin.
+//! The binary is installed with setuid permissions and will thus run as
+//! root (euid ~ root, ruid ~ nobody)
+//!
+//! The forwarding behavior is the following:
+//!   - PVE installed: Use PVE's notifications.cfg
+//!   - PBS installed: Use PBS's notifications.cfg if present. If not,
+//!     use an empty configuration and add a default sendmail target and
+//!     a matcher - this is needed because notifications are not yet
+//!     integrated in PBS.
+//!   - PVE/PBS co-installed: Use PVE's config *and* PBS's config, but if
+//!     PBS's config does not exist, a default sendmail target will *not* be
+//!     added. We assume that PVE's config contains the desired notification
+//!     behavior for system mails.
+//!
+use std::io::Read;
 use std::path::Path;
-use std::process::Command;
 
-use anyhow::{bail, format_err, Error};
-use serde::Deserialize;
+use anyhow::Error;
 
-use proxmox_schema::{ObjectSchema, Schema, StringSchema};
-use proxmox_section_config::{SectionConfig, SectionConfigPlugin};
+use proxmox_notify::context::pbs::PBS_CONTEXT;
+use proxmox_notify::context::pve::PVE_CONTEXT;
+use proxmox_notify::endpoints::sendmail::SendmailConfig;
+use proxmox_notify::matcher::MatcherConfig;
+use proxmox_notify::Config;
 use proxmox_sys::fs;
 
-const PBS_USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg";
-const PBS_ROOT_USER: &str = "root@pam";
-
-// FIXME: Switch to the actual schema when possible in terms of dependency.
-// It's safe to assume that the config was written with the actual schema restrictions, so parsing
-// it with the less restrictive schema should be enough for the purpose of getting the mail address.
-const DUMMY_ID_SCHEMA: Schema = StringSchema::new("dummy ID").min_length(3).schema();
-const DUMMY_EMAIL_SCHEMA: Schema = StringSchema::new("dummy email").schema();
-const DUMMY_USER_SCHEMA: ObjectSchema = ObjectSchema {
-    description: "minimal PBS user",
-    properties: &[
-        ("userid", false, &DUMMY_ID_SCHEMA),
-        ("email", true, &DUMMY_EMAIL_SCHEMA),
-    ],
-    additional_properties: true,
-    default_key: None,
-};
-
-#[derive(Deserialize)]
-struct DummyPbsUser {
-    pub email: Option<String>,
-}
-
-const PVE_USER_CFG_FILENAME: &str = "/etc/pve/user.cfg";
-const PVE_DATACENTER_CFG_FILENAME: &str = "/etc/pve/datacenter.cfg";
-const PVE_ROOT_USER: &str = "root@pam";
+const PVE_CFG_PATH: &str = "/etc/pve";
+const PVE_PUB_NOTIFICATION_CFG_FILENAME: &str = "/etc/pve/notifications.cfg";
+const PVE_PRIV_NOTIFICATION_CFG_FILENAME: &str = "/etc/pve/priv/notifications.cfg";
 
-/// Convenience helper to get the trimmed contents of an optional &str, mapping blank ones to `None`
-/// and creating a String from it for returning.
-fn normalize_for_return(s: Option<&str>) -> Option<String> {
-    match s?.trim() {
-        "" => None,
-        s => Some(s.to_string()),
-    }
-}
+const PBS_CFG_PATH: &str = "/etc/proxmox-backup";
+const PBS_PUB_NOTIFICATION_CFG_FILENAME: &str = "/etc/proxmox-backup/notifications.cfg";
+const PBS_PRIV_NOTIFICATION_CFG_FILENAME: &str = "/etc/proxmox-backup/notifications-priv.cfg";
 
-/// Extract the root user's email address from the PBS user config.
-fn get_pbs_mail_to(content: &str) -> Option<String> {
-    let mut config = SectionConfig::new(&DUMMY_ID_SCHEMA).allow_unknown_sections(true);
-    let user_plugin = SectionConfigPlugin::new(
-        "user".to_string(),
-        Some("userid".to_string()),
-        &DUMMY_USER_SCHEMA,
-    );
-    config.register_plugin(user_plugin);
-
-    match config.parse(PBS_USER_CFG_FILENAME, content) {
-        Ok(parsed) => {
-            parsed.sections.get(PBS_ROOT_USER)?;
-            match parsed.lookup::<DummyPbsUser>("user", PBS_ROOT_USER) {
-                Ok(user) => normalize_for_return(user.email.as_deref()),
-                Err(err) => {
-                    log::error!("unable to parse {} - {}", PBS_USER_CFG_FILENAME, err);
-                    None
-                }
-            }
-        }
+/// Wrapper around `proxmox_sys::fs::file_read_optional_string` which also returns `None` upon error
+/// after logging it.
+fn attempt_file_read<P: AsRef<Path>>(path: P) -> Option<String> {
+    match fs::file_read_optional_string(path.as_ref()) {
+        Ok(contents) => contents,
         Err(err) => {
-            log::error!("unable to parse {} - {}", PBS_USER_CFG_FILENAME, err);
+            log::error!("unable to read {path:?}: {err}", path = path.as_ref());
             None
         }
     }
 }
 
-/// Extract the root user's email address from the PVE user config.
-fn get_pve_mail_to(content: &str) -> Option<String> {
-    normalize_for_return(content.lines().find_map(|line| {
-        let fields: Vec<&str> = line.split(':').collect();
-        #[allow(clippy::get_first)] // to keep expression style consistent
-        match fields.get(0)?.trim() == "user" && fields.get(1)?.trim() == PVE_ROOT_USER {
-            true => fields.get(6).copied(),
-            false => None,
-        }
-    }))
-}
+/// Read data from stdin, until EOF is encountered.
+fn read_stdin() -> Result<Vec<u8>, Error> {
+    let mut input = Vec::new();
+    let stdin = std::io::stdin();
+    let mut handle = stdin.lock();
 
-/// Extract the From-address configured in the PVE datacenter config.
-fn get_pve_mail_from(content: &str) -> Option<String> {
-    normalize_for_return(
-        content
-            .lines()
-            .find_map(|line| line.strip_prefix("email_from:")),
-    )
+    handle.read_to_end(&mut input)?;
+    Ok(input)
 }
 
-/// Executes sendmail as a child process with the specified From/To-addresses, expecting the mail
-/// contents to be passed via stdin inherited from this program.
-fn forward_mail(mail_from: String, mail_to: Vec<String>) -> Result<(), Error> {
-    if mail_to.is_empty() {
-        bail!("user 'root@pam' does not have an email address");
-    }
+fn forward_common(mail: &[u8], config: &Config) -> Result<(), Error> {
+    let real_uid = nix::unistd::getuid();
+    // The uid is passed so that `sendmail` can be called as the a correct user.
+    // (sendmail will show a warning if called from a setuid process)
+    let notification =
+        proxmox_notify::Notification::new_forwarded_mail(mail, Some(real_uid.as_raw()))?;
+
+    proxmox_notify::api::common::send(config, &notification)?;
 
-    log::info!("forward mail to <{}>", mail_to.join(","));
+    Ok(())
+}
 
-    let mut cmd = Command::new("sendmail");
-    cmd.args([
-        "-bm", "-N", "never", // never send DSN (avoid mail loops)
-        "-f", &mail_from, "--",
-    ]);
-    cmd.args(mail_to);
-    cmd.env("PATH", "/sbin:/bin:/usr/sbin:/usr/bin");
+/// Forward a mail to PVE's notification system
+fn forward_for_pve(mail: &[u8]) -> Result<(), Error> {
+    let config = attempt_file_read(PVE_PUB_NOTIFICATION_CFG_FILENAME).unwrap_or_default();
+    let priv_config = attempt_file_read(PVE_PRIV_NOTIFICATION_CFG_FILENAME).unwrap_or_default();
 
-    // with status(), child inherits stdin
-    cmd.status()
-        .map_err(|err| format_err!("command {:?} failed - {}", cmd, err))?;
+    let config = Config::new(&config, &priv_config)?;
 
-    Ok(())
+    proxmox_notify::context::set_context(&PVE_CONTEXT);
+    forward_common(mail, &config)
 }
 
-/// Wrapper around `proxmox_sys::fs::file_read_optional_string` which also returns `None` upon error
-/// after logging it.
-fn attempt_file_read<P: AsRef<Path>>(path: P) -> Option<String> {
-    match fs::file_read_optional_string(path) {
-        Ok(contents) => contents,
-        Err(err) => {
-            log::error!("{}", err);
-            None
+/// Forward a mail to PBS's notification system
+fn forward_for_pbs(mail: &[u8], has_pve: bool) -> Result<(), Error> {
+    let config = if Path::new(PBS_PUB_NOTIFICATION_CFG_FILENAME).exists() {
+        let config = attempt_file_read(PBS_PUB_NOTIFICATION_CFG_FILENAME).unwrap_or_default();
+        let priv_config = attempt_file_read(PBS_PRIV_NOTIFICATION_CFG_FILENAME).unwrap_or_default();
+
+        Config::new(&config, &priv_config)?
+    } else {
+        // TODO: This can be removed once PBS has full notification integration
+        let mut config = Config::new("", "")?;
+        if !has_pve {
+            proxmox_notify::api::sendmail::add_endpoint(
+                &mut config,
+                &SendmailConfig {
+                    name: "default-target".to_string(),
+                    mailto_user: Some(vec!["root@pam".to_string()]),
+                    ..Default::default()
+                },
+            )?;
+
+            proxmox_notify::api::matcher::add_matcher(
+                &mut config,
+                &MatcherConfig {
+                    name: "default-matcher".to_string(),
+                    target: Some(vec!["default-target".to_string()]),
+                    ..Default::default()
+                },
+            )?;
         }
-    }
+        config
+    };
+
+    proxmox_notify::context::set_context(&PBS_CONTEXT);
+    forward_common(mail, &config)?;
+
+    Ok(())
 }
 
 fn main() {
@@ -135,40 +127,31 @@ fn main() {
         log::LevelFilter::Info,
         Some("proxmox-mail-forward"),
     ) {
-        eprintln!("unable to inititialize syslog - {}", err);
+        eprintln!("unable to initialize syslog: {err}");
     }
 
-    let pbs_user_cfg_content = attempt_file_read(PBS_USER_CFG_FILENAME);
-    let pve_user_cfg_content = attempt_file_read(PVE_USER_CFG_FILENAME);
-    let pve_datacenter_cfg_content = attempt_file_read(PVE_DATACENTER_CFG_FILENAME);
-
-    let real_uid = nix::unistd::getuid();
-    if let Err(err) = nix::unistd::setresuid(real_uid, real_uid, real_uid) {
-        log::error!(
-            "mail forward failed: unable to set effective uid to {}: {}",
-            real_uid,
-            err
-        );
-        return;
-    }
+    // Read the mail that is to be forwarded from stdin
+    match read_stdin() {
+        Ok(mail) => {
+            let mut has_pve = false;
 
-    let pbs_mail_to = pbs_user_cfg_content.and_then(|content| get_pbs_mail_to(&content));
-    let pve_mail_to = pve_user_cfg_content.and_then(|content| get_pve_mail_to(&content));
-    let pve_mail_from = pve_datacenter_cfg_content.and_then(|content| get_pve_mail_from(&content));
-
-    let mail_from = pve_mail_from.unwrap_or_else(|| "root".to_string());
+            // Assume a PVE installation if /etc/pve exists
+            if Path::new(PVE_CFG_PATH).exists() {
+                has_pve = true;
+                if let Err(err) = forward_for_pve(&mail) {
+                    log::error!("could not forward mail for Proxmox VE: {err}");
+                }
+            }
 
-    let mut mail_to = vec![];
-    if let Some(pve_mail_to) = pve_mail_to {
-        mail_to.push(pve_mail_to);
-    }
-    if let Some(pbs_mail_to) = pbs_mail_to {
-        if !mail_to.contains(&pbs_mail_to) {
-            mail_to.push(pbs_mail_to);
+            // Assume a PBS installation if /etc/proxmox-backup exists
+            if Path::new(PBS_CFG_PATH).exists() {
+                if let Err(err) = forward_for_pbs(&mail, has_pve) {
+                    log::error!("could not forward mail for Proxmox Backup Server: {err}");
+                }
+            }
+        }
+        Err(err) => {
+            log::error!("could not read mail from STDIN: {err}")
         }
-    }
-
-    if let Err(err) = forward_mail(mail_from, mail_to) {
-        log::error!("mail forward failed: {}", err);
     }
 }
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH v2 proxmox-mail-forward 52/52] update d/control
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (50 preceding siblings ...)
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-mail-forward 51/52] feed forwarded mails into proxmox_notify Lukas Wagner
@ 2023-11-14 13:00 ` Lukas Wagner
  2023-11-16 11:57 ` [pve-devel] [PATCH widget-toolkit 0/2] follow-ups for notification series Dominik Csapak
                   ` (2 subsequent siblings)
  54 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-14 13:00 UTC (permalink / raw)
  To: pve-devel

proxmox-schema and proxmox-section config is not required anymore.
add new dependency to proxmox-notify.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    Changes v2 -> v3:
      - new in v3

 debian/control | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/debian/control b/debian/control
index 43af70e..eb83a32 100644
--- a/debian/control
+++ b/debian/control
@@ -6,8 +6,10 @@ Build-Depends: cargo:native,
                librust-anyhow-1+default-dev,
                librust-log-0.4+default-dev (>= 0.4.17~~),
                librust-nix-0.26+default-dev,
-               librust-proxmox-schema-1+default-dev (>= 1.3~~),
-               librust-proxmox-section-config-1+default-dev (>= 1.0.2-~~),
+               librust-proxmox-notify-0.2+default-dev,
+               librust-proxmox-notify-0.2+mail-forwarder-dev,
+               librust-proxmox-notify-0.2+pbs-context-dev,
+               librust-proxmox-notify-0.2+pve-context-dev,
                librust-proxmox-sys-0.5+default-dev,
                librust-serde-1+default-dev,
                librust-serde-1+derive-dev,
-- 
2.39.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] applied: [PATCH v2 debcargo-conf 01/52] cherry-pick chumsky 0.9.2 from debian unstable
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 01/52] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
@ 2023-11-14 16:15   ` Thomas Lamprecht
  0 siblings, 0 replies; 65+ messages in thread
From: Thomas Lamprecht @ 2023-11-14 16:15 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner

Am 14/11/2023 um 13:59 schrieb Lukas Wagner:
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
>  src/chumsky/debian/changelog               |  5 +++
>  src/chumsky/debian/copyright               | 39 +++++++++++++++++
>  src/chumsky/debian/copyright.debcargo.hint | 51 ++++++++++++++++++++++
>  src/chumsky/debian/debcargo.toml           |  2 +
>  4 files changed, 97 insertions(+)
>  create mode 100644 src/chumsky/debian/changelog
>  create mode 100644 src/chumsky/debian/copyright
>  create mode 100644 src/chumsky/debian/copyright.debcargo.hint
>  create mode 100644 src/chumsky/debian/debcargo.toml
> 
>

applied, thanks!




^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] applied: [PATCH v2 debcargo-conf 02/52] update lettre to 0.11.1
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 02/52] update lettre to 0.11.1 Lukas Wagner
@ 2023-11-14 16:15   ` Thomas Lamprecht
  0 siblings, 0 replies; 65+ messages in thread
From: Thomas Lamprecht @ 2023-11-14 16:15 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner

Am 14/11/2023 um 13:59 schrieb Lukas Wagner:
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
>  src/lettre/debian/changelog                   | 10 +++
>  .../debian/patches/downgrade_fastrand.patch   | 13 ++++
>  .../debian/patches/downgrade_idna.patch       | 13 ++++
>  src/lettre/debian/patches/downgrade_url.patch | 13 ++++
>  .../patches/remove_unused_features.patch      | 69 ++++++++++---------
>  src/lettre/debian/patches/series              |  4 +-
>  .../patches/upgrade_quoted_printable.patch    | 13 ----
>  7 files changed, 88 insertions(+), 47 deletions(-)
>  create mode 100644 src/lettre/debian/patches/downgrade_fastrand.patch
>  create mode 100644 src/lettre/debian/patches/downgrade_idna.patch
>  create mode 100644 src/lettre/debian/patches/downgrade_url.patch
>  delete mode 100644 src/lettre/debian/patches/upgrade_quoted_printable.patch
> 
>

applied, thanks!




^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH manager] ui: fix backup job create
  2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 31/52] ui: vzdump: remove left-overs from target/policy based notifications Lukas Wagner
@ 2023-11-16 11:52   ` Dominik Csapak
  0 siblings, 0 replies; 65+ messages in thread
From: Dominik Csapak @ 2023-11-16 11:52 UTC (permalink / raw)
  To: pve-devel

'delete' is only possible for editing jobs, not creating them

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
follow up to lukas patches 
 www/manager6/dc/Backup.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/www/manager6/dc/Backup.js b/www/manager6/dc/Backup.js
index e1c76a1d..9aae4090 100644
--- a/www/manager6/dc/Backup.js
+++ b/www/manager6/dc/Backup.js
@@ -39,8 +39,10 @@ Ext.define('PVE.dc.BackupEdit', {
 	    // Get rid of new-old parameters for notification settings.
 	    // These should only be set for those selected few who ran
 	    // pve-manager from pvetest.
-	    Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-policy' });
-	    Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-target' });
+	    if (!isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-policy' });
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-target' });
+	    }
 
 	    if (!values.id && isCreate) {
 		values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
-- 
2.30.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH widget-toolkit 0/2] follow-ups for notification series
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (51 preceding siblings ...)
  2023-11-14 13:00 ` [pve-devel] [PATCH v2 proxmox-mail-forward 52/52] update d/control Lukas Wagner
@ 2023-11-16 11:57 ` Dominik Csapak
  2023-11-16 11:57   ` [pve-devel] [PATCH widget-toolkit 1/2] notification matcher: improve handling empty and invalid values Dominik Csapak
  2023-11-16 11:57   ` [pve-devel] [PATCH widget-toolkit 2/2] notification matcher: improve wording for mode Dominik Csapak
  2023-11-17  8:41 ` [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Dominik Csapak
  2023-11-17 15:31 ` [pve-devel] applied-series: " Thomas Lamprecht
  54 siblings, 2 replies; 65+ messages in thread
From: Dominik Csapak @ 2023-11-16 11:57 UTC (permalink / raw)
  To: pve-devel

here a few things i could fix, sorry for being so late with this

there are still a few non optimal things in the ux, but nothing i could
fix in a quick manner

not that my fixes are just to make it work properly and the most
important UX issues. There are still some things that can be improved
(UX as well as code wise).

Dominik Csapak (2):
  notification matcher: improve handling empty and invalid values
  notification matcher: improve wording for mode

 src/window/NotificationMatcherEdit.js | 138 ++++++++++++++++++--------
 1 file changed, 99 insertions(+), 39 deletions(-)

-- 
2.30.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH widget-toolkit 1/2] notification matcher: improve handling empty and invalid values
  2023-11-16 11:57 ` [pve-devel] [PATCH widget-toolkit 0/2] follow-ups for notification series Dominik Csapak
@ 2023-11-16 11:57   ` Dominik Csapak
  2023-11-16 11:57   ` [pve-devel] [PATCH widget-toolkit 2/2] notification matcher: improve wording for mode Dominik Csapak
  1 sibling, 0 replies; 65+ messages in thread
From: Dominik Csapak @ 2023-11-16 11:57 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/window/NotificationMatcherEdit.js | 84 ++++++++++++++++++++++++++-
 1 file changed, 81 insertions(+), 3 deletions(-)

diff --git a/src/window/NotificationMatcherEdit.js b/src/window/NotificationMatcherEdit.js
index c0dfa34..0f29203 100644
--- a/src/window/NotificationMatcherEdit.js
+++ b/src/window/NotificationMatcherEdit.js
@@ -288,6 +288,35 @@ Ext.define('Proxmox.panel.NotificationRulesEditPanel', {
     xtype: 'pmxNotificationMatchRulesEditPanel',
     mixins: ['Proxmox.Mixin.CBind'],
 
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	// we want to also set the empty value, but 'bind' does not do that so
+	// we have to set it then (and only then) to get the correct value in
+	// the tree
+	control: {
+	    'field': {
+		change: function(cmp) {
+		    let me = this;
+		    let vm = me.getViewModel();
+		    if (cmp.field) {
+			let record = vm.get('selectedRecord');
+			if (!record) {
+			    return;
+			}
+			let data = Ext.apply({}, record.get('data'));
+			let value = cmp.getValue();
+			// only update if the value is empty (or empty array)
+			if (!value || !value.length) {
+			    data[cmp.field] = value;
+			    record.set({ data });
+			}
+		    }
+		},
+	    },
+	},
+    },
+
     viewModel: {
 	data: {
 	    selectedRecord: null,
@@ -569,20 +598,32 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTree', {
 
 	switch (type) {
 	    case 'match-severity': {
-		let v = data.value.join(', ');
+		let v = data.value;
+		if (Ext.isArray(data.value)) {
+		    v = data.value.join(', ');
+		}
 		text = Ext.String.format(gettext("Match severity: {0}"), v);
 		iconCls = 'fa fa-exclamation';
+		if (!v) {
+		    iconCls += ' critical';
+		}
 	    } break;
 	    case 'match-field': {
 		let field = data.field;
 		let value = data.value;
 		text = Ext.String.format(gettext("Match field: {0}={1}"), field, value);
 		iconCls = 'fa fa-cube';
+		if (!field || !value) {
+		    iconCls += ' critical';
+		}
 	    } break;
 	    case 'match-calendar': {
 		let v = data.value;
 		text = Ext.String.format(gettext("Match calendar: {0}"), v);
 		iconCls = 'fa fa-calendar-o';
+		if (!v || !v.length) {
+		    iconCls += ' critical';
+		}
 	    } break;
 	    case 'mode':
 		if (data.value === 'all') {
@@ -628,6 +669,15 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTree', {
 	    getValue: function() {
 		return this.value;
 	    },
+	    getErrors: function() {
+		for (const matcher of this.value ?? []) {
+		    let matches = matcher.match(/^([^:]+):([^=]+)=(.+)$/);
+		    if (!matches) {
+			return [""]; // fake error for validation
+		    }
+		}
+		return [];
+	    },
 	    getSubmitValue: function() {
 		let value = this.value;
 		if (!value) {
@@ -647,6 +697,14 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTree', {
 	    getValue: function() {
 		return this.value;
 	    },
+	    getErrors: function() {
+		for (const severities of this.value ?? []) {
+		    if (!severities) {
+			return [""]; // fake error for validation
+		    }
+		}
+		return [];
+	    },
 	    getSubmitValue: function() {
 		let value = this.value;
 		if (!value) {
@@ -684,6 +742,14 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTree', {
 	    getValue: function() {
 		return this.value;
 	    },
+	    getErrors: function() {
+		for (const timespan of this.value ?? []) {
+		    if (!timespan) {
+			return [""]; // fake error for validation
+		    }
+		}
+		return [];
+	    },
 	    getSubmitValue: function() {
 		let value = this.value;
 		return value;
@@ -712,10 +778,14 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTree', {
 
 		switch (type) {
 		    case 'match-field':
-			matchFieldStmts.push(`${data.type}:${data.field}=${data.value}`);
+			matchFieldStmts.push(`${data.type}:${data.field ?? ''}=${data.value ?? ''}`);
 			break;
 		    case 'match-severity':
-			matchSeverityStmts.push(data.value.join(','));
+			if (Ext.isArray(data.value)) {
+			    matchSeverityStmts.push(data.value.join(','));
+			} else {
+			    matchSeverityStmts.push(data.value);
+			}
 			break;
 		    case 'match-calendar':
 			matchCalendarStmts.push(data.value);
@@ -780,6 +850,9 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTree', {
 		node.remove(true);
 	    }
 
+	    if (!value) {
+		return;
+	    }
 	    let records = value.map(parseMatchField);
 
 	    let rootNode = treeStore.getRootNode();
@@ -979,6 +1052,7 @@ Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
 	    isFormField: false,
 	    allowBlank: false,
 	    submitValue: false,
+	    field: 'type',
 
 	    bind: {
 		hidden: '{!typeIsMatchField}',
@@ -999,6 +1073,7 @@ Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
 	    allowBlank: false,
 	    editable: true,
 	    displayField: 'key',
+	    field: 'field',
 	    bind: {
 		hidden: '{!typeIsMatchField}',
 		disabled: '{!typeIsMatchField}',
@@ -1017,6 +1092,7 @@ Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
 	    isFormField: false,
 	    submitValue: false,
 	    allowBlank: false,
+	    field: 'value',
 	    bind: {
 		hidden: '{!typeIsMatchField}',
 		disabled: '{!typeIsMatchField}',
@@ -1029,6 +1105,7 @@ Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
 	    isFormField: false,
 	    allowBlank: true,
 	    multiSelect: true,
+	    field: 'value',
 
 	    bind: {
 		value: '{matchSeverityValue}',
@@ -1050,6 +1127,7 @@ Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
 	    allowBlank: false,
 	    editable: true,
 	    displayField: 'key',
+	    field: 'value',
 
 	    bind: {
 		value: '{matchCalendarValue}',
-- 
2.30.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] [PATCH widget-toolkit 2/2] notification matcher: improve wording for mode
  2023-11-16 11:57 ` [pve-devel] [PATCH widget-toolkit 0/2] follow-ups for notification series Dominik Csapak
  2023-11-16 11:57   ` [pve-devel] [PATCH widget-toolkit 1/2] notification matcher: improve handling empty and invalid values Dominik Csapak
@ 2023-11-16 11:57   ` Dominik Csapak
  1 sibling, 0 replies; 65+ messages in thread
From: Dominik Csapak @ 2023-11-16 11:57 UTC (permalink / raw)
  To: pve-devel

by removing the 'invert' checkbox and instead show the 4 modes possible,
we still assemble/parse the invert for the backend

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/window/NotificationMatcherEdit.js | 54 +++++++++------------------
 1 file changed, 18 insertions(+), 36 deletions(-)

diff --git a/src/window/NotificationMatcherEdit.js b/src/window/NotificationMatcherEdit.js
index 0f29203..5a88288 100644
--- a/src/window/NotificationMatcherEdit.js
+++ b/src/window/NotificationMatcherEdit.js
@@ -516,35 +516,22 @@ Ext.define('Proxmox.panel.NotificationRulesEditPanel', {
 		    let me = this;
 		    let record = me.get('selectedRecord');
 		    let currentData = record.get('data');
+		    let invert = false;
+		    if (value.startsWith('not')) {
+			value = value.substring(3);
+			invert = true;
+		    }
 		    record.set({
 			data: {
 			    ...currentData,
 			    value,
+			    invert,
 			},
 		    });
 		},
 		get: function(record) {
-		    return record?.get('data')?.value;
-		},
-	    },
-	    invertMatch: {
-		bind: {
-		    bindTo: '{selectedRecord}',
-		    deep: true,
-		},
-		set: function(value) {
-		    let me = this;
-		    let record = me.get('selectedRecord');
-		    let currentData = record.get('data');
-		    record.set({
-			data: {
-			    ...currentData,
-			    invert: value,
-			},
-		    });
-		},
-		get: function(record) {
-		    return record?.get('data')?.invert;
+		    let prefix = record?.get('data').invert ? 'not' : '';
+		    return prefix + record?.get('data')?.value;
 		},
 	    },
 	},
@@ -791,8 +778,12 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTree', {
 			matchCalendarStmts.push(data.value);
 			break;
 		    case 'mode':
-			modeStmt = data.value;
-			invertMatchStmt = data.invert;
+			if (data.value.startsWith('not')) {
+			    modeStmt = data.value.substring(3); // after 'not''
+			    invertMatchStmt = true;
+			} else {
+			    modeStmt = data.value;
+			}
 			break;
 		}
 
@@ -1004,9 +995,13 @@ Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
 	    allowBlank: false,
 	    isFormField: false,
 
+	    matchFieldWidth: false,
+
 	    comboItems: [
 		['all', gettext('All rules match')],
 		['any', gettext('Any rule matches')],
+		['notall', gettext('At least one rule does not match')],
+		['notany', gettext('No rule matches')],
 	    ],
 	    bind: {
 		hidden: '{!showMatchingMode}',
@@ -1014,19 +1009,6 @@ Ext.define('Proxmox.panel.NotificationMatchRuleSettings', {
 		value: '{rootMode}',
 	    },
 	},
-	{
-	    xtype: 'proxmoxcheckbox',
-	    fieldLabel: gettext('Invert match'),
-	    isFormField: false,
-	    uncheckedValue: 0,
-	    defaultValue: 0,
-	    bind: {
-		hidden: '{!showMatchingMode}',
-		disabled: '{!showMatchingMode}',
-		value: '{invertMatch}',
-	    },
-
-	},
 	{
 	    xtype: 'proxmoxKVComboBox',
 	    fieldLabel: gettext('Node type'),
-- 
2.30.2





^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (52 preceding siblings ...)
  2023-11-16 11:57 ` [pve-devel] [PATCH widget-toolkit 0/2] follow-ups for notification series Dominik Csapak
@ 2023-11-17  8:41 ` Dominik Csapak
  2023-11-20  9:03   ` Lukas Wagner
  2023-11-17 15:31 ` [pve-devel] applied-series: " Thomas Lamprecht
  54 siblings, 1 reply; 65+ messages in thread
From: Dominik Csapak @ 2023-11-17  8:41 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner

one additional comment to the ux:

the match field could use a bit of improvement:

the docs describe the current fields, but for users that
don't deep dive into the docs first it may be confusing having
a simple text input field for that

at least when the type is 'exact' we could offer a
dropdown for the 'type' value (can still be editable)

in the mid/long term i think having a backend generated list
of those somehow would make sense (i.e. some kind
of 'registering' and an endpoint that lists the types,
or other metadata) but i think that was planned by you anyway
(or something along those lines?)

also it would be good to have a link to the docs on the
filter edit panel to the relevant section
(e.g. for such things as the types/metadata/etc.)




^ permalink raw reply	[flat|nested] 65+ messages in thread

* [pve-devel] applied-series: [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail
  2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
                   ` (53 preceding siblings ...)
  2023-11-17  8:41 ` [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Dominik Csapak
@ 2023-11-17 15:31 ` Thomas Lamprecht
  54 siblings, 0 replies; 65+ messages in thread
From: Thomas Lamprecht @ 2023-11-17 15:31 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner

applied the whole series, much thanks, really nice work here!

Also applied the follow-ups from Dominik, thanks for helping out!




^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail
  2023-11-17  8:41 ` [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Dominik Csapak
@ 2023-11-20  9:03   ` Lukas Wagner
  2023-11-20  9:11     ` Dominik Csapak
  2023-11-20  9:11     ` Thomas Lamprecht
  0 siblings, 2 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-20  9:03 UTC (permalink / raw)
  To: Dominik Csapak, Proxmox VE development discussion



On 11/17/23 09:41, Dominik Csapak wrote:
> one additional comment to the ux:
> 
> the match field could use a bit of improvement:
> 
> the docs describe the current fields, but for users that
> don't deep dive into the docs first it may be confusing having
> a simple text input field for that
> 
> at least when the type is 'exact' we could offer a
> dropdown for the 'type' value (can still be editable)

I'm actually evaluating if it would make sense to have a separate
'match-type' match rule. That one would use the 'type' field
in the notification metadata, but would be a bit more obvious to use for 
the user.

I think filtering notifications by type is probably going to be one of 
the most common things a user wants to do. Right now, if a user only
cares about a subset of notification types, e.g. backups and 
replication, a matcher would could look like this:

matcher: test
     mode any
     match-field exact:type=vzdump
     match-field exact:type=replication

Here, we have to use the 'any' mode, so this is awkward, if we also want 
to filter based on severity.


Alternatively, a regex-based solution could be used:

matcher: test2
     match-field regex:type=^(vzdump|replication)$

This requires regex knowledge, and the resulting regular expression can 
become very long if one wants to match many other types of notifications.

A 'match-type' rule would simplify this by simply allowing one to list 
all notification types on wants to match, eg.

matcher: test3
     match-type vzdump,replication


For this 'specialized' match-rule we could easily add a list of known
values in a drop-down (first hardcoded in the UI, later retrieved via an 
API call)

> 
> in the mid/long term i think having a backend generated list
> of those somehow would make sense (i.e. some kind
> of 'registering' and an endpoint that lists the types,
> or other metadata) but i think that was planned by you anyway
> (or something along those lines?)
> 

Yup, that's on my todo-list, having something like a notification
registry, where we can register notification types, their templates
(probably separate templates for plain text/HTML, as I don't quite like
how rendering from a single template turned out in the end, too finicky 
and lacking flexibility), and the params needed to render the template.
That way we can enumerate them in the UI quite easily (and
also auto-generate the list of notifications in the docs)

> also it would be good to have a link to the docs on the
> filter edit panel to the relevant section
> (e.g. for such things as the types/metadata/etc.)
> 
Noted, will be added in a follow-up.

-- 
- Lukas




^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail
  2023-11-20  9:03   ` Lukas Wagner
@ 2023-11-20  9:11     ` Dominik Csapak
  2023-11-20  9:49       ` Lukas Wagner
  2023-11-20  9:11     ` Thomas Lamprecht
  1 sibling, 1 reply; 65+ messages in thread
From: Dominik Csapak @ 2023-11-20  9:11 UTC (permalink / raw)
  To: Lukas Wagner, Proxmox VE development discussion

On 11/20/23 10:03, Lukas Wagner wrote:
> 
> 
> On 11/17/23 09:41, Dominik Csapak wrote:
>> one additional comment to the ux:
>>
>> the match field could use a bit of improvement:
>>
>> the docs describe the current fields, but for users that
>> don't deep dive into the docs first it may be confusing having
>> a simple text input field for that
>>
>> at least when the type is 'exact' we could offer a
>> dropdown for the 'type' value (can still be editable)
> 
> I'm actually evaluating if it would make sense to have a separate
> 'match-type' match rule. That one would use the 'type' field
> in the notification metadata, but would be a bit more obvious to use for the user.
> 
> I think filtering notifications by type is probably going to be one of the most common things a user 
> wants to do. Right now, if a user only
> cares about a subset of notification types, e.g. backups and replication, a matcher would could look 
> like this:
> 
> matcher: test
>      mode any
>      match-field exact:type=vzdump
>      match-field exact:type=replication
> 
> Here, we have to use the 'any' mode, so this is awkward, if we also want to filter based on severity.
> 
> 
> Alternatively, a regex-based solution could be used:
> 
> matcher: test2
>      match-field regex:type=^(vzdump|replication)$
> 
> This requires regex knowledge, and the resulting regular expression can become very long if one 
> wants to match many other types of notifications.
> 
> A 'match-type' rule would simplify this by simply allowing one to list all notification types on 
> wants to match, eg.
> 
> matcher: test3
>      match-type vzdump,replication
> 
> 
> For this 'specialized' match-rule we could easily add a list of known
> values in a drop-down (first hardcoded in the UI, later retrieved via an API call)

maybe having it more generic and making it 'match-list' with a property and multiple
values would make more sense? altough that would be very similar to allow
nesting of modes e.g.

mode all
  - mode any
    - match-field exact:type=foo
    - match-field exact:type=foo
  - match-severity:...

would that be feasible to do (probably not in the short term)?

> 
>>
>> in the mid/long term i think having a backend generated list
>> of those somehow would make sense (i.e. some kind
>> of 'registering' and an endpoint that lists the types,
>> or other metadata) but i think that was planned by you anyway
>> (or something along those lines?)
>>
> 
> Yup, that's on my todo-list, having something like a notification
> registry, where we can register notification types, their templates
> (probably separate templates for plain text/HTML, as I don't quite like
> how rendering from a single template turned out in the end, too finicky and lacking flexibility), 
> and the params needed to render the template.
> That way we can enumerate them in the UI quite easily (and
> also auto-generate the list of notifications in the docs)
> 
>> also it would be good to have a link to the docs on the
>> filter edit panel to the relevant section
>> (e.g. for such things as the types/metadata/etc.)
>>
> Noted, will be added in a follow-up.
> 





^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail
  2023-11-20  9:03   ` Lukas Wagner
  2023-11-20  9:11     ` Dominik Csapak
@ 2023-11-20  9:11     ` Thomas Lamprecht
  1 sibling, 0 replies; 65+ messages in thread
From: Thomas Lamprecht @ 2023-11-20  9:11 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner, Dominik Csapak

Am 20/11/2023 um 10:03 schrieb Lukas Wagner:
> On 11/17/23 09:41, Dominik Csapak wrote:
>> one additional comment to the ux:
>>
>> the match field could use a bit of improvement:
>>
>> the docs describe the current fields, but for users that
>> don't deep dive into the docs first it may be confusing having
>> a simple text input field for that
>>
>> at least when the type is 'exact' we could offer a
>> dropdown for the 'type' value (can still be editable)
> 
> I'm actually evaluating if it would make sense to have a separate
> 'match-type' match rule. That one would use the 'type' field
> in the notification metadata, but would be a bit more obvious to use for 
> the user.
> 
> I think filtering notifications by type is probably going to be one of 
> the most common things a user wants to do. Right now, if a user only
> cares about a subset of notification types, e.g. backups and 
> replication, a matcher would could look like this:
> 

match-type sounds like it really could be nice, and as you say, one of the
more commonly used things.
Until there's a static (api delivered) list of known types, we could also
just document them in pve-docs, for now there aren't that many IIRC.




^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail
  2023-11-20  9:11     ` Dominik Csapak
@ 2023-11-20  9:49       ` Lukas Wagner
  0 siblings, 0 replies; 65+ messages in thread
From: Lukas Wagner @ 2023-11-20  9:49 UTC (permalink / raw)
  To: Dominik Csapak, Proxmox VE development discussion



On 11/20/23 10:11, Dominik Csapak wrote:
>> A 'match-type' rule would simplify this by simply allowing one to list 
>> all notification types on wants to match, eg.
>>
>> matcher: test3
>>      match-type vzdump,replication
>>
>>
>> For this 'specialized' match-rule we could easily add a list of known
>> values in a drop-down (first hardcoded in the UI, later retrieved via 
>> an API call)
> 
> maybe having it more generic and making it 'match-list' with a property 
> and multiple
> values would make more sense? altough that would be very similar to allow
> nesting of modes e.g.
> 
> mode all
>   - mode any
>     - match-field exact:type=foo
>     - match-field exact:type=foo
>   - match-severity:...
> 
> would that be feasible to do (probably not in the short term)?
> 

This could easily be implemented as an extension to the 'match-field' 
rule by adding another mode:

match-field list:type=vzdump,replication

Maybe 'list' is not the best name for this mode, but you get the idea.

Theoretically, we could have this 'generic' approach in the backend,
while also adding a 'match-type' 'virtual' node type (using the generic 
list-matching) in the UI to make things obvious for the user.

The 'composable' nesting of nodes would be added independently of this.
The implementation in proxmox_notify will be pretty easy, but the UI/API 
handler part could be a bit tricky - since we have to add/modify/delete
multiple matchers 'atomically' in a single transaction (when pressing
OK in the GUI).

So either a 'match-type' or the list-extension would definitely be
nice until we have that - and also afterwards, because you can skip
nested nodes in some cases if you have this feature.

-- 
- Lukas




^ permalink raw reply	[flat|nested] 65+ messages in thread

end of thread, other threads:[~2023-11-20  9:49 UTC | newest]

Thread overview: 65+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-11-14 12:59 [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 01/52] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
2023-11-14 16:15   ` [pve-devel] applied: " Thomas Lamprecht
2023-11-14 12:59 ` [pve-devel] [PATCH v2 debcargo-conf 02/52] update lettre to 0.11.1 Lukas Wagner
2023-11-14 16:15   ` [pve-devel] applied: " Thomas Lamprecht
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 03/52] notify: introduce Error::Generic Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 04/52] notify: factor out notification content into its own type Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 05/52] notify: replace filters and groups with matcher-based system Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 06/52] notify: add calendar matcher Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 07/52] notify: matcher: introduce common trait for match directives Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 08/52] notify: let a matcher always match if it has no matching directives Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 09/52] sys: email: add `forward` Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 10/52] notify: add mechanisms for email message forwarding Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 11/52] notify: add PVE/PBS context Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 12/52] notify: add 'smtp' endpoint Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 13/52] notify: add api for smtp endpoints Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 14/52] notify: add 'disable' parameter for matchers and targets Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox 15/52] notify: add built-in config and 'origin' parameter Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 16/52] notify: adapt to new matcher-based notification routing Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 17/52] notify: add bindings for smtp API calls Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 18/52] pve-rs: notify: remove notify_context for PVE Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 19/52] notify: add 'disable' parameter Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-perl-rs 20/52] notify: support 'origin' paramter Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-cluster 21/52] notify: adapt to matcher based notification system Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-guest-common 22/52] vzdump: deprecate mailto/mailnotification/notification-{target, policy} Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-ha-manager 23/52] env: switch to matcher-based notification system Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 24/52] api: notification: remove notification groups Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 25/52] api: notification: add new matcher-based notification API Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 26/52] ui: dc: remove unneeded notification events panel Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 27/52] vzdump: adapt to new matcher based notification system Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 28/52] api: apt: adapt to matcher-based notifications Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 29/52] api: replication: adapt to matcher-based notification system Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 30/52] test: fix vzdump notification test Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 31/52] ui: vzdump: remove left-overs from target/policy based notifications Lukas Wagner
2023-11-16 11:52   ` [pve-devel] [PATCH manager] ui: fix backup job create Dominik Csapak
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 32/52] ui: dc: config: show notification panel again Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 33/52] notify: add API routes for smtp endpoints Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 34/52] api: notification: add disable and origin params Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-manager 35/52] api: notification: simplify ACLs for notification Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 36/52] notification ui: add target selector for matcher Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 37/52] notification ui: remove filter setting for targets Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 38/52] notification ui: remove notification groups Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 39/52] notification ui: rename filter to matcher Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 40/52] notification: matcher: add UI for matcher editing Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 41/52] notification ui: unprotected mailto-root target Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 42/52] noficiation: matcher edit: make 'field' an editable combobox Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 43/52] panel: notification: add gui for SMTP endpoints Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 44/52] notification ui: add enable checkbox for targets/matchers Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-widget-toolkit 45/52] notification ui: add column for 'origin' Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 46/52] notifications: update docs to for matcher-based notifications Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 47/52] notifications: document SMTP endpoints Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 48/52] notifications: document 'comment' option for targets/matchers Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 49/52] notifications: add documentation for system mail forwarding Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 pve-docs 50/52] notifications: change to simplified ACL structure Lukas Wagner
2023-11-14 12:59 ` [pve-devel] [PATCH v2 proxmox-mail-forward 51/52] feed forwarded mails into proxmox_notify Lukas Wagner
2023-11-14 13:00 ` [pve-devel] [PATCH v2 proxmox-mail-forward 52/52] update d/control Lukas Wagner
2023-11-16 11:57 ` [pve-devel] [PATCH widget-toolkit 0/2] follow-ups for notification series Dominik Csapak
2023-11-16 11:57   ` [pve-devel] [PATCH widget-toolkit 1/2] notification matcher: improve handling empty and invalid values Dominik Csapak
2023-11-16 11:57   ` [pve-devel] [PATCH widget-toolkit 2/2] notification matcher: improve wording for mode Dominik Csapak
2023-11-17  8:41 ` [pve-devel] [PATCH v2 many 00/52] revamp notifications; smtp endpoints; system mail Dominik Csapak
2023-11-20  9:03   ` Lukas Wagner
2023-11-20  9:11     ` Dominik Csapak
2023-11-20  9:49       ` Lukas Wagner
2023-11-20  9:11     ` Thomas Lamprecht
2023-11-17 15:31 ` [pve-devel] applied-series: " Thomas Lamprecht

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal