* [pbs-devel] [PATCH proxmox 0/1] sendmail function @ 2020-08-20 9:23 Hannes Laimer 2020-08-20 9:23 ` [pbs-devel] [PATCH proxmox 1/1] email: add small function to send multi-part emails using sendmail Hannes Laimer 0 siblings, 1 reply; 4+ messages in thread From: Hannes Laimer @ 2020-08-20 9:23 UTC (permalink / raw) To: pbs-devel Small function to send a multi-part mail to a list of recipients. Hannes Laimer (1): email: add small function to send multi-part emails using sendmail proxmox/src/tools/email.rs | 130 +++++++++++++++++++++++++++++++++++++ proxmox/src/tools/mod.rs | 1 + 2 files changed, 131 insertions(+) create mode 100644 proxmox/src/tools/email.rs -- 2.20.1 ^ permalink raw reply [flat|nested] 4+ messages in thread
* [pbs-devel] [PATCH proxmox 1/1] email: add small function to send multi-part emails using sendmail 2020-08-20 9:23 [pbs-devel] [PATCH proxmox 0/1] sendmail function Hannes Laimer @ 2020-08-20 9:23 ` Hannes Laimer 2020-08-20 12:34 ` Stoiko Ivanov 0 siblings, 1 reply; 4+ messages in thread From: Hannes Laimer @ 2020-08-20 9:23 UTC (permalink / raw) To: pbs-devel Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> --- proxmox/src/tools/email.rs | 130 +++++++++++++++++++++++++++++++++++++ proxmox/src/tools/mod.rs | 1 + 2 files changed, 131 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..b73a6d6 --- /dev/null +++ b/proxmox/src/tools/email.rs @@ -0,0 +1,130 @@ +//! Email related utilities. + +use std::process::{Command, Stdio}; +use anyhow::{bail, Error}; +use std::io::Write; +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 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 body = String::new(); + let boundary = format!("----_=_NextPart_001_{}", time()?); + + body.push_str("Content-Type: multipart/alternative;\n"); + body.push_str(&format!("\tboundary=\"{}\"\n", boundary)); + body.push_str("MIME-Version: 1.0\n"); + body.push_str(&format!("FROM: {} <{}>\n", author, mailfrom)); + body.push_str(&format!("TO: {}\n", &recipients)); + body.push_str(&format!("SUBJECT: {}\n", subject)); + body.push('\n'); + body.push_str("This is a multi-part message in MIME format.\n\n"); + body.push_str(&format!("--{}\n", boundary)); + if let Some(text) = text { + body.push_str("Content-Type: text/plain;\n"); + body.push_str("\tcharset=\"UTF8\"\n"); + body.push_str("Content-Transfer-Encoding: 8bit\n"); + body.push('\n'); + body.push_str(text); + body.push_str(&format!("\n--{}\n", boundary)); + } + if let Some(html) = html { + body.push_str("Content-Type: text/html;\n"); + body.push_str("\tcharset=\"UTF8\"\n"); + body.push_str("Content-Transfer-Encoding: 8bit\n"); + body.push('\n'); + body.push_str(html); + body.push_str(&format!("\n--{}\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("<b>HTML</b>"), + Some("bim@bam.bum"), + Some("test1")); + assert!(result.is_err()); + } + + #[test] + fn test2() { + let result = sendmail( + vec![], + "Subject2", + None, + Some("<b>HTML</b>"), + None, + Some("test1")); + assert!(result.is_err()); + } + + #[test] + fn test3() { + let result = sendmail( + vec!["a@b.c"], + "Subject3", + None, + Some("<b>HTML</b>"), + 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 ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [pbs-devel] [PATCH proxmox 1/1] email: add small function to send multi-part emails using sendmail 2020-08-20 9:23 ` [pbs-devel] [PATCH proxmox 1/1] email: add small function to send multi-part emails using sendmail Hannes Laimer @ 2020-08-20 12:34 ` Stoiko Ivanov 2020-08-20 14:08 ` Thomas Lamprecht 0 siblings, 1 reply; 4+ messages in thread From: Stoiko Ivanov @ 2020-08-20 12:34 UTC (permalink / raw) To: Hannes Laimer; +Cc: Proxmox Backup Server development discussion Thanks for the patch! some comments/suggestions on e-mail inline: On Thu, 20 Aug 2020 11:23:50 +0200 Hannes Laimer <h.laimer@proxmox.com> wrote: > Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> > --- > proxmox/src/tools/email.rs | 130 +++++++++++++++++++++++++++++++++++++ > proxmox/src/tools/mod.rs | 1 + > 2 files changed, 131 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..b73a6d6 > --- /dev/null > +++ b/proxmox/src/tools/email.rs > @@ -0,0 +1,130 @@ > +//! Email related utilities. > + > +use std::process::{Command, Stdio}; > +use anyhow::{bail, Error}; > +use std::io::Write; > +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 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 body = String::new(); > + let boundary = format!("----_=_NextPart_001_{}", time()?); > + > + body.push_str("Content-Type: multipart/alternative;\n"); > + body.push_str(&format!("\tboundary=\"{}\"\n", boundary)); > + body.push_str("MIME-Version: 1.0\n"); > + body.push_str(&format!("FROM: {} <{}>\n", author, mailfrom)); > + body.push_str(&format!("TO: {}\n", &recipients)); > + body.push_str(&format!("SUBJECT: {}\n", subject)); iirc the header needs to be encoded if it contains non-ascii characters (the content-type charset only relates to the body) - I assume that postfix/exim would do the correct thing these days (and send the mail with smtputf-8 extension (though I'm not sure what would happen if the receiving server does not support that) - would suggest to test sending such a mail with a non-ascii character in the subject. writing the header names (TO FROM SUBJECT) in all-caps seems odd to my eyes (but I guess this is pretty cosmetic) I would suggest adding a Date header (though postfix does that for locally submitted e-mail) - else the mails are more likely to be considered spam. Finally - if you only have a text/plain part I would only send that part (without the wrapping in multipart/alternative) (similarly for a single text/html part without text/plain part). > + body.push('\n'); > + body.push_str("This is a multi-part message in MIME format.\n\n"); > + body.push_str(&format!("--{}\n", boundary)); > + if let Some(text) = text { > + body.push_str("Content-Type: text/plain;\n"); > + body.push_str("\tcharset=\"UTF8\"\n"); While most MTA, scanners, MUAs won't care much - the charset should probably be written as 'UTF-8' (see https://tools.ietf.org/html/rfc3629 (section 8)) > + body.push_str("Content-Transfer-Encoding: 8bit\n"); > + body.push('\n'); > + body.push_str(text); > + body.push_str(&format!("\n--{}\n", boundary)); > + } > + if let Some(html) = html { > + body.push_str("Content-Type: text/html;\n"); > + body.push_str("\tcharset=\"UTF8\"\n"); > + body.push_str("Content-Transfer-Encoding: 8bit\n"); > + body.push('\n'); > + body.push_str(html); > + body.push_str(&format!("\n--{}\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("<b>HTML</b>"), > + Some("bim@bam.bum"), > + Some("test1")); > + assert!(result.is_err()); > + } > + > + #[test] > + fn test2() { > + let result = sendmail( > + vec![], > + "Subject2", > + None, > + Some("<b>HTML</b>"), > + None, > + Some("test1")); > + assert!(result.is_err()); > + } > + > + #[test] > + fn test3() { > + let result = sendmail( > + vec!["a@b.c"], > + "Subject3", > + None, > + Some("<b>HTML</b>"), > + 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; ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [pbs-devel] [PATCH proxmox 1/1] email: add small function to send multi-part emails using sendmail 2020-08-20 12:34 ` Stoiko Ivanov @ 2020-08-20 14:08 ` Thomas Lamprecht 0 siblings, 0 replies; 4+ messages in thread From: Thomas Lamprecht @ 2020-08-20 14:08 UTC (permalink / raw) To: Proxmox Backup Server development discussion, Stoiko Ivanov, Hannes Laimer On 20.08.20 14:34, Stoiko Ivanov wrote: > Thanks for the patch! > > some comments/suggestions on e-mail inline: > > On Thu, 20 Aug 2020 11:23:50 +0200 > Hannes Laimer <h.laimer@proxmox.com> wrote: > >> Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> >> --- >> proxmox/src/tools/email.rs | 130 +++++++++++++++++++++++++++++++++++++ >> proxmox/src/tools/mod.rs | 1 + >> 2 files changed, 131 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..b73a6d6 >> --- /dev/null >> +++ b/proxmox/src/tools/email.rs >> @@ -0,0 +1,130 @@ >> +//! Email related utilities. >> + >> +use std::process::{Command, Stdio}; >> +use anyhow::{bail, Error}; >> +use std::io::Write; >> +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 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 body = String::new(); >> + let boundary = format!("----_=_NextPart_001_{}", time()?); >> + >> + body.push_str("Content-Type: multipart/alternative;\n"); >> + body.push_str(&format!("\tboundary=\"{}\"\n", boundary)); >> + body.push_str("MIME-Version: 1.0\n"); >> + body.push_str(&format!("FROM: {} <{}>\n", author, mailfrom)); >> + body.push_str(&format!("TO: {}\n", &recipients)); >> + body.push_str(&format!("SUBJECT: {}\n", subject)); > > iirc the header needs to be encoded if it contains non-ascii characters > (the content-type charset only relates to the body) - I assume that > postfix/exim would do the correct thing these days (and send the mail with > smtputf-8 extension (though I'm not sure what would happen if the > receiving server does not support that) > - would suggest to test sending such a mail with a non-ascii character > in the subject. > > writing the header names (TO FROM SUBJECT) in all-caps seems odd to my > eyes (but I guess this is pretty cosmetic) > > I would suggest adding a Date header (though postfix does that for locally > submitted e-mail) - else the mails are more likely to be considered spam. > > Finally - if you only have a text/plain part I would only send that part > (without the wrapping in multipart/alternative) (similarly for a > single text/html part without text/plain part). > > >> + body.push('\n'); >> + body.push_str("This is a multi-part message in MIME format.\n\n"); >> + body.push_str(&format!("--{}\n", boundary)); >> + if let Some(text) = text { >> + body.push_str("Content-Type: text/plain;\n"); >> + body.push_str("\tcharset=\"UTF8\"\n"); > While most MTA, scanners, MUAs won't care much - the charset should > probably be written as 'UTF-8' (see https://tools.ietf.org/html/rfc3629 > (section 8)) > You're right with your feedback, but I guess that most stems from the original Perl implementation[0] Hannes used as model. We (meaning one of you two ;P) should overhaul that one too then. [0]: https://git.proxmox.com/?p=pve-common.git;a=blob;f=src/PVE/Tools.pm;h=f9270d9b10340eec789346e81f4a926772ccabfd;hb=HEAD#l1439 ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2020-08-20 14:08 UTC | newest] Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2020-08-20 9:23 [pbs-devel] [PATCH proxmox 0/1] sendmail function Hannes Laimer 2020-08-20 9:23 ` [pbs-devel] [PATCH proxmox 1/1] email: add small function to send multi-part emails using sendmail Hannes Laimer 2020-08-20 12:34 ` Stoiko Ivanov 2020-08-20 14:08 ` Thomas Lamprecht
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox