use std::fmt::Write as _;
use crate::error::SmtpError;
#[derive(Debug, Clone)]
pub struct Address {
pub email: String,
pub name: Option<String>,
}
impl Address {
#[must_use]
pub fn new(email: &str) -> Self {
Self {
email: email.to_string(),
name: None,
}
}
#[must_use]
pub fn with_name(name: &str, email: &str) -> Self {
Self {
email: email.to_string(),
name: Some(name.to_string()),
}
}
#[must_use]
pub fn to_header(&self) -> String {
self.name.as_ref().map_or_else(
|| self.email.clone(),
|name| format!("\"{}\" <{}>", name, self.email),
)
}
#[must_use]
pub fn to_envelope(&self) -> &str {
&self.email
}
}
#[derive(Debug, Clone)]
pub struct Email {
pub from: Address,
pub to: Vec<Address>,
pub cc: Vec<Address>,
pub bcc: Vec<Address>,
pub subject: String,
pub body: String,
pub message_id: Option<String>,
pub date: Option<String>,
}
impl Email {
#[must_use]
pub fn builder() -> EmailBuilder {
EmailBuilder::default()
}
pub fn all_recipients(&self) -> impl Iterator<Item = &Address> {
self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter())
}
#[must_use]
pub fn headers(&self) -> String {
let mut h = String::with_capacity(512);
let _ = write!(h, "From: {}\r\n", self.from.to_header());
let to_list: Vec<String> = self.to.iter().map(Address::to_header).collect();
let _ = write!(h, "To: {}\r\n", to_list.join(", "));
if !self.cc.is_empty() {
let cc_list: Vec<String> = self.cc.iter().map(Address::to_header).collect();
let _ = write!(h, "Cc: {}\r\n", cc_list.join(", "));
}
let _ = write!(h, "Subject: {}\r\n", self.subject);
if let Some(ref date) = self.date {
let _ = write!(h, "Date: {date}\r\n");
}
h.push_str("MIME-Version: 1.0\r\n");
h.push_str("Content-Type: text/plain; charset=UTF-8\r\n");
if let Some(ref id) = self.message_id {
let _ = write!(h, "Message-ID: <{id}>\r\n");
}
let _ = write!(
h,
"X-Mailer: esp-idf-smtp/{}\r\n",
env!("CARGO_PKG_VERSION")
);
h
}
#[must_use]
pub fn formatted_body(&self) -> String {
let mut out = String::with_capacity(self.body.len() + 64);
for line in self.body.split('\n') {
let line = line.strip_suffix('\r').unwrap_or(line);
if line.starts_with('.') {
out.push('.');
}
out.push_str(line);
out.push_str("\r\n");
}
out
}
}
#[derive(Debug, Default)]
pub struct EmailBuilder {
from: Option<Address>,
to: Vec<Address>,
cc: Vec<Address>,
bcc: Vec<Address>,
subject: Option<String>,
body: Option<String>,
message_id: Option<String>,
date: Option<String>,
}
impl EmailBuilder {
#[must_use]
pub fn from(mut self, email: &str) -> Self {
self.from = Some(Address::new(email));
self
}
#[must_use]
pub fn from_named(mut self, name: &str, email: &str) -> Self {
self.from = Some(Address::with_name(name, email));
self
}
#[must_use]
pub fn to(mut self, email: &str) -> Self {
self.to.push(Address::new(email));
self
}
#[must_use]
pub fn cc(mut self, email: &str) -> Self {
self.cc.push(Address::new(email));
self
}
#[must_use]
pub fn bcc(mut self, email: &str) -> Self {
self.bcc.push(Address::new(email));
self
}
#[must_use]
pub fn subject(mut self, subject: &str) -> Self {
self.subject = Some(subject.to_string());
self
}
#[must_use]
pub fn body(mut self, body: &str) -> Self {
self.body = Some(body.to_string());
self
}
#[must_use]
pub fn message_id(mut self, id: &str) -> Self {
self.message_id = Some(id.to_string());
self
}
#[must_use]
pub fn date(mut self, date: &str) -> Self {
self.date = Some(date.to_string());
self
}
pub fn build(self) -> Result<Email, SmtpError> {
let from = self.from.ok_or_else(|| SmtpError::InvalidEmail {
message: "from address is required".into(),
})?;
if self.to.is_empty() {
return Err(SmtpError::InvalidEmail {
message: "at least one To recipient is required".into(),
});
}
let subject = self.subject.ok_or_else(|| SmtpError::InvalidEmail {
message: "subject is required".into(),
})?;
let body = self.body.ok_or_else(|| SmtpError::InvalidEmail {
message: "body is required".into(),
})?;
Ok(Email {
from,
to: self.to,
cc: self.cc,
bcc: self.bcc,
subject,
body,
message_id: self.message_id,
date: self.date,
})
}
}