[pve-devel] [PATCH proxmox 04/11] notify: add mechanisms for email message forwarding
Lukas Wagner
l.wagner at proxmox.com
Thu Aug 31 13:06:14 CEST 2023
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 at proxmox.com>
---
Cargo.toml | 1 +
proxmox-notify/Cargo.toml | 2 +
proxmox-notify/src/endpoints/gotify.rs | 21 +++--
proxmox-notify/src/endpoints/sendmail.rs | 62 ++++++++-------
proxmox-notify/src/filter.rs | 8 +-
proxmox-notify/src/lib.rs | 98 ++++++++++++++++++++----
6 files changed, 138 insertions(+), 54 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index e334ac1..9adfe59 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -61,6 +61,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 1541b8b..441b6e1 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -11,6 +11,7 @@ exclude.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 }
@@ -26,5 +27,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 83df41f..261573b 100644
--- a/proxmox-notify/src/endpoints/gotify.rs
+++ b/proxmox-notify/src/endpoints/gotify.rs
@@ -11,7 +11,7 @@ use proxmox_schema::{api, Updater};
use crate::context::context;
use crate::renderer::TemplateRenderer;
use crate::schema::ENTITY_NAME_SCHEMA;
-use crate::{renderer, Endpoint, Error, Notification, Severity};
+use crate::{renderer, Content, Endpoint, Error, Notification, Severity};
fn severity_to_priority(level: Severity) -> u32 {
match level {
@@ -87,13 +87,18 @@ impl Endpoint for GotifyEndpoint {
fn send(&self, notification: &Notification) -> Result<(), Error> {
let properties = notification.properties.as_ref();
- let title = renderer::render_template(
- TemplateRenderer::Plaintext,
- ¬ification.title,
- properties,
- )?;
- let message =
- renderer::render_template(TemplateRenderer::Plaintext, ¬ification.body, properties)?;
+ let (title, message) = match ¬ification.content {
+ Content::Template { title, body } => {
+ let rendered_title =
+ renderer::render_template(TemplateRenderer::Plaintext, title, properties)?;
+ let rendered_message =
+ renderer::render_template(TemplateRenderer::Plaintext, body, properties)?;
+
+ (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
// in code tags. Otherwise tables etc. are not formatted properly
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index 26e2a17..9cc3f31 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -8,7 +8,7 @@ use proxmox_schema::{api, Updater};
use crate::context::context;
use crate::renderer::TemplateRenderer;
use crate::schema::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA};
-use crate::{renderer, Endpoint, Error, Notification};
+use crate::{renderer, Content, Endpoint, Error, Notification};
pub(crate) const SENDMAIL_TYPENAME: &str = "sendmail";
@@ -103,40 +103,44 @@ impl Endpoint for SendmailEndpoint {
}
let properties = notification.properties.as_ref();
-
- let subject = renderer::render_template(
- TemplateRenderer::Plaintext,
- ¬ification.title,
- properties,
- )?;
- let html_part =
- renderer::render_template(TemplateRenderer::Html, ¬ification.body, properties)?;
- let text_part =
- renderer::render_template(TemplateRenderer::Plaintext, ¬ification.body, properties)?;
-
- let author = self
- .config
- .author
- .clone()
- .unwrap_or_else(|| context().default_sendmail_author());
-
+ let recipients_str: Vec<&str> = recipients.iter().map(String::as_str).collect();
let mailfrom = self
.config
.from_address
.clone()
.unwrap_or_else(|| context().default_sendmail_from());
- let recipients_str: Vec<&str> = recipients.iter().map(String::as_str).collect();
-
- proxmox_sys::email::sendmail(
- &recipients_str,
- &subject,
- Some(&text_part),
- Some(&html_part),
- Some(&mailfrom),
- Some(&author),
- )
- .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into()))
+ match ¬ification.content {
+ Content::Template { title, body } => {
+ let subject =
+ renderer::render_template(TemplateRenderer::Plaintext, title, properties)?;
+ let html_part =
+ renderer::render_template(TemplateRenderer::Html, body, properties)?;
+ let text_part =
+ renderer::render_template(TemplateRenderer::Plaintext, body, properties)?;
+
+ let author = self
+ .config
+ .author
+ .clone()
+ .unwrap_or_else(|| context().default_sendmail_author());
+
+ proxmox_sys::email::sendmail(
+ &recipients_str,
+ &subject,
+ Some(&text_part),
+ Some(&html_part),
+ Some(&mailfrom),
+ Some(&author),
+ )
+ .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()))
+ }
+ }
}
fn name(&self) -> &str {
diff --git a/proxmox-notify/src/filter.rs b/proxmox-notify/src/filter.rs
index 748ec4e..d052512 100644
--- a/proxmox-notify/src/filter.rs
+++ b/proxmox-notify/src/filter.rs
@@ -160,7 +160,7 @@ impl<'a> FilterMatcher<'a> {
#[cfg(test)]
mod tests {
use super::*;
- use crate::config;
+ use crate::{config, Content};
fn parse_filters(config: &str) -> Result<Vec<FilterConfig>, Error> {
let (config, _) = config::config(config)?;
@@ -169,8 +169,10 @@ mod tests {
fn empty_notification_with_severity(severity: Severity) -> Notification {
Notification {
- title: String::new(),
- body: String::new(),
+ content: Content::Template {
+ title: String::new(),
+ body: String::new(),
+ },
severity,
properties: Default::default(),
}
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index f7d480c..eebc57a 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -116,17 +116,79 @@ pub trait Endpoint {
fn filter(&self) -> Option<&str>;
}
+#[derive(Debug, Clone)]
+pub enum Content {
+ /// Title and body will be rendered as a template
+ Template { title: String, body: String },
+ /// A special content type for forwarded mails. Contains the raw content of the original mail
+ /// as well as a (non-template) fallback title and body for endpoints that are not based
+ /// on email.
+ #[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)]
/// Notification which can be sent
pub struct Notification {
/// Notification severity
- pub severity: Severity,
- /// The title of the notification
- pub title: String,
- /// Notification text
- pub body: String,
+ severity: Severity,
+ /// Notification content
+ #[allow(dead_code)] // Unused in some feature flag permutations
+ content: Content,
/// Additional metadata for the notification
- pub properties: Option<Value>,
+ #[allow(dead_code)] // Unused in some feature flag permutations
+ properties: Option<Value>,
+}
+
+impl Notification {
+ pub fn new_templated<S: AsRef<str>>(
+ severity: Severity,
+ title: S,
+ body: S,
+ properties: Option<Value>,
+ ) -> Self {
+ Self {
+ severity,
+ content: Content::Template {
+ title: title.as_ref().to_string(),
+ body: body.as_ref().to_string(),
+ },
+ properties,
+ }
+ }
+
+ #[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.
+ severity: Severity::Error,
+ content: Content::ForwardedMail {
+ raw: raw_mail.into(),
+ title,
+ body,
+ uid,
+ },
+ properties: None,
+ })
+ }
}
/// Notification configuration
@@ -384,8 +446,10 @@ impl Bus {
pub fn test_target(&self, target: &str) -> Result<(), Error> {
let notification = Notification {
severity: Severity::Info,
- title: "Test notification".into(),
- body: "This is a test of the notification target '{{ target }}'".into(),
+ content: Content::Template {
+ title: "Test notification".into(),
+ body: "This is a test of the notification target '{{ target }}'".into(),
+ },
properties: Some(json!({ "target": target })),
};
@@ -474,8 +538,10 @@ mod tests {
bus.send(
"endpoint",
&Notification {
- title: "Title".into(),
- body: "Body".into(),
+ content: Content::Template {
+ title: "Title".into(),
+ body: "Body".into(),
+ },
severity: Severity::Info,
properties: Default::default(),
},
@@ -514,8 +580,10 @@ mod tests {
bus.send(
channel,
&Notification {
- title: "Title".into(),
- body: "Body".into(),
+ content: Content::Template {
+ title: "Title".into(),
+ body: "Body".into(),
+ },
severity: Severity::Info,
properties: Default::default(),
},
@@ -582,8 +650,10 @@ mod tests {
bus.send(
"channel1",
&Notification {
- title: "Title".into(),
- body: "Body".into(),
+ content: Content::Template {
+ title: "Title".into(),
+ body: "Body".into(),
+ },
severity,
properties: Default::default(),
},
--
2.39.2
More information about the pve-devel
mailing list