[pve-devel] [PATCH proxmox 1/6] notify: copy sendmail/forward fn's from proxmox_sys

Lukas Wagner l.wagner at proxmox.com
Mon Jun 24 14:31:29 CEST 2024


proxmox_notify is the only user of those functions, so it makes
sense to move them here. A future commit will mark the
original functions from proxmox_sys as deprecated.

The functions were slightly modified, mostly to not
rely on anyhow for error reporting. Also they
are now private functions.

Signed-off-by: Lukas Wagner <l.wagner at proxmox.com>
---
 proxmox-notify/Cargo.toml                |   1 +
 proxmox-notify/src/endpoints/sendmail.rs | 189 ++++++++++++++++++++++-
 2 files changed, 188 insertions(+), 2 deletions(-)

diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml
index d3eae584..e55be0cc 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -9,6 +9,7 @@ exclude.workspace = true
 
 [dependencies]
 anyhow.workspace = true
+base64.workspace = true
 const_format.workspace = true
 handlebars = { workspace = true }
 lettre = { workspace = true, optional = true }
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index da0c0cc7..e75902fc 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -1,3 +1,6 @@
+use std::io::Write;
+use std::process::{Command, Stdio};
+
 use serde::{Deserialize, Serialize};
 
 use proxmox_schema::api_types::COMMENT_SCHEMA;
@@ -133,7 +136,7 @@ impl Endpoint for SendmailEndpoint {
                     .clone()
                     .unwrap_or_else(|| context().default_sendmail_author());
 
-                proxmox_sys::email::sendmail(
+                sendmail(
                     &recipients_str,
                     &subject,
                     Some(&text_part),
@@ -145,7 +148,7 @@ impl Endpoint for SendmailEndpoint {
             }
             #[cfg(feature = "mail-forwarder")]
             Content::ForwardedMail { raw, uid, .. } => {
-                proxmox_sys::email::forward(&recipients_str, &mailfrom, raw, *uid)
+                forward(&recipients_str, &mailfrom, raw, *uid)
                     .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into()))
             }
         }
@@ -160,3 +163,185 @@ impl Endpoint for SendmailEndpoint {
         self.config.disable.unwrap_or_default()
     }
 }
+
+/// Sends multi-part mail with text and/or html to a list of recipients
+///
+/// Includes the header `Auto-Submitted: auto-generated`, so that auto-replies
+/// (i.e. OOO replies) won't trigger.
+/// ``sendmail`` is used for sending the mail.
+fn sendmail(
+    mailto: &[&str],
+    subject: &str,
+    text: Option<&str>,
+    html: Option<&str>,
+    mailfrom: Option<&str>,
+    author: Option<&str>,
+) -> Result<(), Error> {
+    use std::fmt::Write as _;
+
+    if mailto.is_empty() {
+        return Err(Error::Generic(
+            "At least one recipient has to be specified!".into(),
+        ));
+    }
+    let mailfrom = mailfrom.unwrap_or("root");
+    let recipients = mailto.join(",");
+    let author = author.unwrap_or("Proxmox Backup Server");
+
+    let now = proxmox_time::epoch_i64();
+
+    let mut sendmail_process = match Command::new("/usr/sbin/sendmail")
+        .arg("-B")
+        .arg("8BITMIME")
+        .arg("-f")
+        .arg(mailfrom)
+        .arg("--")
+        .args(mailto)
+        .stdin(Stdio::piped())
+        .spawn()
+    {
+        Err(err) => {
+            return Err(Error::Generic(format!(
+                "could not spawn sendmail process: {err}"
+            )))
+        }
+        Ok(process) => process,
+    };
+    let mut is_multipart = false;
+    if let (Some(_), Some(_)) = (text, html) {
+        is_multipart = true;
+    }
+
+    let mut body = String::new();
+    let boundary = format!("----_=_NextPart_001_{}", now);
+    if is_multipart {
+        body.push_str("Content-Type: multipart/alternative;\n");
+        let _ = writeln!(body, "\tboundary=\"{}\"", boundary);
+        body.push_str("MIME-Version: 1.0\n");
+    } else if !subject.is_ascii() {
+        body.push_str("MIME-Version: 1.0\n");
+    }
+    if !subject.is_ascii() {
+        let _ = writeln!(body, "Subject: =?utf-8?B?{}?=", base64::encode(subject));
+    } else {
+        let _ = writeln!(body, "Subject: {}", subject);
+    }
+    let _ = writeln!(body, "From: {} <{}>", author, mailfrom);
+    let _ = writeln!(body, "To: {}", &recipients);
+    let rfc2822_date = proxmox_time::epoch_to_rfc2822(now)
+        .map_err(|err| Error::Generic(format!("failed to format time: {err}")))?;
+    let _ = writeln!(body, "Date: {}", rfc2822_date);
+    body.push_str("Auto-Submitted: auto-generated;\n");
+
+    if is_multipart {
+        body.push('\n');
+        body.push_str("This is a multi-part message in MIME format.\n");
+        let _ = write!(body, "\n--{}\n", boundary);
+    }
+    if let Some(text) = text {
+        body.push_str("Content-Type: text/plain;\n");
+        body.push_str("\tcharset=\"UTF-8\"\n");
+        body.push_str("Content-Transfer-Encoding: 8bit\n");
+        body.push('\n');
+        body.push_str(text);
+        if is_multipart {
+            let _ = write!(body, "\n--{}\n", boundary);
+        }
+    }
+    if let Some(html) = html {
+        body.push_str("Content-Type: text/html;\n");
+        body.push_str("\tcharset=\"UTF-8\"\n");
+        body.push_str("Content-Transfer-Encoding: 8bit\n");
+        body.push('\n');
+        body.push_str(html);
+        if is_multipart {
+            let _ = write!(body, "\n--{}--", boundary);
+        }
+    }
+
+    if let Err(err) = sendmail_process
+        .stdin
+        .take()
+        .unwrap()
+        .write_all(body.as_bytes())
+    {
+        return Err(Error::Generic(format!(
+            "couldn't write to sendmail stdin: {err}"
+        )));
+    };
+
+    // wait() closes stdin of the child
+    if let Err(err) = sendmail_process.wait() {
+        return Err(Error::Generic(format!(
+            "sendmail did not exit successfully: {err}"
+        )));
+    }
+
+    Ok(())
+}
+
+/// Forwards an email message to a given list of recipients.
+///
+/// ``sendmail`` is used for sending the mail, thus `message` must be
+/// compatible with that (the message is piped into stdin unmodified).
+#[cfg(feature = "mail-forwarder")]
+fn forward(mailto: &[&str], mailfrom: &str, message: &[u8], uid: Option<u32>) -> Result<(), Error> {
+    use std::os::unix::process::CommandExt;
+
+    if mailto.is_empty() {
+        return Err(Error::Generic(
+            "At least one recipient has to be specified!".into(),
+        ));
+    }
+
+    let mut builder = Command::new("/usr/sbin/sendmail");
+
+    builder
+        .args([
+            "-N", "never", // never send DSN (avoid mail loops)
+            "-f", mailfrom, "--",
+        ])
+        .args(mailto)
+        .stdin(Stdio::piped())
+        .stdout(Stdio::null())
+        .stderr(Stdio::null());
+
+    if let Some(uid) = uid {
+        builder.uid(uid);
+    }
+
+    let mut process = builder
+        .spawn()
+        .map_err(|err| Error::Generic(format!("could not spawn sendmail process: {err}")))?;
+
+    process
+        .stdin
+        .take()
+        .unwrap()
+        .write_all(message)
+        .map_err(|err| Error::Generic(format!("couldn't write to sendmail stdin: {err}")))?;
+
+    process
+        .wait()
+        .map_err(|err| Error::Generic(format!("sendmail did not exit successfully: {err}")))?;
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn email_without_recipients() {
+        let result = sendmail(
+            &[],
+            "Subject2",
+            None,
+            Some("<b>HTML</b>"),
+            None,
+            Some("test1"),
+        );
+        assert!(result.is_err());
+    }
+}
-- 
2.39.2





More information about the pve-devel mailing list