351 lines
9.6 KiB
Rust
351 lines
9.6 KiB
Rust
use nom::{
|
|
bytes::complete::tag,
|
|
combinator::{map, opt},
|
|
multi::many0,
|
|
sequence::{preceded, terminated, tuple},
|
|
IResult,
|
|
};
|
|
|
|
use crate::mime::charset::EmailCharset;
|
|
use crate::text::misc_token::{mime_word, MIMEWord};
|
|
use crate::text::words::mime_atom;
|
|
use crate::mime::{AnyMIME, MIME, NaiveMIME};
|
|
|
|
// --------- NAIVE TYPE
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub struct NaiveType<'a> {
|
|
pub main: &'a [u8],
|
|
pub sub: &'a [u8],
|
|
pub params: Vec<Parameter<'a>>,
|
|
}
|
|
impl<'a> NaiveType<'a> {
|
|
pub fn to_type(&self) -> AnyType {
|
|
self.into()
|
|
}
|
|
}
|
|
pub fn naive_type(input: &[u8]) -> IResult<&[u8], NaiveType> {
|
|
map(
|
|
tuple((mime_atom, tag("/"), mime_atom, parameter_list)),
|
|
|(main, _, sub, params)| NaiveType { main, sub, params },
|
|
)(input)
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub struct Parameter<'a> {
|
|
pub name: &'a [u8],
|
|
pub value: MIMEWord<'a>,
|
|
}
|
|
pub fn parameter(input: &[u8]) -> IResult<&[u8], Parameter> {
|
|
map(
|
|
tuple((mime_atom, tag(b"="), mime_word)),
|
|
|(name, _, value)| Parameter { name, value },
|
|
)(input)
|
|
}
|
|
pub fn parameter_list(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
|
|
terminated(many0(preceded(tag(";"), parameter)), opt(tag(";")))(input)
|
|
}
|
|
|
|
// MIME TYPES TRANSLATED TO RUST TYPING SYSTEM
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum AnyType {
|
|
// Composite types
|
|
Multipart(Multipart),
|
|
Message(Deductible<Message>),
|
|
|
|
// Discrete types
|
|
Text(Deductible<Text>),
|
|
Binary(Binary),
|
|
}
|
|
|
|
impl<'a> From<&'a NaiveType<'a>> for AnyType {
|
|
fn from(nt: &'a NaiveType<'a>) -> Self {
|
|
match nt.main.to_ascii_lowercase().as_slice() {
|
|
b"multipart" => Multipart::try_from(nt)
|
|
.map(Self::Multipart)
|
|
.unwrap_or(Self::Text(DeductibleText::default())),
|
|
b"message" => Self::Message(DeductibleMessage::Explicit(Message::from(nt))),
|
|
b"text" => Self::Text(DeductibleText::Explicit(Text::from(nt))),
|
|
_ => Self::Binary(Binary::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> AnyType {
|
|
pub fn to_mime(self, fields: NaiveMIME<'a>) -> AnyMIME<'a> {
|
|
match self {
|
|
Self::Multipart(interpreted_type) => AnyMIME::Mult(MIME::<Multipart> { interpreted_type, fields }),
|
|
Self::Message(interpreted_type) => AnyMIME::Msg(MIME::<DeductibleMessage> { interpreted_type, fields }),
|
|
Self::Text(interpreted_type) => AnyMIME::Txt(MIME::<DeductibleText> { interpreted_type, fields }),
|
|
Self::Binary(interpreted_type) => AnyMIME::Bin(MIME::<Binary> { interpreted_type, fields }),
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub enum Deductible<T: Default> {
|
|
Inferred(T),
|
|
Explicit(T),
|
|
}
|
|
impl<T: Default> Default for Deductible<T> {
|
|
fn default() -> Self {
|
|
Self::Inferred(T::default())
|
|
}
|
|
}
|
|
|
|
// REAL PARTS
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub struct Multipart {
|
|
pub subtype: MultipartSubtype,
|
|
pub boundary: String,
|
|
}
|
|
impl Multipart {
|
|
pub fn main_type(&self) -> String {
|
|
"multipart".into()
|
|
}
|
|
}
|
|
impl<'a> TryFrom<&'a NaiveType<'a>> for Multipart {
|
|
type Error = ();
|
|
|
|
fn try_from(nt: &'a NaiveType<'a>) -> Result<Self, Self::Error> {
|
|
nt.params
|
|
.iter()
|
|
.find(|x| x.name.to_ascii_lowercase().as_slice() == b"boundary")
|
|
.map(|boundary| Multipart {
|
|
subtype: MultipartSubtype::from(nt),
|
|
boundary: boundary.value.to_string(),
|
|
})
|
|
.ok_or(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub enum MultipartSubtype {
|
|
Alternative,
|
|
Mixed,
|
|
Digest,
|
|
Parallel,
|
|
Report,
|
|
Unknown,
|
|
}
|
|
impl ToString for MultipartSubtype {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
Self::Alternative => "alternative",
|
|
Self::Mixed => "mixed",
|
|
Self::Digest => "digest",
|
|
Self::Parallel => "parallel",
|
|
Self::Report => "report",
|
|
Self::Unknown => "mixed",
|
|
}.into()
|
|
}
|
|
}
|
|
impl<'a> From<&NaiveType<'a>> for MultipartSubtype {
|
|
fn from(nt: &NaiveType<'a>) -> Self {
|
|
match nt.sub.to_ascii_lowercase().as_slice() {
|
|
b"alternative" => Self::Alternative,
|
|
b"mixed" => Self::Mixed,
|
|
b"digest" => Self::Digest,
|
|
b"parallel" => Self::Parallel,
|
|
b"report" => Self::Report,
|
|
_ => Self::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Default, Clone)]
|
|
pub enum MessageSubtype {
|
|
#[default]
|
|
RFC822,
|
|
Partial,
|
|
External,
|
|
Unknown,
|
|
}
|
|
impl ToString for MessageSubtype {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
Self::RFC822 => "rfc822",
|
|
Self::Partial => "partial",
|
|
Self::External => "external",
|
|
Self::Unknown => "rfc822",
|
|
}.into()
|
|
}
|
|
}
|
|
|
|
pub type DeductibleMessage = Deductible<Message>;
|
|
#[derive(Debug, PartialEq, Default, Clone)]
|
|
pub struct Message {
|
|
pub subtype: MessageSubtype,
|
|
}
|
|
impl<'a> From<&NaiveType<'a>> for Message {
|
|
fn from(nt: &NaiveType<'a>) -> Self {
|
|
match nt.sub.to_ascii_lowercase().as_slice() {
|
|
b"rfc822" => Self { subtype: MessageSubtype::RFC822 },
|
|
b"partial" => Self { subtype: MessageSubtype::Partial },
|
|
b"external" => Self { subtype: MessageSubtype::External },
|
|
_ => Self { subtype: MessageSubtype::Unknown },
|
|
}
|
|
}
|
|
}
|
|
impl From<Deductible<Message>> for Message {
|
|
fn from(d: Deductible<Message>) -> Self {
|
|
match d {
|
|
Deductible::Inferred(t) | Deductible::Explicit(t) => t
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type DeductibleText = Deductible<Text>;
|
|
#[derive(Debug, PartialEq, Default, Clone)]
|
|
pub struct Text {
|
|
pub subtype: TextSubtype,
|
|
pub charset: Deductible<EmailCharset>,
|
|
}
|
|
impl<'a> From<&NaiveType<'a>> for Text {
|
|
fn from(nt: &NaiveType<'a>) -> Self {
|
|
Self {
|
|
subtype: TextSubtype::from(nt),
|
|
charset: nt
|
|
.params
|
|
.iter()
|
|
.find(|x| x.name.to_ascii_lowercase().as_slice() == b"charset")
|
|
.map(|x| Deductible::Explicit(EmailCharset::from(x.value.to_string().as_bytes())))
|
|
.unwrap_or(Deductible::Inferred(EmailCharset::US_ASCII)),
|
|
}
|
|
}
|
|
}
|
|
impl From<Deductible<Text>> for Text {
|
|
fn from(d: Deductible<Text>) -> Self {
|
|
match d {
|
|
Deductible::Inferred(t) | Deductible::Explicit(t) => t
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Default, Clone)]
|
|
pub enum TextSubtype {
|
|
#[default]
|
|
Plain,
|
|
Html,
|
|
Unknown,
|
|
}
|
|
impl ToString for TextSubtype {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
Self::Plain | Self::Unknown => "plain",
|
|
Self::Html => "html",
|
|
}.into()
|
|
}
|
|
}
|
|
impl<'a> From<&NaiveType<'a>> for TextSubtype {
|
|
fn from(nt: &NaiveType<'a>) -> Self {
|
|
match nt.sub.to_ascii_lowercase().as_slice() {
|
|
b"plain" => Self::Plain,
|
|
b"html" => Self::Html,
|
|
_ => Self::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Default, Clone)]
|
|
pub struct Binary {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::mime::charset::EmailCharset;
|
|
use crate::text::quoted::QuotedString;
|
|
use crate::mime::r#type::Deductible;
|
|
|
|
#[test]
|
|
fn test_parameter() {
|
|
assert_eq!(
|
|
parameter(b"charset=utf-8"),
|
|
Ok((
|
|
&b""[..],
|
|
Parameter {
|
|
name: &b"charset"[..],
|
|
value: MIMEWord::Atom(&b"utf-8"[..]),
|
|
}
|
|
)),
|
|
);
|
|
assert_eq!(
|
|
parameter(b"charset=\"utf-8\""),
|
|
Ok((
|
|
&b""[..],
|
|
Parameter {
|
|
name: &b"charset"[..],
|
|
value: MIMEWord::Quoted(QuotedString(vec![&b"utf-8"[..]])),
|
|
}
|
|
)),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_type_plaintext() {
|
|
let (rest, nt) = naive_type(b"text/plain;\r\n charset=utf-8").unwrap();
|
|
assert_eq!(rest, &b""[..]);
|
|
|
|
assert_eq!(
|
|
nt.to_type(),
|
|
AnyType::Text(Deductible::Explicit(Text {
|
|
charset: Deductible::Explicit(EmailCharset::UTF_8),
|
|
subtype: TextSubtype::Plain,
|
|
}))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_type_multipart() {
|
|
let (rest, nt) = naive_type(b"multipart/mixed;\r\n\tboundary=\"--==_mimepart_64a3f2c69114f_2a13d020975fe\";\r\n\tcharset=UTF-8").unwrap();
|
|
assert_eq!(rest, &[]);
|
|
assert_eq!(
|
|
nt.to_type(),
|
|
AnyType::Multipart(Multipart {
|
|
subtype: MultipartSubtype::Mixed,
|
|
boundary: "--==_mimepart_64a3f2c69114f_2a13d020975fe".into(),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_type_message() {
|
|
let (rest, nt) = naive_type(b"message/rfc822").unwrap();
|
|
assert_eq!(rest, &[]);
|
|
|
|
assert_eq!(nt.to_type(), AnyType::Message(Deductible::Explicit(Message { subtype: MessageSubtype::RFC822 })));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parameter_ascii() {
|
|
assert_eq!(
|
|
parameter(b"charset = (simple) us-ascii (Plain text)"),
|
|
Ok((
|
|
&b""[..],
|
|
Parameter {
|
|
name: &b"charset"[..],
|
|
value: MIMEWord::Atom(&b"us-ascii"[..]),
|
|
}
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parameter_terminated_with_semi_colon() {
|
|
assert_eq!(
|
|
parameter_list(b";boundary=\"festivus\";"),
|
|
Ok((
|
|
&b""[..],
|
|
vec![Parameter {
|
|
name: &b"boundary"[..],
|
|
value: MIMEWord::Quoted(QuotedString(vec![&b"festivus"[..]])),
|
|
}],
|
|
))
|
|
);
|
|
}
|
|
}
|