From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 10BA890ADB for ; Mon, 2 Oct 2023 10:07:05 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id F31A014046 for ; Mon, 2 Oct 2023 10:06:36 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Mon, 2 Oct 2023 10:06:35 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 7579A4479B for ; Mon, 2 Oct 2023 10:06:34 +0200 (CEST) From: Lukas Wagner To: pve-devel@lists.proxmox.com Date: Mon, 2 Oct 2023 10:06:17 +0200 Message-Id: <20231002080624.198759-5-l.wagner@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20231002080624.198759-1-l.wagner@proxmox.com> References: <20231002080624.198759-1-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.028 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH v2 proxmox 04/11] notify: add mechanisms for email message forwarding X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 02 Oct 2023 08:07:05 -0000 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 --- 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, 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, + /// 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, + }, +} + #[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, + #[allow(dead_code)] // Unused in some feature flag permutations + properties: Option, +} + +impl Notification { + pub fn new_templated>( + severity: Severity, + title: S, + body: S, + properties: Option, + ) -> 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) -> Result { + 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