public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint
@ 2023-11-08 15:39 Lukas Wagner
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 debcargo-conf 01/11] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
                   ` (11 more replies)
  0 siblings, 12 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:39 UTC (permalink / raw)
  To: pve-devel

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

These patches require a couple of other patches that have not been applied yet:

  series: "overhaul notification system, use matchers instead of filters" [3], 
  pve-manager: "api: notifications: give targets and matchers their own ACL namespace" [4]
  pve-docs: "notifications: update docs to for matcher-based notifications" [5]

The first two patches were cherry-picked and rebased from the  'system mail 
forwarding' patch series from [2].  I decided to pull them in so that I can
already implement the mail forwarding part for SMTP targets.

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

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



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 (4):
  sys: email: add `forward`
  notify: add mechanisms for email message forwarding
  notify: add 'smtp' endpoint
  notify: add api for smtp endpoints

 Cargo.toml                                  |   2 +
 proxmox-notify/Cargo.toml                   |   6 +-
 proxmox-notify/src/api/mod.rs               |  33 ++
 proxmox-notify/src/api/smtp.rs              | 356 ++++++++++++++++++++
 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/gotify.rs      |   3 +
 proxmox-notify/src/endpoints/mod.rs         |   4 +
 proxmox-notify/src/endpoints/sendmail.rs    |  27 +-
 proxmox-notify/src/endpoints/smtp.rs        | 250 ++++++++++++++
 proxmox-notify/src/lib.rs                   |  57 ++++
 proxmox-sys/src/email.rs                    |  52 ++-
 13 files changed, 820 insertions(+), 19 deletions(-)
 create mode 100644 proxmox-notify/src/api/smtp.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


proxmox-perl-rs:

Lukas Wagner (1):
  notify: add bindings for smtp API calls

 common/src/notify.rs | 106 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)


pve-manager:

Lukas Wagner (1):
  notify: add API routes for smtp endpoints

 PVE/API2/Cluster/Notifications.pm | 323 ++++++++++++++++++++++++++++++
 1 file changed, 323 insertions(+)


proxmox-widget-toolkit:

Lukas Wagner (1):
  panel: notification: add gui for SMTP endpoints

 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


pve-docs:

Lukas Wagner (2):
  notifications: document SMTP endpoints
  notifications: document 'comment' option for targets/matchers

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


Summary over all repositories:
  32 files changed, 1765 insertions(+), 121 deletions(-)

-- 
murpp v0.4.0





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

* [pve-devel] [PATCH v4 debcargo-conf 01/11] cherry-pick chumsky 0.9.2 from debian unstable
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
@ 2023-11-08 15:39 ` Lukas Wagner
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 debcargo-conf 02/11] update lettre to 0.11.1 Lukas Wagner
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:39 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] 18+ messages in thread

* [pve-devel] [PATCH v4 debcargo-conf 02/11] update lettre to 0.11.1
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 debcargo-conf 01/11] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
@ 2023-11-08 15:39 ` Lukas Wagner
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 03/11] sys: email: add `forward` Lukas Wagner
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:39 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] 18+ messages in thread

* [pve-devel] [PATCH v4 proxmox 03/11] sys: email: add `forward`
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 debcargo-conf 01/11] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 debcargo-conf 02/11] update lettre to 0.11.1 Lukas Wagner
@ 2023-11-08 15:39 ` Lukas Wagner
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 04/11] notify: add mechanisms for email message forwarding Lukas Wagner
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:39 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] 18+ messages in thread

* [pve-devel] [PATCH v4 proxmox 04/11] notify: add mechanisms for email message forwarding
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (2 preceding siblings ...)
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 03/11] sys: email: add `forward` Lukas Wagner
@ 2023-11-08 15:39 ` Lukas Wagner
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 05/11] notify: add 'smtp' endpoint Lukas Wagner
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:39 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] 18+ messages in thread

* [pve-devel] [PATCH v4 proxmox 05/11] notify: add 'smtp' endpoint
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (3 preceding siblings ...)
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 04/11] notify: add mechanisms for email message forwarding Lukas Wagner
@ 2023-11-08 15:39 ` Lukas Wagner
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox 06/11] notify: add api for smtp endpoints Lukas Wagner
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:39 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 f2b4db5..8741b9b 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 }
 once_cell.workspace = true
@@ -28,7 +29,8 @@ 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"]
+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] 18+ messages in thread

* [pve-devel] [PATCH v4 proxmox 06/11] notify: add api for smtp endpoints
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (4 preceding siblings ...)
  2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 05/11] notify: add 'smtp' endpoint Lukas Wagner
@ 2023-11-08 15:40 ` Lukas Wagner
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox-perl-rs 07/11] notify: add bindings for smtp API calls Lukas Wagner
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:40 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] 18+ messages in thread

* [pve-devel] [PATCH v4 proxmox-perl-rs 07/11] notify: add bindings for smtp API calls
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (5 preceding siblings ...)
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox 06/11] notify: add api for smtp endpoints Lukas Wagner
@ 2023-11-08 15:40 ` Lukas Wagner
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-manager 08/11] notify: add API routes for smtp endpoints Lukas Wagner
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:40 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] 18+ messages in thread

* [pve-devel] [PATCH v4 pve-manager 08/11] notify: add API routes for smtp endpoints
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (6 preceding siblings ...)
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox-perl-rs 07/11] notify: add bindings for smtp API calls Lukas Wagner
@ 2023-11-08 15:40 ` Lukas Wagner
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox-widget-toolkit 09/11] panel: notification: add gui for SMTP endpoints Lukas Wagner
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:40 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 6ff6d89e..81a8c5af 100644
--- a/PVE/API2/Cluster/Notifications.pm
+++ b/PVE/API2/Cluster/Notifications.pm
@@ -193,6 +193,14 @@ __PACKAGE__->register_method ({
 		};
 	    }
 
+	    for my $target (@{$config->get_smtp_endpoints()}) {
+		push @$result, {
+		    name => $target->{name},
+		    comment => $target->{comment},
+		    type => 'smtp',
+		};
+	    }
+
 	    $result
 	};
 
@@ -757,6 +765,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] 18+ messages in thread

* [pve-devel] [PATCH v4 proxmox-widget-toolkit 09/11] panel: notification: add gui for SMTP endpoints
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (7 preceding siblings ...)
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-manager 08/11] notify: add API routes for smtp endpoints Lukas Wagner
@ 2023-11-08 15:40 ` Lukas Wagner
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-docs 10/11] notifications: document " Lukas Wagner
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:40 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] 18+ messages in thread

* [pve-devel] [PATCH v4 pve-docs 10/11] notifications: document SMTP endpoints
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (8 preceding siblings ...)
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox-widget-toolkit 09/11] panel: notification: add gui for SMTP endpoints Lukas Wagner
@ 2023-11-08 15:40 ` Lukas Wagner
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-docs 11/11] notifications: document 'comment' option for targets/matchers Lukas Wagner
  2023-11-08 15:52 ` [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Dietmar Maurer
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:40 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] 18+ messages in thread

* [pve-devel] [PATCH v4 pve-docs 11/11] notifications: document 'comment' option for targets/matchers
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (9 preceding siblings ...)
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-docs 10/11] notifications: document " Lukas Wagner
@ 2023-11-08 15:40 ` Lukas Wagner
  2023-11-08 15:52 ` [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Dietmar Maurer
  11 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2023-11-08 15:40 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] 18+ messages in thread

* Re: [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint
  2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
                   ` (10 preceding siblings ...)
  2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-docs 11/11] notifications: document 'comment' option for targets/matchers Lukas Wagner
@ 2023-11-08 15:52 ` Dietmar Maurer
  2023-11-09 10:23   ` Lukas Wagner
  11 siblings, 1 reply; 18+ messages in thread
From: Dietmar Maurer @ 2023-11-08 15:52 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner

> 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).

Isn't this totally unreliable? What if the server responds with a 
temporary error code? (An MTA retries several times).




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

* Re: [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint
  2023-11-08 15:52 ` [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Dietmar Maurer
@ 2023-11-09 10:23   ` Lukas Wagner
  2023-11-09 12:16     ` Dietmar Maurer
  0 siblings, 1 reply; 18+ messages in thread
From: Lukas Wagner @ 2023-11-09 10:23 UTC (permalink / raw)
  To: Dietmar Maurer, Proxmox VE development discussion

On 11/8/23 16:52, Dietmar Maurer wrote:
>> 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).
> 
> Isn't this totally unreliable? What if the server responds with a
> temporary error code? (An MTA retries several times).

The notification system has no mechanism yet for queuing/retries,
so yes, at the moment a SMTP endpoint would indeed be less reliable than 
a 'sendmail' endpoint. I'm not sure though if I would call it
'totally unreliable'.
The same thing applies for gotify/webhook endpoints - if the network or 
Gotify server is down, a notification cannot be sent.
A queuing/retry mechanism could be added at some point, but this would 
require some bigger changes, as the notification system is completely 
stateless right now.

-- 
- Lukas




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

* Re: [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint
  2023-11-09 10:23   ` Lukas Wagner
@ 2023-11-09 12:16     ` Dietmar Maurer
  2023-11-09 12:34       ` Lukas Wagner
  0 siblings, 1 reply; 18+ messages in thread
From: Dietmar Maurer @ 2023-11-09 12:16 UTC (permalink / raw)
  To: Lukas Wagner, Proxmox VE development discussion

> On 11/8/23 16:52, Dietmar Maurer wrote:
> >> 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).
> > 
> > Isn't this totally unreliable? What if the server responds with a
> > temporary error code? (An MTA retries several times).
> 
> The notification system has no mechanism yet for queuing/retries,
> so yes, at the moment a SMTP endpoint would indeed be less reliable than 
> a 'sendmail' endpoint. I'm not sure though if I would call it
> 'totally unreliable'.

This kind of notification system is quite popular for (PHP) web-sites contact 
form. I have seen many over-simplified implementation overs the years,
and yes, it is totally unreliable.

That is why we always used an MTA to deliver mails...




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

* Re: [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint
  2023-11-09 12:16     ` Dietmar Maurer
@ 2023-11-09 12:34       ` Lukas Wagner
  2023-11-09 13:10         ` Thomas Lamprecht
  0 siblings, 1 reply; 18+ messages in thread
From: Lukas Wagner @ 2023-11-09 12:34 UTC (permalink / raw)
  To: Dietmar Maurer, Proxmox VE development discussion; +Cc: Thomas Lamprecht

On 11/9/23 13:16, Dietmar Maurer wrote:
>> On 11/8/23 16:52, Dietmar Maurer wrote:
>>>> 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).
>>>
>>> Isn't this totally unreliable? What if the server responds with a
>>> temporary error code? (An MTA retries several times).
>>
>> The notification system has no mechanism yet for queuing/retries,
>> so yes, at the moment a SMTP endpoint would indeed be less reliable than
>> a 'sendmail' endpoint. I'm not sure though if I would call it
>> 'totally unreliable'.
> 
> This kind of notification system is quite popular for (PHP) web-sites contact
> form. I have seen many over-simplified implementation overs the years,
> and yes, it is totally unreliable.
> 
> That is why we always used an MTA to deliver mails...

I see. What would be your suggestion? To not have such a plugin at all?
I implemented this because it was explicitly mentioned by Thomas in the 
tracking bugzilla issue for an overhauled notification system [1].
Not having to configure Postfix if one wants to use an external
SMTP relay seems to be add quite a lot of value to the user (e.g. 
judging from [2] and [3])
As a compromise, maybe we could just add a note to the docs
that discusses the reliability aspects of 'sendmail' vs 'smtp'
endpoints?

[1] https://bugzilla.proxmox.com/show_bug.cgi?id=4156
[2] https://bugzilla.proxmox.com/show_bug.cgi?id=2965
[3] 
https://forum.proxmox.com/threads/get-postfix-to-send-notifications-email-externally.59940/

-- 
- Lukas




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

* Re: [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint
  2023-11-09 12:34       ` Lukas Wagner
@ 2023-11-09 13:10         ` Thomas Lamprecht
  2023-11-09 15:35           ` Dietmar Maurer
  0 siblings, 1 reply; 18+ messages in thread
From: Thomas Lamprecht @ 2023-11-09 13:10 UTC (permalink / raw)
  To: Lukas Wagner, Dietmar Maurer, Proxmox VE development discussion

Am 09/11/2023 um 13:34 schrieb Lukas Wagner:
> On 11/9/23 13:16, Dietmar Maurer wrote:
>>> On 11/8/23 16:52, Dietmar Maurer wrote:
>>>>> 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).
>>>>
>>>> Isn't this totally unreliable? What if the server responds with a
>>>> temporary error code? (An MTA retries several times).
>>>
>>> The notification system has no mechanism yet for queuing/retries,
>>> so yes, at the moment a SMTP endpoint would indeed be less reliable than
>>> a 'sendmail' endpoint. I'm not sure though if I would call it
>>> 'totally unreliable'.
>>
>> This kind of notification system is quite popular for (PHP) web-sites contact
>> form. I have seen many over-simplified implementation overs the years,
>> and yes, it is totally unreliable.
>>
>> That is why we always used an MTA to deliver mails...
> 
> I see. What would be your suggestion? To not have such a plugin at all?
> I implemented this because it was explicitly mentioned by Thomas in the 
> tracking bugzilla issue for an overhauled notification system [1].
> Not having to configure Postfix if one wants to use an external
> SMTP relay seems to be add quite a lot of value to the user (e.g. 
> judging from [2] and [3])


While Dietmar is definitively has a point, and making this more robust
in the long run would be good, I see the (re-)queuing and the availability
of the endpoints as a bit different things.

And as you say, like currently the request to gotify needs to get through,
the one to the SMTP serve does too.
Also, user-facing SMTP servers are most of the time not doing any grey
listing or rate limiting or the like, as the authentication itself is
enough proof that a legitimate user wants to send a mail anyway, so once
the MTA of the users' mail provider accepted it, they'll handle retries
with the target MTAs.

In the end one might indeed want to move the actual sending out to a daemon
(it's own, or if we really want to avoid an extra daemon then one of the
existing, frequently running, ones like pvescheduler or pvestatd) and the
library calls just generating a queue-entry file in, e.g., the
/var/spool/pve-notification directory.

But adding that would not require any breaking change w.r.t. configs or
the like, so while definitively good to have in the long run, it would not
need to be necessarily done now.

> As a compromise, maybe we could just add a note to the docs
> that discusses the reliability aspects of 'sendmail' vs 'smtp'
> endpoints?
> 

Sure, for now adding a general hint to the documentation that they are
send one-shot only would be good.




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

* Re: [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint
  2023-11-09 13:10         ` Thomas Lamprecht
@ 2023-11-09 15:35           ` Dietmar Maurer
  0 siblings, 0 replies; 18+ messages in thread
From: Dietmar Maurer @ 2023-11-09 15:35 UTC (permalink / raw)
  To: Thomas Lamprecht, Lukas Wagner, Proxmox VE development discussion

> > As a compromise, maybe we could just add a note to the docs
> > that discusses the reliability aspects of 'sendmail' vs 'smtp'
> > endpoints?
> > 
> 
> Sure, for now adding a general hint to the documentation that they are
> send one-shot only would be good.

Ok for me.




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

end of thread, other threads:[~2023-11-09 15:35 UTC | newest]

Thread overview: 18+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-11-08 15:39 [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Lukas Wagner
2023-11-08 15:39 ` [pve-devel] [PATCH v4 debcargo-conf 01/11] cherry-pick chumsky 0.9.2 from debian unstable Lukas Wagner
2023-11-08 15:39 ` [pve-devel] [PATCH v4 debcargo-conf 02/11] update lettre to 0.11.1 Lukas Wagner
2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 03/11] sys: email: add `forward` Lukas Wagner
2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 04/11] notify: add mechanisms for email message forwarding Lukas Wagner
2023-11-08 15:39 ` [pve-devel] [PATCH v4 proxmox 05/11] notify: add 'smtp' endpoint Lukas Wagner
2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox 06/11] notify: add api for smtp endpoints Lukas Wagner
2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox-perl-rs 07/11] notify: add bindings for smtp API calls Lukas Wagner
2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-manager 08/11] notify: add API routes for smtp endpoints Lukas Wagner
2023-11-08 15:40 ` [pve-devel] [PATCH v4 proxmox-widget-toolkit 09/11] panel: notification: add gui for SMTP endpoints Lukas Wagner
2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-docs 10/11] notifications: document " Lukas Wagner
2023-11-08 15:40 ` [pve-devel] [PATCH v4 pve-docs 11/11] notifications: document 'comment' option for targets/matchers Lukas Wagner
2023-11-08 15:52 ` [pve-devel] [PATCH v4 many 00/11] notifications: add SMTP endpoint Dietmar Maurer
2023-11-09 10:23   ` Lukas Wagner
2023-11-09 12:16     ` Dietmar Maurer
2023-11-09 12:34       ` Lukas Wagner
2023-11-09 13:10         ` Thomas Lamprecht
2023-11-09 15:35           ` Dietmar Maurer

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