all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal