From: Lukas Wagner Date: Tue, 14 Nov 2023 12:59:21 +0000 (+0100) Subject: notify: add api for smtp endpoints X-Git-Url: https://git.proxmox.com/?a=commitdiff_plain;h=20b290893aa264335b2fcf6323d85aba15da4186;p=proxmox.git notify: add api for smtp endpoints Signed-off-by: Lukas Wagner --- diff --git a/proxmox-notify/src/api/mod.rs b/proxmox-notify/src/api/mod.rs index 8042157a..762d448a 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, HttpE } } } + Ok(referrers) } @@ -148,6 +156,31 @@ fn get_referenced_entities(config: &Config, entity: &str) -> HashSet { expanded } +#[allow(unused)] +fn set_private_config_entry( + 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 00000000..bd9d7bb8 --- /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, 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 { + 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 9c92da04..a6899b42 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, - /// Filter to apply - #[serde(skip_serializing_if = "Option::is_none")] - pub filter: Option, } #[derive(Serialize, Deserialize)] @@ -105,7 +98,6 @@ pub struct SmtpConfig { pub enum DeleteableSmtpProperty { Author, Comment, - Filter, Mailto, MailtoUser, Password,