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
next prev 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