public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v4 proxmox 06/11] notify: add api for smtp endpoints
Date: Wed,  8 Nov 2023 16:40:00 +0100	[thread overview]
Message-ID: <20231108154005.895814-7-l.wagner@proxmox.com> (raw)
In-Reply-To: <20231108154005.895814-1-l.wagner@proxmox.com>

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





  parent reply	other threads:[~2023-11-08 15:40 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 ` Lukas Wagner [this message]
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20231108154005.895814-7-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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