use super::mime_type::Mime;
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct MailHeaders(pub Vec<(String, String)>);
impl std::fmt::Display for MailHeaders {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for i in self.0.iter().map(|(k, v)| HeaderFoldable(k, v)) {
write!(f, "{i}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub enum BodyType {
Regular(Vec<String>),
Mime(Box<Mime>),
Undefined,
}
impl Default for BodyType {
fn default() -> Self {
Self::Undefined
}
}
impl std::fmt::Display for BodyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Regular(content) => {
for i in content {
if i.starts_with('.') {
std::fmt::Write::write_char(f, '.')?;
}
f.write_str(i)?;
f.write_str("\r\n")?;
}
Ok(())
}
Self::Mime(content) => {
write!(f, "{content}")
}
Self::Undefined => Ok(()),
}
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct Mail {
pub headers: MailHeaders,
pub body: BodyType,
}
#[derive(Debug)]
struct HeaderFoldable<'a>(&'a str, &'a str);
impl<'a> std::fmt::Display for HeaderFoldable<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let key = convert_case::Casing::to_case(&self.0, convert_case::Case::Train)
.replace("Id", "ID")
.replace("Mime-Version", "MIME-Version")
.replace("Dkim", "DKIM")
.replace("Arc", "ARC")
.replace("Spf", "SPF")
.replace("X-Ms", "X-MS")
.replace("X-Vr", "X-VR");
f.write_str(&key)?;
f.write_str(": ")?;
let mut byte_writable = self.1;
if byte_writable.is_empty() {
return f.write_str("\r\n");
}
let mut prev = key.len() + 2;
while !byte_writable.is_empty() {
let (left, right) = if byte_writable.len() + prev > 998 {
byte_writable[..998 - prev]
.rfind(char::is_whitespace)
.map(|idx| (&byte_writable[..idx], &byte_writable[idx..]))
} else {
None
}
.unwrap_or((byte_writable, ""));
f.write_str(left)?;
f.write_str("\r\n")?;
byte_writable = right;
if !byte_writable.is_empty() {
std::fmt::Write::write_char(f, '\t')?;
prev = 1;
}
}
Ok(())
}
}
impl std::fmt::Display for Mail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.headers)?;
if !matches!(self.body, BodyType::Mime(_)) {
f.write_str("\r\n")?;
}
write!(f, "{}", self.body)
}
}
impl Mail {
pub fn rewrite_mail_from(&mut self, value: &str) {
if let Some((_, old)) = self
.headers
.0
.iter_mut()
.find(|(header, _)| header.eq_ignore_ascii_case("from"))
{
*old = value.to_string();
} else {
self.headers.0.push(("From".to_string(), value.to_string()));
}
}
pub fn rewrite_rcpt(&mut self, old: &str, new: &str) {
if let Some((_, rcpts)) = self
.headers
.0
.iter_mut()
.find(|(header, _)| header.eq_ignore_ascii_case("to"))
{
*rcpts = rcpts.replace(old, new);
} else {
self.headers.0.push(("To".to_string(), new.to_string()));
}
}
pub fn add_rcpt(&mut self, new: &str) {
if let Some((_, rcpts)) = self
.headers
.0
.iter_mut()
.find(|(header, _)| header.eq_ignore_ascii_case("to"))
{
*rcpts = format!("{rcpts}, {new}");
} else {
self.headers.0.push(("To".to_string(), new.to_string()));
}
}
pub fn remove_rcpt(&mut self, old: &str) {
self.headers
.0
.iter_mut()
.find(|(header, _)| header.eq_ignore_ascii_case("to"))
.and_then::<(), _>(|(_, rcpts)| {
if rcpts.find(old) == Some(0) {
*rcpts = rcpts.replace(format!("{old}, ").as_str(), "");
} else {
*rcpts = rcpts.replace(format!(", {old}").as_str(), "");
}
None
});
}
pub fn set_header(&mut self, name: &str, value: &str) {
if let Some((_, old_value)) = self
.headers
.0
.iter_mut()
.find(|(header, _)| header.eq_ignore_ascii_case(name))
{
*old_value = value.to_string();
} else {
self.headers.0.push((name.to_string(), value.to_string()));
}
}
pub fn rename_header(&mut self, old: &str, new: &str) {
if let Some((old_name, _)) = self
.headers
.0
.iter_mut()
.find(|(header, _)| header.eq_ignore_ascii_case(old))
{
*old_name = new.to_string();
}
}
#[must_use]
pub fn get_header(&self, name: &str) -> Option<&str> {
self.headers
.0
.iter()
.find(|(header, _)| header.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
#[must_use]
pub fn get_header_rev(&self, name: &str) -> Option<&str> {
self.headers
.0
.iter()
.rev()
.find(|(header, _)| header.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
#[must_use]
pub fn count_header(&self, name: &str) -> usize {
self.headers
.0
.iter()
.filter(|(header, _)| header.eq_ignore_ascii_case(name))
.count()
}
pub fn prepend_headers(&mut self, headers: impl IntoIterator<Item = (String, String)>) {
self.headers.0.splice(..0, headers);
}
pub fn push_headers(&mut self, headers: impl IntoIterator<Item = (String, String)>) {
self.headers.0.extend(headers);
}
pub fn remove_header(&mut self, name: &str) -> bool {
if let Some(index) = self
.headers
.0
.iter()
.position(|header| header.0.eq_ignore_ascii_case(name))
{
self.headers.0.remove(index);
true
} else {
false
}
}
}
#[cfg(test)]
mod test {
use crate::message::mime_type::{MimeBodyType, MimeHeader};
use super::*;
#[test]
fn test_construct_mail() {
let empty_mail = Mail {
headers: MailHeaders(vec![("From".to_string(), "a@a".to_string())]),
body: BodyType::Undefined,
};
assert_eq!(format!("{empty_mail}"), "From: a@a\r\n\r\n".to_string());
let regular_mail = Mail {
headers: MailHeaders(vec![("From".to_string(), "a@a".to_string())]),
body: BodyType::Regular(vec!["This is a regular body.".to_string()]),
};
assert_eq!(
format!("{regular_mail}"),
"From: a@a\r\n\r\nThis is a regular body.\r\n".to_string()
);
let mime_mail = Mail {
headers: MailHeaders(vec![
("From".to_string(), "a@a".to_string()),
("Mime-Version".to_string(), "1.0".to_string()),
]),
body: BodyType::Mime(Box::new(Mime {
headers: vec![MimeHeader {
name: "content-type".to_string(),
value: "text/plain".to_string(),
args: std::collections::HashMap::new(),
}],
content: MimeBodyType::Regular(vec!["this is a regular mime body.".to_string()]),
})),
};
assert_eq!(
format!("{mime_mail}"),
[
"From: a@a\r\n",
"MIME-Version: 1.0\r\n",
"Content-Type: text/plain\r\n",
"\r\n",
"this is a regular mime body.\r\n",
]
.concat()
);
}
#[test]
fn test_append_headers() {
let mut mail = Mail {
body: BodyType::Regular(vec!["email content".to_string()]),
..Mail::default()
};
mail.push_headers(vec![
("Subject".to_string(), "testing an email".to_string()),
("MIME-Version".to_string(), "1.0".to_string()),
]);
assert_eq!(
format!("{mail}"),
[
"Subject: testing an email\r\n",
"MIME-Version: 1.0\r\n",
"\r\n",
"email content\r\n"
]
.concat()
);
mail.prepend_headers(vec![
("From".to_string(), "b@b".to_string()),
(
"Date".to_string(),
"tue, 30 nov 2021 20:54:27 +0100".to_string(),
),
("To".to_string(), "john@doe.com, green@foo.bar".to_string()),
]);
assert_eq!(
format!("{mail}"),
[
"From: b@b\r\n",
"Date: tue, 30 nov 2021 20:54:27 +0100\r\n",
"To: john@doe.com, green@foo.bar\r\n",
"Subject: testing an email\r\n",
"MIME-Version: 1.0\r\n",
"\r\n",
"email content\r\n"
]
.concat()
);
}
#[test]
fn test_rcpt_mutation() {
let mut mail = Mail::default();
mail.rewrite_mail_from("a@a");
assert_eq!(
mail.headers,
MailHeaders(vec![("From".to_string(), "a@a".to_string())])
);
mail.rewrite_mail_from("b@b");
assert_eq!(
mail.headers,
MailHeaders(vec![("From".to_string(), "b@b".to_string())])
);
mail.rewrite_rcpt("b@b", "a@a");
assert_eq!(
mail.headers,
MailHeaders(vec![
("From".to_string(), "b@b".to_string()),
("To".to_string(), "a@a".to_string())
])
);
mail.add_rcpt("green@foo.bar");
assert_eq!(
mail.headers,
MailHeaders(vec![
("From".to_string(), "b@b".to_string()),
("To".to_string(), "a@a, green@foo.bar".to_string())
])
);
mail.rewrite_rcpt("a@a", "john@doe");
assert_eq!(
mail.headers,
MailHeaders(vec![
("From".to_string(), "b@b".to_string()),
("To".to_string(), "john@doe, green@foo.bar".to_string())
])
);
mail.remove_rcpt("john@doe");
assert_eq!(
mail.headers,
MailHeaders(vec![
("From".to_string(), "b@b".to_string()),
("To".to_string(), "green@foo.bar".to_string())
])
);
}
}