forked from Proxmox/proxmox
notify: add api for smtp endpoints
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
This commit is contained in:
parent
53627a1952
commit
20b290893a
@ -1,3 +1,4 @@
|
|||||||
|
use serde::Serialize;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use proxmox_http_error::HttpError;
|
use proxmox_http_error::HttpError;
|
||||||
@ -10,6 +11,8 @@ pub mod gotify;
|
|||||||
pub mod matcher;
|
pub mod matcher;
|
||||||
#[cfg(feature = "sendmail")]
|
#[cfg(feature = "sendmail")]
|
||||||
pub mod sendmail;
|
pub mod sendmail;
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
pub mod smtp;
|
||||||
|
|
||||||
// We have our own, local versions of http_err and http_bail, because
|
// 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,
|
// 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();
|
exists = exists || gotify::get_endpoint(config, name).is_ok();
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
{
|
||||||
|
exists = exists || smtp::get_endpoint(config, name).is_ok();
|
||||||
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
http_bail!(NOT_FOUND, "endpoint '{name}' does not exist")
|
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)
|
Ok(referrers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +156,31 @@ fn get_referenced_entities(config: &Config, entity: &str) -> HashSet<String> {
|
|||||||
expanded
|
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)]
|
#[cfg(test)]
|
||||||
mod test_helpers {
|
mod test_helpers {
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
356
proxmox-notify/src/api/smtp.rs
Normal file
356
proxmox-notify/src/api/smtp.rs
Normal file
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
@ -58,10 +58,6 @@ pub enum SmtpMode {
|
|||||||
optional: true,
|
optional: true,
|
||||||
schema: COMMENT_SCHEMA,
|
schema: COMMENT_SCHEMA,
|
||||||
},
|
},
|
||||||
filter: {
|
|
||||||
optional: true,
|
|
||||||
schema: ENTITY_NAME_SCHEMA,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)]
|
)]
|
||||||
#[derive(Debug, Serialize, Deserialize, Updater, Default)]
|
#[derive(Debug, Serialize, Deserialize, Updater, Default)]
|
||||||
@ -95,9 +91,6 @@ pub struct SmtpConfig {
|
|||||||
/// Comment
|
/// Comment
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
/// Filter to apply
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub filter: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -105,7 +98,6 @@ pub struct SmtpConfig {
|
|||||||
pub enum DeleteableSmtpProperty {
|
pub enum DeleteableSmtpProperty {
|
||||||
Author,
|
Author,
|
||||||
Comment,
|
Comment,
|
||||||
Filter,
|
|
||||||
Mailto,
|
Mailto,
|
||||||
MailtoUser,
|
MailtoUser,
|
||||||
Password,
|
Password,
|
||||||
|
Loading…
Reference in New Issue
Block a user