public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH proxmox v2 1/1] sendmail: add additional header map
@ 2026-06-16  9:54 Nicolas Frey
  0 siblings, 0 replies; only message in thread
From: Nicolas Frey @ 2026-06-16  9:54 UTC (permalink / raw)
  To: pve-devel

with corresponding methods to add them.

this adds a new enum `MailHeader` in order to have a controlled set of
headers (currently 'Reply-To', 'In-Reply-To', 'Cc', 'Bcc', as they
seemed sensible and might get used more often)

if later down the line it is necessary to add arbitrary headers, the
enum can be expanded to a `Custom(String)` variant, though this would
explicitly need to point out that the caller is responsible for the
overlap/correctness.

Signed-off-by: Nicolas Frey <n.frey@proxmox.com>
---

Notes:
    Changes since v1 (thanks Shannon!):
    * use a limited set of headers restricted by an enum
    * check is_ascii on the header bodies and correctly encode them
    * use BTreeMap instead of HashMap, as it is a small set of Headers,
        improving the memory footprint and performance

 proxmox-sendmail/src/lib.rs | 75 +++++++++++++++++++++++++++++++++++++
 1 file changed, 75 insertions(+)

diff --git a/proxmox-sendmail/src/lib.rs b/proxmox-sendmail/src/lib.rs
index db751305..5a04199b 100644
--- a/proxmox-sendmail/src/lib.rs
+++ b/proxmox-sendmail/src/lib.rs
@@ -3,6 +3,8 @@
 //! and alternative html parts to one or multiple receivers via ``sendmail``.
 //!

+use std::collections::BTreeMap;
+use std::fmt::Display;
 use std::io::Write;
 use std::process::{Command, Stdio};

@@ -236,6 +238,30 @@ impl Attachment<'_> {
     }
 }

+/// List of headers which can be set as additional headers in an email to be sent by proxmox-sendmail.
+/// This enum may contain any standard headers found in the list of
+/// [IANA Message Headers](https://www.iana.org/assignments/message-headers/message-headers.xhtml)
+/// with Protocal "mail"
+#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
+pub enum MailHeader {
+    ReplyTo,
+    InReplyTo,
+    Cc,
+    Bcc,
+}
+
+impl Display for MailHeader {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let name = match self {
+            MailHeader::ReplyTo => "Reply-To",
+            MailHeader::InReplyTo => "In-Reply-To",
+            MailHeader::Cc => "Cc",
+            MailHeader::Bcc => "Bcc",
+        };
+        write!(f, "{name}")
+    }
+}
+
 /// This struct is used to define mails that are to be sent via the `sendmail` command.
 pub struct Mail<'a> {
     mail_author: String,
@@ -247,6 +273,7 @@ pub struct Mail<'a> {
     attachments: Vec<Attachment<'a>>,
     mask_participants: bool,
     noreply: Option<Recipient>,
+    additional_headers: BTreeMap<MailHeader, &'a str>,
 }

 impl<'a> Mail<'a> {
@@ -265,6 +292,7 @@ impl<'a> Mail<'a> {
             attachments: Vec::new(),
             mask_participants: true,
             noreply: None,
+            additional_headers: BTreeMap::new(),
         }
     }

@@ -393,6 +421,18 @@ impl<'a> Mail<'a> {
         self
     }

+    /// Set an additional header that is listed in [`MailHeader`] with its corresponding body.
+    pub fn set_header(&mut self, header: MailHeader, body: &'a str) {
+        self.additional_headers.insert(header, body);
+    }
+
+    /// Builder-style method to set an additional header that is listed in [`MailHeader`] with its
+    /// corresponding body.
+    pub fn with_header(mut self, header: MailHeader, body: &'a str) -> Self {
+        self.set_header(header, body);
+        self
+    }
+
     /// Sends the email. This will fail if no recipients have been added.
     ///
     /// Note: An `Auto-Submitted: auto-generated` header is added to avoid triggering OOO and
@@ -534,6 +574,7 @@ impl<'a> Mail<'a> {
             || !self.mail_author.is_ascii()
             || !self.body_txt.is_ascii()
             || encoded_to
+            || self.additional_headers.iter().any(|(_, b)| !b.is_ascii())
         {
             header.push_str("MIME-Version: 1.0\n");
         }
@@ -584,6 +625,15 @@ impl<'a> Mail<'a> {
         let rfc2822_date = proxmox_time::epoch_to_rfc2822(now)
             .with_context(|| "could not convert epoch to rfc2822 date")?;
         writeln!(header, "Date: {rfc2822_date}")?;
+
+        for (h, b) in &self.additional_headers {
+            if !b.is_ascii() {
+                writeln!(header, "{h}: =?utf-8?B?{}?=", proxmox_base64::encode(b))?;
+            } else {
+                writeln!(header, "{h}: {b}")?;
+            }
+        }
+
         header.push_str("Auto-Submitted: auto-generated;\n");

         Ok(header)
@@ -681,6 +731,31 @@ mod test {
         assert!(result.is_err());
     }

+    #[test]
+    fn additional_headers() {
+        let mail = Mail::new("Sender", "mail@example.com", "hi", "body")
+            .with_recipient_and_name("Jane Doe", "j.doe@example.com")
+            .with_header(MailHeader::ReplyTo, "mail@example.com")
+            .with_header(MailHeader::Cc, "cc1@example.com,cc2@example.com");
+        let body = mail.format_mail(0).expect("could not format mail");
+
+        assert_lines_equal_ignore_date(
+            &body,
+            r#"Subject: hi
+From: Sender <mail@example.com>
+To: Jane Doe <j.doe@example.com>
+Date: Thu, 01 Jan 1970 01:00:00 +0100
+Reply-To: mail@example.com
+Cc: cc1@example.com,cc2@example.com
+Auto-Submitted: auto-generated;
+Content-Type: text/plain;
+	charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+
+body"#,
+        )
+    }
+
     #[test]
     fn simple_ascii_text_mail() {
         let mail = Mail::new(
--
2.47.3



^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-06-16  9:54 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-16  9:54 [PATCH proxmox v2 1/1] sendmail: add additional header map Nicolas Frey

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal