Files
lettre/src/message/mod.rs
Federico Guerinoni 50ac1cdbec src/message: Improve example
Signed-off-by: Federico Guerinoni <guerinoni.federico@gmail.com>
2020-11-22 17:45:52 +01:00

559 lines
17 KiB
Rust

//! Provides a strongly typed way to build emails
//!
//! ### Creating messages
//!
//! This section explains how to create emails.
//!
//! ## Usage
//!
//! ### Format email messages
//!
//! #### With string body
//!
//! The easiest way how we can create email message with simple string.
//!
//! ```rust
//! use lettre::message::Message;
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")?;
//! # Ok(())
//! # }
//! ```
//!
//! Will produce:
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//!
//! Be happy!
//! ```
//!
//! The unicode header data will be encoded using _UTF8-Base64_ encoding.
//!
//! ### With MIME body
//!
//! ##### Single part
//!
//! The more complex way is using MIME contents.
//!
//! ```rust
//! use lettre::message::{header, Message, SinglePart, Part};
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/plain; charset=utf8".parse()?,
//! )).header(header::ContentTransferEncoding::QuotedPrintable)
//! .body("Привет, мир!"),
//! )?;
//! # Ok(())
//! # }
//! ```
//!
//! The body will be encoded using selected `Content-Transfer-Encoding`.
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! 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<dyn Error>> {
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .multipart(
//! MultiPart::mixed()
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::quoted_printable()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body("Hello, world! :)")
//! )
//! .multipart(
//! MultiPart::related()
//! .singlepart(
//! SinglePart::eight_bit()
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
//! .body("<p><b>Hello</b>, <i>world</i>! <img src=cid:123>")
//! )
//! .singlepart(
//! SinglePart::base64()
//! .header(header::ContentType("image/png".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
//! })
//! .header(header::ContentId("<123>".into()))
//! .body("<smile-raw-image-data>")
//! )
//! )
//! )
//! .singlepart(
//! SinglePart::seven_bit()
//! .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("int main() { return 0; }")
//! )
//! )?;
//! # Ok(())
//! # }
//! ```
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! MIME-Version: 1.0
//! Content-Type: multipart/mixed; boundary="RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m"
//!
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
//! Content-Type: multipart/alternative; boundary="qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy"
//!
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
//! Content-Transfer-Encoding: quoted-printable
//! 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
//!
//! <p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>
//! --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--
//!
//! ```
pub use mailbox::*;
pub use mimebody::*;
pub use mime;
mod encoder;
pub mod header;
mod mailbox;
mod mimebody;
mod utf8_b;
use crate::{
address::Envelope,
message::header::{EmailDate, Header, Headers, MailboxesHeader},
Error as EmailError,
};
use std::{convert::TryFrom, time::SystemTime};
use uuid::Uuid;
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
pub trait EmailFormat {
// Use a writer?
fn format(&self, out: &mut Vec<u8>);
}
/// A builder for messages
#[derive(Debug, Clone)]
pub struct MessageBuilder {
headers: Headers,
envelope: Option<Envelope>,
}
impl MessageBuilder {
/// Creates a new default message builder
pub fn new() -> Self {
Self {
headers: Headers::new(),
envelope: None,
}
}
/// Set custom header to message
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
if self.headers.has::<H>() {
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
self
} else {
self.header(header)
}
}
/// Add `Date` header to message
///
/// Shortcut for `self.header(header::Date(date))`.
pub fn date(self, date: EmailDate) -> Self {
self.header(header::Date(date))
}
/// Set `Date` header using current date/time
///
/// Shortcut for `self.date(SystemTime::now())`.
pub fn date_now(self) -> Self {
self.date(SystemTime::now().into())
}
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
self.header(header::Subject(subject.into()))
}
/// Set `Mime-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
/// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.header(header::Sender(mbox))`.
pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender(mbox))
}
/// Set or add mailbox to `From` header
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From(mbox.into()))
}
/// Set or add mailbox to `ReplyTo` header
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
pub fn reply_to(self, mbox: Mailbox) -> Self {
self.mailbox(header::ReplyTo(mbox.into()))
}
/// Set or add mailbox to `To` header
///
/// Shortcut for `self.mailbox(header::To(mbox))`.
pub fn to(self, mbox: Mailbox) -> Self {
self.mailbox(header::To(mbox.into()))
}
/// Set or add mailbox to `Cc` header
///
/// Shortcut for `self.mailbox(header::Cc(mbox))`.
pub fn cc(self, mbox: Mailbox) -> Self {
self.mailbox(header::Cc(mbox.into()))
}
/// Set or add mailbox to `Bcc` header
///
/// Shortcut for `self.mailbox(header::Bcc(mbox))`.
pub fn bcc(self, mbox: Mailbox) -> Self {
self.mailbox(header::Bcc(mbox.into()))
}
/// Set or add message id to [`In-Reply-To`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn in_reply_to(self, id: String) -> Self {
self.header(header::InReplyTo(id))
}
/// Set or add message id to [`References`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn references(self, id: String) -> Self {
self.header(header::References(id))
}
/// Set [Message-Id
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
///
/// Should generally be inserted by the mail relay.
///
/// If `None` is provided, an id will be generated in the
/// `<UUID@HOSTNAME>`.
pub fn message_id(self, id: Option<String>) -> Self {
match id {
Some(i) => self.header(header::MessageId(i)),
None => {
#[cfg(feature = "hostname")]
let hostname = hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string());
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
self.header(header::MessageId(
// https://tools.ietf.org/html/rfc5322#section-3.6.4
format!("<{}@{}>", Uuid::new_v4(), hostname),
))
}
}
}
/// Set [User-Agent
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
pub fn user_agent(self, id: String) -> Self {
self.header(header::UserAgent(id))
}
/// Force specific envelope (by default it is derived from headers)
pub fn envelope(mut self, envelope: Envelope) -> Self {
self.envelope = Some(envelope);
self
}
// TODO: High-level methods for attachments and embedded files
/// Create message from body
fn build(self, body: Body) -> Result<Message, EmailError> {
// Check for missing required headers
// https://tools.ietf.org/html/rfc5322#section-3.6
// Insert Date if missing
let res = if self.headers.get::<header::Date>().is_none() {
self.date_now()
} else {
self
};
// Fail is missing correct originator (Sender or From)
match res.headers.get::<header::From>() {
Some(header::From(f)) => {
let from: Vec<Mailbox> = f.clone().into();
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
return Err(EmailError::TooManyFrom);
}
}
None => {
return Err(EmailError::MissingFrom);
}
}
let envelope = match res.envelope {
Some(e) => e,
None => Envelope::try_from(&res.headers)?,
};
Ok(Message {
headers: res.headers,
body,
envelope,
})
}
// In theory having a body is optional
/// Plain ASCII body
///
/// *WARNING*: Generally not what you want
pub fn body<T: Into<String>>(self, body: T) -> Result<Message, EmailError> {
// 998 chars by line
// CR and LF MUST only occur together as CRLF; they MUST NOT appear
// independently in the body.
let body = body.into();
if !&body.is_ascii() {
return Err(EmailError::NonAsciiChars);
}
self.build(Body::Raw(body))
}
/// Create message using mime body ([`MultiPart`][self::MultiPart])
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Multi(part)))
}
/// Create message using mime body ([`SinglePart`][self::SinglePart])
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Single(part)))
}
}
/// Email message which can be formatted
#[derive(Clone, Debug)]
pub struct Message {
headers: Headers,
body: Body,
envelope: Envelope,
}
#[derive(Clone, Debug)]
enum Body {
Mime(Part),
Raw(String),
}
impl Message {
/// Create a new message builder without headers
pub fn builder() -> MessageBuilder {
MessageBuilder::new()
}
/// Get the headers from the Message
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Get `Message` envelope
pub fn envelope(&self) -> &Envelope {
&self.envelope
}
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
impl EmailFormat for Message {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
match &self.body {
Body::Mime(p) => p.format(out),
Body::Raw(r) => {
out.extend_from_slice(b"\r\n");
out.extend(r.as_bytes())
}
}
}
}
impl Default for MessageBuilder {
fn default() -> Self {
MessageBuilder::new()
}
}
#[cfg(test)]
mod test {
use crate::message::{header, mailbox::Mailbox, Message};
#[test]
fn email_missing_originator() {
assert!(Message::builder().body("Happy new year!").is_err());
}
#[test]
fn email_miminal_message() {
assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("NoBody <nobody@domain.tld>".parse().unwrap())
.body("Happy new year!")
.is_ok());
}
#[test]
fn email_missing_sender() {
assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.from("AnyBody <anybody@domain.tld>".parse().unwrap())
.body("Happy new year!")
.is_err());
}
#[test]
fn email_message() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
let email = Message::builder()
.date(date)
.header(header::From(
vec![Mailbox::new(
Some("Каи".into()),
"kayo@example.com".parse().unwrap(),
)]
.into(),
))
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject("яңа ел белән!".into()))
.body("Happy new year!")
.unwrap();
assert_eq!(
String::from_utf8(email.formatted()).unwrap(),
concat!(
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: Pony O.P. <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"\r\n",
"Happy new year!"
)
);
}
}