From 904789ac3d668414cc6b836a3bee0b115332a7dc Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Fri, 14 May 2021 16:59:08 +0200 Subject: [PATCH] feat(builder): Add helper methods for attachments and text (#618) --- src/message/attachment.rs | 91 +++++++++++++++++++++++++++++++++++++++ src/message/mimebody.rs | 23 +++++++++- src/message/mod.rs | 61 +++++++++----------------- 3 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 src/message/attachment.rs diff --git a/src/message/attachment.rs b/src/message/attachment.rs new file mode 100644 index 0000000..5950c33 --- /dev/null +++ b/src/message/attachment.rs @@ -0,0 +1,91 @@ +use crate::message::{ + header::{self, ContentType}, + IntoBody, SinglePart, +}; + +/// `SinglePart` builder for attachments +/// +/// Allows building attachment parts easily. +#[derive(Clone)] +pub struct Attachment { + disposition: Disposition, +} + +#[derive(Clone)] +enum Disposition { + /// file name + Attached(String), + /// content id + Inline(String), +} + +impl Attachment { + /// Creates a new attachment + pub fn new(filename: String) -> Self { + Attachment { + disposition: Disposition::Attached(filename), + } + } + + /// Creates a new inline attachment + pub fn new_inline(content_id: String) -> Self { + Attachment { + disposition: Disposition::Inline(content_id), + } + } + + /// Build the attachment part + pub fn body(self, content: T, content_type: ContentType) -> SinglePart { + let mut builder = SinglePart::builder(); + builder = match self.disposition { + Disposition::Attached(filename) => { + builder.header(header::ContentDisposition::attachment(&filename)) + } + Disposition::Inline(content_id) => builder + .header(header::ContentId::from(format!("<{}>", content_id))) + .header(header::ContentDisposition::inline()), + }; + builder = builder.header(content_type); + builder.body(content) + } +} + +#[cfg(test)] +mod tests { + use crate::message::header::ContentType; + + #[test] + fn attachment() { + let part = super::Attachment::new(String::from("test.txt")).body( + String::from("Hello world!"), + ContentType::parse("text/plain").unwrap(), + ); + assert_eq!( + &String::from_utf8_lossy(&part.formatted()), + concat!( + "Content-Disposition: attachment; filename=\"test.txt\"\r\n", + "Content-Type: text/plain\r\n", + "Content-Transfer-Encoding: 7bit\r\n\r\n", + "Hello world!\r\n", + ) + ); + } + + #[test] + fn attachment_inline() { + let part = super::Attachment::new_inline(String::from("id")).body( + String::from("Hello world!"), + ContentType::parse("text/plain").unwrap(), + ); + assert_eq!( + &String::from_utf8_lossy(&part.formatted()), + concat!( + "Content-ID: \r\n", + "Content-Disposition: inline\r\n", + "Content-Type: text/plain\r\n", + "Content-Transfer-Encoding: 7bit\r\n\r\n", + "Hello world!\r\n" + ) + ); + } +} diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs index e039c7c..269efba 100644 --- a/src/message/mimebody.rs +++ b/src/message/mimebody.rs @@ -1,7 +1,7 @@ use std::io::Write; use crate::message::{ - header::{ContentTransferEncoding, ContentType, Header, Headers}, + header::{self, ContentTransferEncoding, ContentType, Header, Headers}, EmailFormat, IntoBody, }; use mime::Mime; @@ -112,6 +112,20 @@ impl SinglePart { SinglePartBuilder::new() } + /// Directly create a `SinglePart` from an plain UTF-8 content + pub fn plain(body: T) -> Self { + Self::builder() + .header(header::ContentType::TEXT_PLAIN) + .body(body) + } + + /// Directly create a `SinglePart` from an UTF-8 HTML content + pub fn html(body: T) -> Self { + Self::builder() + .header(header::ContentType::TEXT_HTML) + .body(body) + } + /// Get the headers from singlepart #[inline] pub fn headers(&self) -> &Headers { @@ -330,6 +344,13 @@ impl MultiPart { MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg }) } + /// Alias for HTML and plain text versions of an email + pub fn alternative_plain_html(plain: T, html: V) -> Self { + Self::alternative() + .singlepart(SinglePart::plain(plain)) + .singlepart(SinglePart::html(html)) + } + /// Add part to multipart pub fn part(mut self, part: Part) -> Self { self.parts.push(part); diff --git a/src/message/mod.rs b/src/message/mod.rs index 34adce1..06fdcc6 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -71,21 +71,10 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .multipart( -//! MultiPart::alternative() -//! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType::TEXT_PLAIN) -//! .body(String::from("Hello, world! :)")), -//! ) -//! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType::TEXT_HTML) -//! .body(String::from( -//! "

Hello, world!

", -//! )), -//! ), -//! )?; +//! .multipart(MultiPart::alternative_plain_html( +//! String::from("Hello, world! :)"), +//! String::from("

Hello, world!

"), +//! ))?; //! # Ok(()) //! # } //! ``` @@ -124,7 +113,7 @@ //! //! ```rust //! # use std::error::Error; -//! use lettre::message::{header, Body, Message, MultiPart, Part, SinglePart}; +//! use lettre::message::{header, Attachment, Body, Message, MultiPart, Part, SinglePart}; //! use std::fs; //! //! # fn main() -> Result<(), Box> { @@ -144,35 +133,22 @@ //! MultiPart::mixed() //! .multipart( //! MultiPart::alternative() -//! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType::TEXT_PLAIN) -//! .body(String::from("Hello, world! :)")), -//! ) +//! .singlepart(SinglePart::plain(String::from("Hello, world! :)"))) //! .multipart( //! MultiPart::related() +//! .singlepart(SinglePart::html(String::from( +//! "

Hello, world!

", +//! ))) //! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType::TEXT_HTML) -//! .body(String::from( -//! "

Hello, world!

", -//! )), -//! ) -//! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType::parse("image/png")?) -//! .header(header::ContentDisposition::inline()) -//! .header(header::ContentId::from(String::from("<123>"))) -//! .body(image_body), +//! Attachment::new_inline(String::from("123")) +//! .body(image_body, "image/png".parse().unwrap()), //! ), //! ), //! ) -//! .singlepart( -//! SinglePart::builder() -//! .header(header::ContentType::TEXT_PLAIN) -//! .header(header::ContentDisposition::attachment("example.rs")) -//! .body(String::from("fn main() { println!(\"Hello, World!\") }")), -//! ), +//! .singlepart(Attachment::new(String::from("example.rs")).body( +//! String::from("fn main() { println!(\"Hello, World!\") }"), +//! "text/plain".parse().unwrap(), +//! )), //! )?; //! # Ok(()) //! # } @@ -228,10 +204,12 @@ use std::{convert::TryFrom, io::Write, iter, time::SystemTime}; +pub use attachment::Attachment; pub use body::{Body, IntoBody, MaybeString}; pub use mailbox::*; pub use mimebody::*; +mod attachment; mod body; pub mod header; mod mailbox; @@ -293,7 +271,8 @@ impl MessageBuilder { /// Set `Date` header using current date/time /// - /// Shortcut for `self.date(SystemTime::now())`. + /// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted + /// if no date has been provided. pub fn date_now(self) -> Self { self.date(SystemTime::now()) } @@ -306,7 +285,7 @@ impl MessageBuilder { self.header(header::Subject::from(s)) } - /// Set `Mime-Version` header to 1.0 + /// Set `MIME-Version` header to 1.0 /// /// Shortcut for `self.header(header::MIME_VERSION_1_0)`. ///