From aac5c9929f81494ef0f4bbe0a83173b44058c847 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Wed, 23 Dec 2020 19:20:00 +0100 Subject: [PATCH] message: improve docs (#521) --- examples/README.md | 10 +- src/message/body.rs | 52 +++--- src/message/header/content.rs | 27 ++-- src/message/mimebody.rs | 2 +- src/message/mod.rs | 293 ++++++++++++++++++++-------------- 5 files changed, 228 insertions(+), 156 deletions(-) diff --git a/examples/README.md b/examples/README.md index b0a98ae..0fb0eb6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,15 +2,19 @@ This folder contains examples showing how to use lettre in your own projects. -## Examples +## Message builder examples + +- [basic_html.rs] - Create an HTML email. +- [maud_html.rs] - Create an HTML email using a [maud](https://github.com/lambda-fairy/maud) template. + +## SMTP Examples + - [smtp.rs] - Send an email using a local SMTP daemon on port 25 as a relay. - [smtp_tls.rs] - Send an email over SMTP encrypted with TLS and authenticating with username and password. - [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password. - [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password. - The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/). These files are prefixed with `tokio02_` or `tokio03_`. -- [basic_html.rs] - Create an HTML email. -- [maud_html.rs] - Create an HTML email using a [maud](https://github.com/lambda-fairy/maud) template. [basic_html.rs]: ./basic_html.rs [maud_html.rs]: ./maud_html.rs diff --git a/src/message/body.rs b/src/message/body.rs index 1d620e7..dda71b0 100644 --- a/src/message/body.rs +++ b/src/message/body.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use crate::message::header::ContentTransferEncoding; -/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body. +/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body that has already been encoded. #[derive(Debug, Clone)] pub struct Body { buf: Vec, @@ -13,7 +13,7 @@ pub struct Body { /// Either a `Vec` or a `String`. /// /// If the content is valid utf-8 a `String` should be passed, as it -/// makes for a more efficient `Content-Transfer-Encoding` to be choosen. +/// makes for a more efficient `Content-Transfer-Encoding` to be chosen. #[derive(Debug, Clone)] pub enum MaybeString { Binary(Vec), @@ -23,8 +23,14 @@ pub enum MaybeString { impl Body { /// Encode the supplied `buf`, making it ready to be sent as a body. /// + /// Takes a `Vec` or a `String`. + /// /// Automatically chooses the most efficient encoding between /// `7bit`, `quoted-printable` and `base64`. + /// + /// If `buf` is valid utf-8 a `String` should be supplied, as `String`s + /// can be encoded as `7bit` or `quoted-printable`, while `Vec` always + /// get encoded as `base64`. pub fn new>(buf: B) -> Self { let buf: MaybeString = buf.into(); @@ -34,10 +40,11 @@ impl Body { /// Encode the supplied `buf`, using the provided `encoding`. /// - /// Generally [`Body::new`] should be used. + /// [`Body::new`] is generally the better option. /// - /// Returns an [`Err`] with the supplied `buf` untouched in case - /// the choosen encoding wouldn't have worked. + /// Returns an [`Err`] giving back the supplied `buf`, in case the chosen + /// encoding would have resulted into `buf` being encoded + /// into an invalid body. pub fn new_with_encoding>( buf: B, encoding: ContentTransferEncoding, @@ -137,10 +144,10 @@ impl MaybeString { /// /// If the `MaybeString` was created from a `String` composed only of US-ASCII /// characters, with no lines longer than 1000 characters, then 7bit - /// encoding will be used, else quoted-printable will be choosen. + /// encoding will be used, else quoted-printable will be chosen. /// /// If the `MaybeString` was instead created from a `Vec`, base64 encoding is always - /// choosen. + /// chosen. /// /// `8bit` and `binary` encodings are never returned, as they may not be /// supported by all SMTP servers. @@ -153,31 +160,32 @@ impl MaybeString { } } - /// Returns whether the provided `encoding` would encode this `MaybeString` into - /// a valid email body. + /// Returns `true` if using `encoding` to encode this `MaybeString` + /// would result into an invalid encoded body. fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool { match encoding { ContentTransferEncoding::SevenBit => is_7bit_encoded(&self), ContentTransferEncoding::EightBit => is_8bit_encoded(&self), - ContentTransferEncoding::Binary => true, - ContentTransferEncoding::QuotedPrintable => { - // TODO: check - - true - } - ContentTransferEncoding::Base64 => { - // TODO: check - - true - } + ContentTransferEncoding::Binary + | ContentTransferEncoding::QuotedPrintable + | ContentTransferEncoding::Base64 => true, } } } -/// A trait for [`MessageBuilder::body`][super::MessageBuilder::body] and +/// A trait for something that takes an encoded [`Body`]. +/// +/// Used by [`MessageBuilder::body`][super::MessageBuilder::body] and /// [`SinglePartBuilder::body`][super::SinglePartBuilder::body], /// which can either take something that can be encoded into [`Body`] -/// or a pre-encoded [`Body`] +/// or a pre-encoded [`Body`]. +/// +/// If `encoding` is `None` the best encoding between `7bit`, `quoted-printable` +/// and `base64` is chosen based on the input body. **Best option.** +/// +/// If `encoding` is `Some` the supplied encoding is used. +/// **NOTE:** if using the specified `encoding` would result into a malformed +/// body, this will panic! pub trait IntoBody { fn into_body(self, encoding: Option) -> Body; } diff --git a/src/message/header/content.rs b/src/message/header/content.rs index 866c6c9..1ca4cfb 100644 --- a/src/message/header/content.rs +++ b/src/message/header/content.rs @@ -9,6 +9,11 @@ use std::{ header! { (ContentId, "Content-ID") => [String] } +/// `Content-Transfer-Encoding` of the body +/// +/// The `Message` builder takes care of choosing the most +/// efficient encoding based on the chosen body, so in most +/// use-caches this header shouldn't be set manually. #[derive(Debug, Clone, Copy, PartialEq)] pub enum ContentTransferEncoding { SevenBit, @@ -27,13 +32,12 @@ impl Default for ContentTransferEncoding { impl Display for ContentTransferEncoding { fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult { - use self::ContentTransferEncoding::*; f.write_str(match *self { - SevenBit => "7bit", - QuotedPrintable => "quoted-printable", - Base64 => "base64", - EightBit => "8bit", - Binary => "binary", + Self::SevenBit => "7bit", + Self::QuotedPrintable => "quoted-printable", + Self::Base64 => "base64", + Self::EightBit => "8bit", + Self::Binary => "binary", }) } } @@ -41,13 +45,12 @@ impl Display for ContentTransferEncoding { impl FromStr for ContentTransferEncoding { type Err = String; fn from_str(s: &str) -> Result { - use self::ContentTransferEncoding::*; match s { - "7bit" => Ok(SevenBit), - "quoted-printable" => Ok(QuotedPrintable), - "base64" => Ok(Base64), - "8bit" => Ok(EightBit), - "binary" => Ok(Binary), + "7bit" => Ok(Self::SevenBit), + "quoted-printable" => Ok(Self::QuotedPrintable), + "base64" => Ok(Self::Base64), + "8bit" => Ok(Self::EightBit), + "binary" => Ok(Self::Binary), _ => Err(s.into()), } } diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs index dd7b8db..1fb0033 100644 --- a/src/message/mimebody.rs +++ b/src/message/mimebody.rs @@ -146,7 +146,7 @@ impl SinglePart { &self.headers } - /// Read the body from singlepart + /// Get the encoded body #[inline] pub fn raw_body(&self) -> &[u8] { &self.body diff --git a/src/message/mod.rs b/src/message/mod.rs index e09a074..52e8e53 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -1,16 +1,24 @@ //! Provides a strongly typed way to build emails //! -//! ### Creating messages -//! -//! This section explains how to create emails. -//! //! ## Usage //! -//! ### Format email messages +//! This section demonstrates how to build messages. //! -//! #### With string body +//! +//! //! -//! The easiest way how we can create email message with simple string. +//! +//! ### Plain body +//! +//! The easiest way of creating a message, which uses a plain text body. //! //! ```rust //! use lettre::message::Message; @@ -27,68 +35,36 @@ //! # } //! ``` //! -//! Will produce: +//! Which produces: +//!
+//! Click to expand //! //! ```sh //! From: NoBody //! Reply-To: Yuin //! To: Hei //! Subject: Happy new year +//! Date: Sat, 12 Dec 2020 16:33:19 GMT +//! Content-Transfer-Encoding: 7bit //! //! Be happy! //! ``` +//!
+//!
//! -//! The unicode header data will be encoded using _UTF8-Base64_ encoding. +//! The unicode header data is encoded using _UTF8-Base64_ encoding, when necessary. //! -//! ### With MIME body +//! The `Content-Transfer-Encoding` is chosen based on the best encoding +//! available for the given body, between `7bit`, `quoted-printable` and `base64`. //! -//! ##### Single part +//! ### Plain and HTML body //! -//! The more complex way is using MIME contents. +//! Uses a MIME body to include both plain text and HTML versions of the body. //! //! ```rust -//! use lettre::message::{header, Message, SinglePart, Part}; -//! //! # use std::error::Error; -//! # fn main() -> Result<(), Box> { -//! let m = Message::builder() -//! .from("NoBody ".parse()?) -//! .reply_to("Yuin ".parse()?) -//! .to("Hei ".parse()?) -//! .subject("Happy new year") -//! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType( -//! "text/plain; charset=utf8".parse()?)) -//! .body(String::from("Привет, мир!")), -//! )?; -//! # Ok(()) -//! # } -//! ``` +//! use lettre::message::{header, Message, MultiPart, Part, SinglePart}; //! -//! The body will be encoded using selected `Content-Transfer-Encoding`. -//! -//! ```sh -//! From: NoBody -//! Reply-To: Yuin -//! To: Hei -//! Subject: Happy new year -//! MIME-Version: 1.0 -//! Content-Type: text/plain; charset=utf8 -//! Content-Transfer-Encoding: quoted-printable -//! -//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80! -//! -//! ``` -//! -//! ##### Multiple parts -//! -//! And more advanced way of building message by using multipart MIME contents. -//! -//! ```rust -//! use lettre::message::{header, Message, MultiPart, SinglePart, Part}; -//! -//! # use std::error::Error; //! # fn main() -> Result<(), Box> { //! let m = Message::builder() //! .from("NoBody ".parse()?) @@ -96,93 +72,173 @@ //! .to("Hei ".parse()?) //! .subject("Happy new year") //! .multipart( -//! MultiPart::mixed() -//! .multipart( -//! MultiPart::alternative() +//! MultiPart::alternative() //! .singlepart( //! SinglePart::builder() -//! .header(header::ContentType("text/plain; charset=utf8".parse()?)) -//! .body(String::from("Hello, world! :)")) +//! .header(header::ContentType("text/plain; charset=utf8".parse()?)) +//! .body(String::from("Hello, world! :)")), //! ) -//! .multipart( -//! MultiPart::related() -//! .singlepart( -//! SinglePart::builder() +//! .singlepart( +//! SinglePart::builder() //! .header(header::ContentType("text/html; charset=utf8".parse()?)) -//! .body(String::from("

Hello, world!

")) -//! ) -//! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType("image/png".parse()?)) -//! .header(header::ContentDisposition { -//! disposition: header::DispositionType::Inline, -//! parameters: vec![], -//! }) -//! .header(header::ContentId("<123>".into())) -//! .body(Vec::::from("")) -//! ) -//! ) -//! ) -//! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType("text/plain; charset=utf8".parse()?)) -//! .header(header::ContentDisposition { -//! disposition: header::DispositionType::Attachment, -//! parameters: vec![ -//! header::DispositionParam::Filename( -//! header::Charset::Ext("utf-8".into()), -//! None, "example.c".as_bytes().into() -//! ) -//! ] -//! }) -//! .body(String::from("int main() { return 0; }")) -//! ) +//! .body(String::from( +//! "

Hello, world!

", +//! )), +//! ), //! )?; //! # Ok(()) //! # } //! ``` //! +//! Which produces: +//!
+//! Click to expand +//! //! ```sh //! From: NoBody //! Reply-To: Yuin //! To: Hei //! Subject: Happy new year //! MIME-Version: 1.0 -//! Content-Type: multipart/mixed; boundary="RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m" +//! Date: Sat, 12 Dec 2020 16:33:19 GMT +//! Content-Type: multipart/alternative; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1" //! -//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m -//! Content-Type: multipart/alternative; boundary="qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy" -//! -//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy -//! Content-Transfer-Encoding: quoted-printable +//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 //! Content-Type: text/plain; charset=utf8 -//! -//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80! -//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy -//! Content-Type: multipart/related; boundary="BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8" -//! -//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8 -//! Content-Transfer-Encoding: 8bit -//! Content-Type: text/html; charset=utf8 -//! -//!

Hello, world!

-//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8 -//! Content-Transfer-Encoding: base64 -//! Content-Type: image/png -//! Content-Disposition: inline -//! -//! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg== -//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8-- -//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy-- -//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m //! Content-Transfer-Encoding: 7bit -//! Content-Type: text/plain; charset=utf8 -//! Content-Disposition: attachment; filename="example.c" //! -//! int main() { return 0; } -//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m-- +//! Hello, world! :) +//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 +//! Content-Type: text/html; charset=utf8 +//! Content-Transfer-Encoding: 7bit +//! +//!

Hello, world!

+//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1-- //! //! ``` +//!
+//! +//! ### Complex MIME body +//! +//! This example shows how to include both plain and HTML versions of the body, +//! attachments and inlined images. +//! +//! ```rust +//! # use std::error::Error; +//! use std::fs; +//! use lettre::message::{Body, header, Message, MultiPart, Part, SinglePart}; +//! +//! # fn main() -> Result<(), Box> { +//! let image = fs::read("docs/lettre.png")?; +//! // this image_body can be cloned and reused between emails. +//! // since `Body` holds a pre-encoded body, reusing it means avoiding having +//! // to re-encode the same body for every email (this clearly only applies +//! // when sending multiple emails with the same attachment). +//! let image_body = Body::new(image); +//! +//! let m = Message::builder() +//! .from("NoBody ".parse()?) +//! .reply_to("Yuin ".parse()?) +//! .to("Hei ".parse()?) +//! .subject("Happy new year") +//! .multipart( +//! MultiPart::mixed() +//! .multipart( +//! MultiPart::alternative() +//! .singlepart( +//! SinglePart::builder() +//! .header(header::ContentType("text/plain; charset=utf8".parse()?)) +//! .body(String::from("Hello, world! :)")), +//! ) +//! .multipart( +//! MultiPart::related() +//! .singlepart( +//! SinglePart::builder() +//! .header(header::ContentType( +//! "text/html; charset=utf8".parse()?, +//! )) +//! .body(String::from( +//! "

Hello, world!

", +//! )), +//! ) +//! .singlepart( +//! SinglePart::builder() +//! .header(header::ContentType("image/png".parse()?)) +//! .header(header::ContentDisposition { +//! disposition: header::DispositionType::Inline, +//! parameters: vec![], +//! }) +//! .header(header::ContentId("<123>".into())) +//! .body(image_body), +//! ), +//! ), +//! ) +//! .singlepart( +//! SinglePart::builder() +//! .header(header::ContentType("text/plain; charset=utf8".parse()?)) +//! .header(header::ContentDisposition { +//! disposition: header::DispositionType::Attachment, +//! parameters: vec![header::DispositionParam::Filename( +//! header::Charset::Ext("utf-8".into()), +//! None, +//! "example.rs".as_bytes().into(), +//! )], +//! }) +//! .body(String::from("fn main() { println!(\"Hello, World!\") }")), +//! ), +//! )?; +//! # Ok(()) +//! # } +//! ``` +//! +//! Which produces: +//!
+//! Click to expand +//! +//! ```sh +//! From: NoBody +//! Reply-To: Yuin +//! To: Hei +//! Subject: Happy new year +//! MIME-Version: 1.0 +//! Date: Sat, 12 Dec 2020 16:30:45 GMT +//! Content-Type: multipart/mixed; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1" +//! +//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 +//! Content-Type: multipart/alternative; boundary="EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk" +//! +//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk +//! Content-Type: text/plain; charset=utf8 +//! Content-Transfer-Encoding: 7bit +//! +//! Hello, world! :) +//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk +//! Content-Type: multipart/related; boundary="eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr" +//! +//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr +//! Content-Type: text/html; charset=utf8 +//! Content-Transfer-Encoding: 7bit +//! +//!

Hello, world!

+//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr +//! Content-Type: image/png +//! Content-Disposition: inline +//! Content-ID: <123> +//! Content-Transfer-Encoding: base64 +//! +//! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg== +//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr-- +//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk-- +//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 +//! Content-Type: text/plain; charset=utf8 +//! Content-Disposition: attachment; filename="example.rs" +//! Content-Transfer-Encoding: 7bit +//! +//! fn main() { println!("Hello, World!") } +//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1-- +//! +//! ``` +//!
pub use body::{Body, IntoBody, MaybeString}; pub use mailbox::*; @@ -478,6 +534,7 @@ impl Message { impl EmailFormat for Message { fn format(&self, out: &mut Vec) { out.extend_from_slice(self.headers.to_string().as_bytes()); + match &self.body { MessageBody::Mime(p) => p.format(out), MessageBody::Raw(r) => {