[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,
-            &notification.title,
-            properties,
-        )?;
-        let message =
-            renderer::render_template(TemplateRenderer::Plaintext, &notification.body, properties)?;
+        let (title, message) = match &notification.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,
-            &notification.title,
-            properties,
-        )?;
-        let html_part =
-            renderer::render_template(TemplateRenderer::Html, &notification.body, properties)?;
-        let text_part =
-            renderer::render_template(TemplateRenderer::Plaintext, &notification.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 &notification.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