#![doc = include_str!("../README.md")]
#![deny(rust_2018_idioms)]
#[forbid(unsafe_code)]
pub mod encoders;
pub mod headers;
pub mod mime;
use std::{
borrow::Cow,
io::{self, Write},
};
use headers::{
address::Address,
content_type::ContentType,
date::Date,
message_id::{generate_message_id_header, MessageId},
text::Text,
Header, HeaderType,
};
use mime::{BodyPart, MimePart};
#[derive(Clone, Debug)]
pub struct MessageBuilder<'x> {
pub headers: Vec<(Cow<'x, str>, HeaderType<'x>)>,
pub html_body: Option<MimePart<'x>>,
pub text_body: Option<MimePart<'x>>,
pub attachments: Option<Vec<MimePart<'x>>>,
pub body: Option<MimePart<'x>>,
}
impl Default for MessageBuilder<'_> {
fn default() -> Self {
Self::new()
}
}
impl<'x> MessageBuilder<'x> {
pub fn new() -> Self {
MessageBuilder {
headers: Vec::new(),
html_body: None,
text_body: None,
attachments: None,
body: None,
}
}
pub fn message_id(self, value: impl Into<MessageId<'x>>) -> Self {
self.header("Message-ID", value.into())
}
pub fn in_reply_to(self, value: impl Into<MessageId<'x>>) -> Self {
self.header("In-Reply-To", value.into())
}
pub fn references(self, value: impl Into<MessageId<'x>>) -> Self {
self.header("References", value.into())
}
pub fn sender(self, value: impl Into<Address<'x>>) -> Self {
self.header("Sender", value.into())
}
pub fn from(self, value: impl Into<Address<'x>>) -> Self {
self.header("From", value.into())
}
pub fn to(self, value: impl Into<Address<'x>>) -> Self {
self.header("To", value.into())
}
pub fn cc(self, value: impl Into<Address<'x>>) -> Self {
self.header("Cc", value.into())
}
pub fn bcc(self, value: impl Into<Address<'x>>) -> Self {
self.header("Bcc", value.into())
}
pub fn reply_to(self, value: impl Into<Address<'x>>) -> Self {
self.header("Reply-To", value.into())
}
pub fn subject(self, value: impl Into<Text<'x>>) -> Self {
self.header("Subject", value.into())
}
pub fn date(self, value: impl Into<Date>) -> Self {
self.header("Date", value.into())
}
pub fn header(
mut self,
header: impl Into<Cow<'x, str>>,
value: impl Into<HeaderType<'x>>,
) -> Self {
self.headers.push((header.into(), value.into()));
self
}
pub fn headers<T, U, V>(mut self, header: T, values: U) -> Self
where
T: Into<Cow<'x, str>>,
U: IntoIterator<Item = V>,
V: Into<HeaderType<'x>>,
{
let header = header.into();
for value in values {
self.headers.push((header.clone(), value.into()));
}
self
}
pub fn text_body(mut self, value: impl Into<Cow<'x, str>>) -> Self {
self.text_body = Some(MimePart::new("text/plain", BodyPart::Text(value.into())));
self
}
pub fn html_body(mut self, value: impl Into<Cow<'x, str>>) -> Self {
self.html_body = Some(MimePart::new("text/html", BodyPart::Text(value.into())));
self
}
pub fn attachment(
mut self,
content_type: impl Into<ContentType<'x>>,
filename: impl Into<Cow<'x, str>>,
value: impl Into<BodyPart<'x>>,
) -> Self {
self.attachments
.get_or_insert_with(Vec::new)
.push(MimePart::new(content_type, value).attachment(filename));
self
}
pub fn inline(
mut self,
content_type: impl Into<ContentType<'x>>,
cid: impl Into<Cow<'x, str>>,
value: impl Into<BodyPart<'x>>,
) -> Self {
self.attachments
.get_or_insert_with(Vec::new)
.push(MimePart::new(content_type, value).inline().cid(cid));
self
}
pub fn body(mut self, value: MimePart<'x>) -> Self {
self.body = Some(value);
self
}
pub fn write_to(self, mut output: impl Write) -> io::Result<()> {
let mut has_date = false;
let mut has_message_id = false;
let mut has_mime_version = false;
for (header_name, header_value) in &self.headers {
if !has_date && header_name == "Date" {
has_date = true;
} else if !has_message_id && header_name == "Message-ID" {
has_message_id = true;
} else if !has_mime_version && header_name == "MIME-Version" {
has_mime_version = true;
}
output.write_all(header_name.as_bytes())?;
output.write_all(b": ")?;
header_value.write_header(&mut output, header_name.len() + 2)?;
}
if !has_message_id {
output.write_all(b"Message-ID: ")?;
#[cfg(feature = "gethostname")]
generate_message_id_header(
&mut output,
gethostname::gethostname().to_str().unwrap_or("localhost"),
)?;
#[cfg(not(feature = "gethostname"))]
generate_message_id_header(&mut output, "localhost")?;
output.write_all(b"\r\n")?;
}
if !has_date {
output.write_all(b"Date: ")?;
output.write_all(Date::now().to_rfc822().as_bytes())?;
output.write_all(b"\r\n")?;
}
if !has_mime_version {
output.write_all(b"MIME-Version: 1.0\r\n")?;
}
self.write_body(output)
}
pub fn write_body(self, output: impl Write) -> io::Result<()> {
(if let Some(body) = self.body {
body
} else {
match (self.text_body, self.html_body, self.attachments) {
(Some(text), Some(html), Some(attachments)) => {
let mut parts = Vec::with_capacity(attachments.len() + 1);
parts.push(MimePart::new("multipart/alternative", vec![text, html]));
parts.extend(attachments);
MimePart::new("multipart/mixed", parts)
}
(Some(text), Some(html), None) => {
MimePart::new("multipart/alternative", vec![text, html])
}
(Some(text), None, Some(attachments)) => {
let mut parts = Vec::with_capacity(attachments.len() + 1);
parts.push(text);
parts.extend(attachments);
MimePart::new("multipart/mixed", parts)
}
(Some(text), None, None) => text,
(None, Some(html), Some(attachments)) => {
let mut parts = Vec::with_capacity(attachments.len() + 1);
parts.push(html);
parts.extend(attachments);
MimePart::new("multipart/mixed", parts)
}
(None, Some(html), None) => html,
(None, None, Some(attachments)) => MimePart::new("multipart/mixed", attachments),
(None, None, None) => MimePart::new("text/plain", "\n"),
}
})
.write_part(output)?;
Ok(())
}
pub fn write_to_vec(self) -> io::Result<Vec<u8>> {
let mut output = Vec::new();
self.write_to(&mut output)?;
Ok(output)
}
pub fn write_to_string(self) -> io::Result<String> {
let mut output = Vec::new();
self.write_to(&mut output)?;
String::from_utf8(output).map_err(io::Error::other)
}
}
#[cfg(test)]
mod tests {
use mail_parser::MessageParser;
use crate::{
headers::{address::Address, url::URL},
mime::MimePart,
MessageBuilder,
};
#[test]
fn build_nested_message() {
let output = MessageBuilder::new()
.from(Address::new_address("John Doe".into(), "john@doe.com"))
.to(Address::new_address("Jane Doe".into(), "jane@doe.com"))
.subject("RFC 8621 Section 4.1.4 test")
.body(MimePart::new(
"multipart/mixed",
vec![
MimePart::new("text/plain", "Part A contents go here...").inline(),
MimePart::new(
"multipart/mixed",
vec![
MimePart::new(
"multipart/alternative",
vec![
MimePart::new(
"multipart/mixed",
vec![
MimePart::new(
"text/plain",
"Part B contents go here...",
)
.inline(),
MimePart::new(
"image/jpeg",
"Part C contents go here...".as_bytes(),
)
.inline(),
MimePart::new(
"text/plain",
"Part D contents go here...",
)
.inline(),
],
),
MimePart::new(
"multipart/related",
vec![
MimePart::new(
"text/html",
"Part E contents go here...",
)
.inline(),
MimePart::new(
"image/jpeg",
"Part F contents go here...".as_bytes(),
),
],
),
],
),
MimePart::new("image/jpeg", "Part G contents go here...".as_bytes())
.attachment("image_G.jpg"),
MimePart::new(
"application/x-excel",
"Part H contents go here...".as_bytes(),
),
MimePart::new(
"x-message/rfc822",
"Part J contents go here...".as_bytes(),
),
],
),
MimePart::new("text/plain", "Part K contents go here...").inline(),
],
))
.write_to_vec()
.unwrap();
MessageParser::new().parse(&output).unwrap();
}
#[test]
fn build_message() {
let output = MessageBuilder::new()
.from(("John Doe", "john@doe.com"))
.to(vec![
("Antoine de Saint-Exupéry", "antoine@exupery.com"),
("안녕하세요 세계", "test@test.com"),
("Xin chào", "addr@addr.com"),
])
.bcc(vec![
(
"Привет, мир",
vec![
("ASCII recipient", "addr1@addr7.com"),
("ハロー・ワールド", "addr2@addr6.com"),
("áéíóú", "addr3@addr5.com"),
("Γειά σου Κόσμε", "addr4@addr4.com"),
],
),
(
"Hello world",
vec![
("שלום עולם", "addr5@addr3.com"),
("¡El ñandú comió ñoquis!", "addr6@addr2.com"),
("Recipient", "addr7@addr1.com"),
],
),
])
.header("List-Archive", URL::new("http://example.com/archive"))
.subject("Hello world!")
.text_body("Hello, world!\n".repeat(20))
.html_body("<p>¡Hola Mundo!</p>".repeat(20))
.inline("image/png", "cid:image", [0, 1, 2, 3, 4, 5].as_ref())
.attachment("text/plain", "my fíle.txt", "안녕하세요 세계".repeat(20))
.attachment(
"text/plain",
"ハロー・ワールド",
"ハロー・ワールド".repeat(20).into_bytes(),
)
.write_to_vec()
.unwrap();
MessageParser::new().parse(&output).unwrap();
}
}