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 C5DD56778A for ; Thu, 27 Aug 2020 12:00:43 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id BD88716609 for ; Thu, 27 Aug 2020 12:00:43 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (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 id 9F655165FF for ; Thu, 27 Aug 2020 12:00:42 +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 6DDDF44666 for ; Thu, 27 Aug 2020 12:00:42 +0200 (CEST) From: Hannes Laimer To: pbs-devel@lists.proxmox.com Date: Thu, 27 Aug 2020 12:00:36 +0200 Message-Id: <20200827100036.75134-2-h.laimer@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20200827100036.75134-1-h.laimer@proxmox.com> References: <20200827100036.75134-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [mod.rs, email.rs] Subject: [pbs-devel] [PATCH proxmox v5 1/1] email: add small function to send multi-part emails using sendmail X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Thu, 27 Aug 2020 10:00:43 -0000 Signed-off-by: Hannes Laimer --- proxmox/src/tools/email.rs | 152 +++++++++++++++++++++++++++++++++++++ proxmox/src/tools/mod.rs | 1 + 2 files changed, 153 insertions(+) create mode 100644 proxmox/src/tools/email.rs diff --git a/proxmox/src/tools/email.rs b/proxmox/src/tools/email.rs new file mode 100644 index 0000000..04a5dc0 --- /dev/null +++ b/proxmox/src/tools/email.rs @@ -0,0 +1,152 @@ +//! Email related utilities. + +use std::process::{Command, Stdio}; +use anyhow::{bail, Error}; +use std::io::Write; +use chrono::{DateTime, Local}; +use crate::tools::time::time; + + +/// Sends multi-part mail with text and/or html to a list of recipients +/// +/// ``sendmail`` is used for sending the mail. +pub fn sendmail(mailto: Vec<&str>, + subject: &str, + text: Option<&str>, + html: Option<&str>, + mailfrom: Option<&str>, + author: Option<&str>) -> Result<(), Error> { + let mail_regex = regex::Regex::new(r"^[a-zA-Z\.0-9-]+@[a-zA-Z\.0-9-]+$").unwrap(); + + if mailto.is_empty() { + bail!("At least one recipient has to be specified!") + } + + for recipient in &mailto { + if !mail_regex.is_match(recipient) { + bail!("'{}' is not a valid email address", recipient) + } + } + + let mailfrom = mailfrom.unwrap_or("root"); + if !mailfrom.eq("root") && !mail_regex.is_match(mailfrom) { + bail!("'{}' is not a valid email address", mailfrom) + } + + let recipients = mailto.join(","); + let author = author.unwrap_or("Proxmox Backup Server"); + + let now: DateTime = Local::now(); + + let mut sendmail_process = match Command::new("/usr/sbin/sendmail") + .arg("-B") + .arg("8BITMIME") + .arg("-f") + .arg(mailfrom) + .arg("--") + .arg(&recipients) + .stdin(Stdio::piped()) + .spawn() { + Err(err) => bail!("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_{}", time()?); + if is_multipart { + body.push_str("Content-Type: multipart/alternative;\n"); + body.push_str(&format!("\tboundary=\"{}\"\n", 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() { + body.push_str(&format!("Subject: =?utf-8?B?{}?=\n", base64::encode(subject))); + } else { + body.push_str(&format!("Subject: {}\n", subject)); + } + body.push_str(&format!("From: {} <{}>\n", author, mailfrom)); + body.push_str(&format!("To: {}\n", &recipients)); + body.push_str(&format!("Date: {}\n", now.to_rfc2822())); + if is_multipart { + body.push('\n'); + body.push_str("This is a multi-part message in MIME format.\n"); + body.push_str(&format!("\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 { + body.push_str(&format!("\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 { + body.push_str(&format!("\n--{}--", boundary)); + } + } + + if let Err(err) = sendmail_process.stdin.take().unwrap().write_all(body.as_bytes()) { + bail!("couldn't write to sendmail stdin: {}", err) + }; + + // wait() closes stdin of the child + if let Err(err) = sendmail_process.wait() { + bail!("sendmail did not exit successfully: {}", err) + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use crate::tools::email::sendmail; + + #[test] + fn test1() { + let result = sendmail( + vec!["somenotvalidemail!", "somealmostvalid email"], + "Subject1", + Some("TEXT"), + Some("HTML"), + Some("bim@bam.bum"), + Some("test1")); + assert!(result.is_err()); + } + + #[test] + fn test2() { + let result = sendmail( + vec![], + "Subject2", + None, + Some("HTML"), + None, + Some("test1")); + assert!(result.is_err()); + } + + #[test] + fn test3() { + let result = sendmail( + vec!["a@b.c"], + "Subject3", + None, + Some("HTML"), + Some("notv@lid.com!"), + Some("test1")); + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/proxmox/src/tools/mod.rs b/proxmox/src/tools/mod.rs index 721e5d1..df6c429 100644 --- a/proxmox/src/tools/mod.rs +++ b/proxmox/src/tools/mod.rs @@ -10,6 +10,7 @@ pub mod borrow; pub mod byte_buffer; pub mod common_regex; pub mod constnamemap; +pub mod email; pub mod fd; pub mod fs; pub mod io; -- 2.20.1