From: Lukas Wagner <l.wagner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox 04/11] notify: add mechanisms for email message forwarding
Date: Thu, 31 Aug 2023 13:06:14 +0200 [thread overview]
Message-ID: <20230831110621.340832-5-l.wagner@proxmox.com> (raw)
In-Reply-To: <20230831110621.340832-1-l.wagner@proxmox.com>
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@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
next prev parent reply other threads:[~2023-08-31 11:06 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-08-31 11:06 [pve-devel] [PATCH many 00/11] notifications: feed system mails into proxmox_notify Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH debcargo-conf 01/11] package mail-parser 0.8.2 Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH proxmox 02/11] sys: email: add `forward` Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH proxmox 03/11] notify: introduce Error::Generic Lukas Wagner
2023-08-31 11:06 ` Lukas Wagner [this message]
2023-08-31 11:06 ` [pve-devel] [PATCH proxmox 05/11] notify: add PVE/PBS context Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH proxmox-perl-rs 06/11] notify: construct Notification via constructor Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH proxmox-perl-rs 07/11] pve-rs: notify: remove notify_context for PVE Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH pve-cluster 08/11] datacenter config: add new parameters for system mail forwarding Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH pve-manager 09/11] ui: notify: add system-mail settings, configuring " Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH proxmox-mail-forward 10/11] feed forwarded mails into proxmox_notify Lukas Wagner
2023-08-31 11:06 ` [pve-devel] [PATCH pve-docs 11/11] notification: add docs for system mail forwarding Lukas Wagner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20230831110621.340832-5-l.wagner@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal