[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