use std::collections::HashMap;
use crate::message::body::{
compiler::tokens::{Part, Props},
GREATER_THAN, MULTIPART_BEGIN, MULTIPART_END,
};
use super::{
creation_date, data_encoding, description, disposition, encoding, filename, modification_date,
multipart_type, name, part_type, prelude::*, read_date, recipient_filename,
};
#[cfg(feature = "pgp")]
use super::{encrypt, sign};
pub(crate) fn parts<'a>() -> impl Parser<'a, &'a str, Vec<Part<'a>>, ParserError<'a>> + Clone {
choice((multipart(), part(), plain_text_part(1).map(Part::PlainText)))
.repeated()
.collect()
.then_ignore(end())
}
pub(crate) fn multipart<'a>() -> impl Parser<'a, &'a str, Part<'a>, ParserError<'a>> + Clone {
recursive(|multipart| {
just(MULTIPART_BEGIN)
.ignore_then(
choice((
multipart_type(),
description(),
#[cfg(feature = "pgp")]
encrypt(),
#[cfg(feature = "pgp")]
sign(),
))
.repeated()
.collect::<Props>(),
)
.then_ignore(just(GREATER_THAN))
.then_ignore(new_line().or_not())
.then(
choice((multipart, part(), plain_text_part(1).map(Part::PlainText)))
.repeated()
.collect(),
)
.then_ignore(just(MULTIPART_END))
.then_ignore(new_line().or_not())
.map(|(props, parts)| Part::Multi(props, parts))
})
}
pub(crate) fn part<'a>() -> impl Parser<'a, &'a str, Part<'a>, ParserError<'a>> + Clone {
part_begin()
.ignore_then(
choice((
part_type(),
filename(),
recipient_filename(),
name(),
encoding(),
data_encoding(),
creation_date(),
modification_date(),
read_date(),
description(),
disposition(),
#[cfg(feature = "pgp")]
encrypt(),
#[cfg(feature = "pgp")]
sign(),
))
.repeated()
.collect::<HashMap<_, _>>()
.then_ignore(just(GREATER_THAN))
.then_ignore(new_line().or_not()),
)
.then(plain_text_part(0))
.then_ignore(part_end().then(new_line().or_not()).or_not())
.map(|(props, content)| Part::Single(props, content))
}
pub(crate) fn plain_text_part<'a>(
min: usize,
) -> impl Parser<'a, &'a str, &'a str, ParserError<'a>> + Clone {
any()
.and_is(choice((part_begin(), part_end(), multipart_begin(), multipart_end())).not())
.repeated()
.at_least(min)
.to_slice()
}
#[cfg(test)]
mod tests {
use concat_with::concat_line;
use std::collections::HashMap;
use crate::message::body::{
compiler::{parsers::prelude::*, tokens::Part},
DISPOSITION, FILENAME, NAME, TYPE,
};
#[test]
fn single_part_no_new_line() {
assert_eq!(
super::part()
.parse("<#part>This is a plain text part.")
.into_result(),
Ok(Part::Single(
HashMap::default(),
"This is a plain text part."
)),
);
assert_eq!(
super::part()
.parse("<#part>This is a plain text part.<#/part>")
.into_result(),
Ok(Part::Single(
HashMap::default(),
"This is a plain text part."
)),
);
}
#[test]
fn single_part_new_line() {
assert_eq!(
super::part()
.parse(concat_line!("<#part>", "This is a plain text part."))
.into_result(),
Ok(Part::Single(
HashMap::default(),
"This is a plain text part."
)),
);
assert_eq!(
super::part()
.parse(concat_line!(
"<#part>",
"This is a plain text part.",
"",
"<#/part>",
))
.into_result(),
Ok(Part::Single(
HashMap::default(),
"This is a plain text part.\n\n"
)),
);
}
#[test]
fn single_html_part() {
assert_eq!(
super::part()
.parse(concat_line!(
"<#part type=text/html>",
"<h1>This is a HTML text part.</h1>",
"<#/part>"
))
.into_result(),
Ok(Part::Single(
HashMap::from_iter([(TYPE, "text/html")]),
"<h1>This is a HTML text part.</h1>\n",
)),
);
}
#[test]
fn attachment() {
assert_eq!(
super::part()
.parse("<#part type=image/jpeg filename=~/rms.jpg disposition=inline><#/part>")
.into_result(),
Ok(Part::Single(
HashMap::from_iter([
(TYPE, "image/jpeg"),
(FILENAME, "~/rms.jpg"),
(DISPOSITION, "inline")
]),
""
)),
);
}
#[test]
fn multi_part() {
assert_eq!(
super::multipart()
.parse(concat_line!(
"<#multipart>",
"This is a plain text part.",
"<#/multipart>"
))
.into_result(),
Ok(Part::Multi(
HashMap::default(),
vec![Part::PlainText("This is a plain text part.\n")]
)),
);
}
#[test]
fn nested_multi_part() {
assert_eq!(
super::multipart()
.parse(concat_line!(
"<#multipart>",
"<#multipart>",
"This is a plain text part.",
"<#/multipart>",
"<#/multipart>"
))
.into_result(),
Ok(Part::Multi(
HashMap::default(),
vec![Part::Multi(
HashMap::default(),
vec![Part::PlainText("This is a plain text part.\n")]
)]
)),
);
assert_eq!(
super::multipart()
.parse(concat_line!(
"<#multipart>",
"<#multipart>",
"<#multipart>",
"<#multipart>",
"This is a plain text part.",
"<#/multipart>",
"<#/multipart>",
"<#/multipart>",
"<#/multipart>"
))
.into_result(),
Ok(Part::Multi(
HashMap::default(),
vec![Part::Multi(
HashMap::default(),
vec![Part::Multi(
HashMap::default(),
vec![Part::Multi(
HashMap::default(),
vec![Part::PlainText("This is a plain text part.\n")]
)]
)]
)]
)),
);
}
#[test]
fn adjacent_multi_part() {
assert_eq!(
super::multipart()
.parse(concat_line!(
"<#multipart>",
"<#multipart>",
"This is a plain text part.",
"<#/multipart>",
"<#multipart>",
"This is a new plain text part.",
"<#/multipart>",
"<#/multipart>"
))
.into_result(),
Ok(Part::Multi(
HashMap::default(),
vec![
Part::Multi(
HashMap::default(),
vec![Part::PlainText("This is a plain text part.\n")]
),
Part::Multi(
HashMap::default(),
vec![Part::PlainText("This is a new plain text part.\n")]
)
]
)),
);
}
#[test]
fn simple_mml() {
assert_eq!(
super::parts()
.parse(concat_line!(
"<#multipart type=alternative>",
"This is a plain text part.",
"<#part type=text/enriched>",
"<center>This is a centered enriched part</center>",
"<#/multipart>",
))
.into_result(),
Ok(vec![Part::Multi(
HashMap::from_iter([(TYPE, "alternative")]),
vec![
Part::PlainText("This is a plain text part.\n"),
Part::Single(
HashMap::from_iter([(TYPE, "text/enriched")]),
"<center>This is a centered enriched part</center>\n"
)
]
)]),
);
}
#[test]
fn advanced_mml() {
assert_eq!(
super::parts()
.parse(concat_line!(
"<#multipart type=mixed>",
"<#part type=image/jpeg filename=~/rms.jpg disposition=inline>",
"<#multipart type=alternative>",
"This is a plain text part.",
"<#part type=text/enriched name=enriched.txt>",
"<center>This is a centered enriched part</center>",
"<#/multipart>",
"This is a new plain text part.",
"<#part disposition=attachment>",
"This plain text part is an attachment.",
"<#/multipart>",
))
.unwrap(),
vec![Part::Multi(
HashMap::from_iter([(TYPE, "mixed")]),
vec![
Part::Single(
HashMap::from_iter([
(TYPE, "image/jpeg"),
(FILENAME, "~/rms.jpg"),
(DISPOSITION, "inline")
]),
""
),
Part::Multi(
HashMap::from_iter([(TYPE, "alternative")]),
vec![
Part::PlainText("This is a plain text part.\n"),
Part::Single(
HashMap::from_iter([
(TYPE, "text/enriched"),
(NAME, "enriched.txt")
]),
"<center>This is a centered enriched part</center>\n",
)
]
),
Part::PlainText("This is a new plain text part.\n"),
Part::Single(
HashMap::from_iter([(DISPOSITION, "attachment")]),
"This plain text part is an attachment.\n",
)
]
)]
);
}
}