Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

21 changed files with 441 additions and 849 deletions

2
Cargo.lock generated
View file

@ -70,7 +70,7 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]] [[package]]
name = "eml-codec" name = "eml-codec"
version = "0.1.2" version = "0.1.0"
dependencies = [ dependencies = [
"base64", "base64",
"chrono", "chrono",

View file

@ -1,16 +1,9 @@
[package] [package]
name = "eml-codec" name = "eml-codec"
version = "0.1.2" version = "0.1.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec"
description = "Email enCOder DECoder in Rust. Support Internet Message Format and MIME (RFC 822, 5322, 2045, 2046, 2047, 2048, 2049)."
documentation = "https://docs.rs/eml-codec"
readme = "README.md"
exclude = [
"doc/",
"resources/",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]
@ -30,4 +23,3 @@ encoding_rs = "0.8"
[dev-dependencies] [dev-dependencies]
walkdir = "2" walkdir = "2"

View file

@ -18,7 +18,7 @@ Content-Type: text/plain; charset=us-ascii
This is the plain text body of the message. Note the blank line This is the plain text body of the message. Note the blank line
between the header information and the body of the message."#; between the header information and the body of the message."#;
let (_, email) = eml_codec::parse_message(input).unwrap(); let (_, email) = eml_codec::email(input).unwrap();
println!( println!(
"{} just sent you an email with subject \"{}\"", "{} just sent you an email with subject \"{}\"",
email.imf.from[0].to_string(), email.imf.from[0].to_string(),

View file

@ -10,7 +10,7 @@ This is the plain text body of the message. Note the blank line
between the header information and the body of the message."#; between the header information and the body of the message."#;
// if you are only interested in email metadata/headers // if you are only interested in email metadata/headers
let (_, imf) = eml_codec::parse_imf(input).unwrap(); let (_, imf) = eml_codec::imf(input).unwrap();
println!( println!(
"{} just sent you an email with subject \"{}\"", "{} just sent you an email with subject \"{}\"",
imf.from[0].to_string(), imf.from[0].to_string(),
@ -18,7 +18,7 @@ between the header information and the body of the message."#;
); );
// if you like to also parse the body/content // if you like to also parse the body/content
let (_, email) = eml_codec::parse_message(input).unwrap(); let (_, email) = eml_codec::email(input).unwrap();
println!( println!(
"{} raw message is:\n{}", "{} raw message is:\n{}",
email.imf.from[0].to_string(), email.imf.from[0].to_string(),

View file

@ -1,62 +1,55 @@
use crate::text::misc_token::{unstructured, Unstructured};
use crate::text::whitespace::{foldable_line, obs_crlf};
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::complete::{tag, take_while1}, bytes::complete::{tag, tag_no_case, take_while1},
character::complete::space0, character::complete::space0,
combinator::{into, recognize}, combinator::map,
multi::many0, multi::{fold_many0},
sequence::{pair, terminated, tuple}, sequence::{pair, terminated, tuple},
IResult, IResult,
}; };
use std::fmt;
use crate::text::misc_token::unstructured; #[derive(Debug, PartialEq)]
use crate::text::whitespace::{foldable_line, obs_crlf}; pub enum CompField<'a, T> {
Known(T),
#[derive(PartialEq, Clone)] Unknown(Kv<'a>),
pub struct Kv2<'a>(pub &'a [u8], pub &'a [u8]); Bad(&'a [u8]),
impl<'a> From<(&'a [u8], &'a [u8])> for Kv2<'a> {
fn from(pair: (&'a [u8], &'a [u8])) -> Self {
Self(pair.0, pair.1)
}
}
impl<'a> fmt::Debug for Kv2<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_tuple("header::Kv2")
.field(&String::from_utf8_lossy(self.0))
.field(&String::from_utf8_lossy(self.1))
.finish()
}
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum Field<'a> { pub struct Kv<'a>(pub &'a [u8], pub Unstructured<'a>);
Good(Kv2<'a>),
Bad(&'a [u8]),
}
impl<'a> From<Kv2<'a>> for Field<'a> {
fn from(kv: Kv2<'a>) -> Self {
Self::Good(kv)
}
}
impl<'a> From<&'a [u8]> for Field<'a> {
fn from(bad: &'a [u8]) -> Self {
Self::Bad(bad)
}
}
/// Parse headers as key/values
pub fn header_kv(input: &[u8]) -> IResult<&[u8], Vec<Field>> { pub fn header<'a, T>(
fx: impl Fn(&'a [u8]) -> IResult<&'a [u8], T> + Copy,
) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], (Vec::<T>, Vec::<Kv>, Vec<&'a [u8]>)> {
move |input| {
terminated( terminated(
many0(alt((into(correct_field), into(foldable_line)))), fold_many0(
alt((
map(fx, CompField::Known),
map(opt_field, |(k, v)| CompField::Unknown(Kv(k, v))),
map(foldable_line, CompField::Bad),
)),
|| (Vec::<T>::new(), Vec::<Kv>::new(), Vec::<&'a [u8]>::new()),
|(mut known, mut unknown, mut bad), item| {
match item {
CompField::Known(v) => known.push(v),
CompField::Unknown(v) => unknown.push(v),
CompField::Bad(v) => bad.push(v),
};
(known, unknown, bad)
}
),
obs_crlf, obs_crlf,
)(input) )(input)
} }
}
pub fn field_any(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn field_name<'a>(name: &'static [u8]) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], &'a [u8]> {
terminated( move |input| terminated(tag_no_case(name), tuple((space0, tag(b":"), space0)))(input)
take_while1(|c| (0x21..=0x7E).contains(&c) && c != 0x3A),
tuple((space0, tag(b":"), space0)),
)(input)
} }
/// Optional field /// Optional field
@ -68,6 +61,15 @@ pub fn field_any(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// %d59-126 ; characters not including /// %d59-126 ; characters not including
/// ; ":". /// ; ":".
/// ``` /// ```
pub fn correct_field(input: &[u8]) -> IResult<&[u8], Kv2> { pub fn opt_field(input: &[u8]) -> IResult<&[u8], (&[u8], Unstructured)> {
terminated(into(pair(field_any, recognize(unstructured))), obs_crlf)(input) terminated(
pair(
terminated(
take_while1(|c| (0x21..=0x7E).contains(&c) && c != 0x3A),
tuple((space0, tag(b":"), space0)),
),
unstructured,
),
obs_crlf,
)(input)
} }

View file

@ -1,14 +1,21 @@
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use nom::combinator::map; use nom::{
branch::alt,
combinator::map,
sequence::{preceded, terminated},
IResult,
};
use crate::header; use crate::header::{field_name, header};
use crate::imf::address::{address_list, mailbox_list, nullable_address_list, AddressList}; use crate::imf::address::{address_list, mailbox_list, nullable_address_list, AddressList};
use crate::imf::datetime::section as date; use crate::imf::datetime::section as date;
use crate::imf::identification::{msg_id, msg_list, MessageID, MessageIDList}; use crate::imf::identification::{msg_id, msg_list, MessageID, MessageIDList};
use crate::imf::mailbox::{mailbox, AddrSpec, MailboxList, MailboxRef}; use crate::imf::mailbox::{mailbox, AddrSpec, MailboxList, MailboxRef};
use crate::imf::mime::{version, Version}; use crate::imf::mime::{version, Version};
use crate::imf::trace::{received_log, return_path, ReceivedLog}; use crate::imf::trace::{received_log, return_path, ReceivedLog};
use crate::imf::Imf;
use crate::text::misc_token::{phrase_list, unstructured, PhraseList, Unstructured}; use crate::text::misc_token::{phrase_list, unstructured, PhraseList, Unstructured};
use crate::text::whitespace::obs_crlf;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Field<'a> { pub enum Field<'a> {
@ -42,34 +49,94 @@ pub enum Field<'a> {
MIMEVersion(Version), MIMEVersion(Version),
} }
impl<'a> TryFrom<&header::Field<'a>> for Field<'a> {
type Error = ();
fn try_from(f: &header::Field<'a>) -> Result<Self, Self::Error> {
let content = match f {
header::Field::Good(header::Kv2(key, value)) => {
match key.to_ascii_lowercase().as_slice() {
b"date" => map(date, Field::Date)(value),
b"from" => map(mailbox_list, Field::From)(value),
b"sender" => map(mailbox, Field::Sender)(value),
b"reply-to" => map(address_list, Field::ReplyTo)(value),
b"to" => map(address_list, Field::To)(value),
b"cc" => map(address_list, Field::Cc)(value),
b"bcc" => map(nullable_address_list, Field::Bcc)(value),
b"message-id" => map(msg_id, Field::MessageID)(value),
b"in-reply-to" => map(msg_list, Field::InReplyTo)(value),
b"references" => map(msg_list, Field::References)(value),
b"subject" => map(unstructured, Field::Subject)(value),
b"comments" => map(unstructured, Field::Comments)(value),
b"keywords" => map(phrase_list, Field::Keywords)(value),
b"return-path" => map(return_path, Field::ReturnPath)(value),
b"received" => map(received_log, Field::Received)(value),
b"mime-version" => map(version, Field::MIMEVersion)(value),
_ => return Err(()),
}
}
_ => return Err(()),
};
content.map(|(_, content)| content).or(Err(())) pub fn field(input: &[u8]) -> IResult<&[u8], Field> {
terminated(
alt((
preceded(field_name(b"date"), map(date, Field::Date)),
preceded(field_name(b"from"), map(mailbox_list, Field::From)),
preceded(field_name(b"sender"), map(mailbox, Field::Sender)),
preceded(field_name(b"reply-to"), map(address_list, Field::ReplyTo)),
preceded(field_name(b"to"), map(address_list, Field::To)),
preceded(field_name(b"cc"), map(address_list, Field::Cc)),
preceded(field_name(b"bcc"), map(nullable_address_list, Field::Bcc)),
preceded(field_name(b"message-id"), map(msg_id, Field::MessageID)),
preceded(field_name(b"in-reply-to"), map(msg_list, Field::InReplyTo)),
preceded(field_name(b"references"), map(msg_list, Field::References)),
preceded(field_name(b"subject"), map(unstructured, Field::Subject)),
preceded(field_name(b"comments"), map(unstructured, Field::Comments)),
preceded(field_name(b"keywords"), map(phrase_list, Field::Keywords)),
preceded(
field_name(b"return-path"),
map(return_path, Field::ReturnPath),
),
preceded(field_name(b"received"), map(received_log, Field::Received)),
preceded(
field_name(b"mime-version"),
map(version, Field::MIMEVersion),
),
)),
obs_crlf,
)(input)
}
pub fn imf(input: &[u8]) -> IResult<&[u8], Imf> {
map(header(field), |(known, unknown, bad)| {
let mut imf = Imf::from_iter(known);
imf.header_ext = unknown;
imf.header_bad = bad;
imf
})(input)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::imf::address::*;
use crate::imf::mailbox::*;
use crate::text::misc_token::*;
use chrono::{FixedOffset, TimeZone};
#[test]
fn test_header() {
let fullmail = b"Date: 7 Mar 2023 08:00:00 +0200
From: someone@example.com
To: someone_else@example.com
Subject: An RFC 822 formatted message
This is the plain text body of the message. Note the blank line
between the header information and the body of the message.";
assert_eq!(
imf(fullmail),
Ok((
&b"This is the plain text body of the message. Note the blank line\nbetween the header information and the body of the message."[..],
Imf {
date: Some(FixedOffset::east_opt(2 * 3600).unwrap().with_ymd_and_hms(2023, 3, 7, 8, 0, 0).unwrap()),
from: vec![MailboxRef {
name: None,
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"someone"[..]))]),
domain: Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
}
}],
to: vec![AddressRef::Single(MailboxRef {
name: None,
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"someone_else"[..]))]),
domain: Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
}
})],
subject: Some(Unstructured(vec![
UnstrToken::Plain(&b"An"[..]),
UnstrToken::Plain(&b"RFC"[..]),
UnstrToken::Plain(&b"822"[..]),
UnstrToken::Plain(&b"formatted"[..]),
UnstrToken::Plain(&b"message"[..]),
])),
..Imf::default()
}
)),
)
} }
} }

View file

@ -1,4 +1,5 @@
/// Parse and represent IMF (Internet Message Format) headers (RFC822, RFC5322) /// Parse and represent IMF (Internet Message Format) headers (RFC822, RFC5322)
pub mod address; pub mod address;
pub mod datetime; pub mod datetime;
pub mod field; pub mod field;
@ -7,15 +8,13 @@ pub mod mailbox;
pub mod mime; pub mod mime;
pub mod trace; pub mod trace;
use nom::{combinator::map, IResult};
use crate::header;
use crate::imf::address::AddressRef; use crate::imf::address::AddressRef;
use crate::imf::field::Field; use crate::imf::field::Field;
use crate::imf::identification::MessageID; use crate::imf::identification::MessageID;
use crate::imf::mailbox::{AddrSpec, MailboxRef}; use crate::imf::mailbox::{AddrSpec, MailboxRef};
use crate::imf::mime::Version; use crate::imf::mime::Version;
use crate::imf::trace::ReceivedLog; use crate::imf::trace::ReceivedLog;
use crate::header;
use crate::text::misc_token::{PhraseList, Unstructured}; use crate::text::misc_token::{PhraseList, Unstructured};
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
@ -51,6 +50,19 @@ pub struct Imf<'a> {
// MIME // MIME
pub mime_version: Option<Version>, pub mime_version: Option<Version>,
// Junk
pub header_ext: Vec<header::Kv<'a>>,
pub header_bad: Vec<&'a [u8]>,
}
impl<'a> Imf<'a> {
pub fn with_opt(mut self, opt: Vec<header::Kv<'a>>) -> Self {
self.header_ext = opt; self
}
pub fn with_bad(mut self, bad: Vec<&'a [u8]>) -> Self {
self.header_bad = bad; self
}
} }
//@FIXME min and max limits are not enforced, //@FIXME min and max limits are not enforced,
@ -80,65 +92,3 @@ impl<'a> FromIterator<Field<'a>> for Imf<'a> {
}) })
} }
} }
pub fn imf(input: &[u8]) -> IResult<&[u8], Imf> {
map(header::header_kv, |fields| {
fields
.iter()
.flat_map(Field::try_from)
.into_iter()
.collect::<Imf>()
})(input)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::imf::address::*;
use crate::imf::mailbox::*;
use crate::text::misc_token::*;
use chrono::{FixedOffset, TimeZone};
#[test]
fn test_header() {
let fullmail = b"Date: 7 Mar 2023 08:00:00 +0200
From: someone@example.com
To: someone_else@example.com
Subject: An RFC 822 formatted message
This is the plain text body of the message. Note the blank line
between the header information and the body of the message.";
assert_eq!(
imf(fullmail),
Ok((
&b"This is the plain text body of the message. Note the blank line\nbetween the header information and the body of the message."[..],
Imf {
date: Some(FixedOffset::east_opt(2 * 3600).unwrap().with_ymd_and_hms(2023, 3, 7, 8, 0, 0).unwrap()),
from: vec![MailboxRef {
name: None,
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"someone"[..]))]),
domain: Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
}
}],
to: vec![AddressRef::Single(MailboxRef {
name: None,
addrspec: AddrSpec {
local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"someone_else"[..]))]),
domain: Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
}
})],
subject: Some(Unstructured(vec![
UnstrToken::Plain(&b"An"[..]),
UnstrToken::Plain(&b"RFC"[..]),
UnstrToken::Plain(&b"822"[..]),
UnstrToken::Plain(&b"formatted"[..]),
UnstrToken::Plain(&b"message"[..]),
])),
..Imf::default()
}
)),
)
}
}

View file

@ -15,10 +15,7 @@ pub mod header;
/// Low-level email-specific text-based representation for data /// Low-level email-specific text-based representation for data
pub mod text; pub mod text;
/// Manipulate buffer of bytes use nom::IResult;
mod pointers;
use nom::{combinator::into, IResult};
/// Parse a whole email including its (MIME) body /// Parse a whole email including its (MIME) body
/// ///
@ -49,17 +46,15 @@ use nom::{combinator::into, IResult};
/// This is the plain text body of the message. Note the blank line /// This is the plain text body of the message. Note the blank line
/// between the header information and the body of the message."#; /// between the header information and the body of the message."#;
/// ///
/// let (_, email) = eml_codec::parse_message(input).unwrap(); /// let (_, email) = eml_codec::email(input).unwrap();
/// println!( /// println!(
/// "{} raw message is:\n{}", /// "{} raw message is:\n{}",
/// email.imf.from[0].to_string(), /// email.imf.from[0].to_string(),
/// String::from_utf8_lossy(email.child.as_text().unwrap().body), /// String::from_utf8_lossy(email.child.as_text().unwrap().body),
/// ); /// );
/// ``` /// ```
pub fn parse_message(input: &[u8]) -> IResult<&[u8], part::composite::Message> { pub fn email(input: &[u8]) -> IResult<&[u8], part::composite::Message> {
into(part::composite::message(mime::MIME::< part::composite::message(mime::MIME::<mime::r#type::Message>::default())(input)
mime::r#type::DeductibleMessage,
>::default()))(input)
} }
/// Only extract the headers of the email that are part of the Internet Message Format spec /// Only extract the headers of the email that are part of the Internet Message Format spec
@ -92,13 +87,13 @@ pub fn parse_message(input: &[u8]) -> IResult<&[u8], part::composite::Message> {
/// This is the plain text body of the message. Note the blank line /// This is the plain text body of the message. Note the blank line
/// between the header information and the body of the message."#; /// between the header information and the body of the message."#;
/// ///
/// let (_, imf) = eml_codec::parse_imf(input).unwrap(); /// let (_, imf) = eml_codec::imf(input).unwrap();
/// println!( /// println!(
/// "{} just sent you an email with subject \"{}\"", /// "{} just sent you an email with subject \"{}\"",
/// imf.from[0].to_string(), /// imf.from[0].to_string(),
/// imf.subject.unwrap().to_string(), /// imf.subject.unwrap().to_string(),
/// ); /// );
/// ``` /// ```
pub fn parse_imf(input: &[u8]) -> IResult<&[u8], imf::Imf> { pub fn imf(input: &[u8]) -> IResult<&[u8], imf::Imf> {
imf::imf(input) imf::field::imf(input)
} }

View file

@ -77,12 +77,6 @@ impl<'a> From<&'a [u8]> for EmailCharset {
} }
} }
impl ToString for EmailCharset {
fn to_string(&self) -> String {
self.as_str().into()
}
}
impl EmailCharset { impl EmailCharset {
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
use EmailCharset::*; use EmailCharset::*;

View file

@ -1,10 +1,16 @@
use nom::combinator::map; use nom::{
branch::alt,
combinator::map,
sequence::{preceded, terminated},
IResult,
};
use crate::header; use crate::header::{field_name};
use crate::imf::identification::{msg_id, MessageID}; use crate::imf::identification::{msg_id, MessageID};
use crate::mime::mechanism::{mechanism, Mechanism}; use crate::mime::mechanism::{mechanism, Mechanism};
use crate::mime::r#type::{naive_type, NaiveType}; use crate::mime::r#type::{naive_type, NaiveType};
use crate::text::misc_token::{unstructured, Unstructured}; use crate::text::misc_token::{unstructured, Unstructured};
use crate::text::whitespace::obs_crlf;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Content<'a> { pub enum Content<'a> {
@ -41,38 +47,38 @@ impl<'a> Content<'a> {
} }
} }
impl<'a> TryFrom<&header::Field<'a>> for Content<'a> { /*
type Error = (); pub fn to_mime<'a, T: WithDefaultType>(list: Vec<Content<'a>>) -> AnyMIMEWithDefault<'a, T> {
fn try_from(f: &header::Field<'a>) -> Result<Self, Self::Error> { list.into_iter().collect::<AnyMIMEWithDefault<T>>()
let content = match f { }*/
header::Field::Good(header::Kv2(key, value)) => match key
.to_ascii_lowercase()
.as_slice()
{
b"content-type" => map(naive_type, Content::Type)(value),
b"content-transfer-encoding" => map(mechanism, Content::TransferEncoding)(value),
b"content-id" => map(msg_id, Content::ID)(value),
b"content-description" => map(unstructured, Content::Description)(value),
_ => return Err(()),
},
_ => return Err(()),
};
//@TODO check that the full value is parsed, otherwise maybe log an error ?! pub fn content(input: &[u8]) -> IResult<&[u8], Content> {
content.map(|(_, content)| content).or(Err(())) terminated(
} alt((
preceded(field_name(b"content-type"), map(naive_type, Content::Type)),
preceded(
field_name(b"content-transfer-encoding"),
map(mechanism, Content::TransferEncoding),
),
preceded(field_name(b"content-id"), map(msg_id, Content::ID)),
preceded(
field_name(b"content-description"),
map(unstructured, Content::Description),
),
)),
obs_crlf,
)(input)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::header; use crate::header::{header};
//use crate::mime::charset::EmailCharset; use crate::mime::charset::EmailCharset;
use crate::mime::r#type::*; use crate::mime::r#type::*;
use crate::text::misc_token::MIMEWord; use crate::text::misc_token::MIMEWord;
use crate::text::quoted::QuotedString; use crate::text::quoted::QuotedString;
/*
#[test] #[test]
fn test_content_type() { fn test_content_type() {
let (rest, content) = let (rest, content) =
@ -82,15 +88,15 @@ mod tests {
if let Content::Type(nt) = content { if let Content::Type(nt) = content {
assert_eq!( assert_eq!(
nt.to_type(), nt.to_type(),
AnyType::Text(Deductible::Explicit(Text { AnyType::Text(Text {
charset: Deductible::Explicit(EmailCharset::UTF_8), charset: EmailCharset::UTF_8,
subtype: TextSubtype::Plain, subtype: TextSubtype::Plain,
})), }),
); );
} else { } else {
panic!("Expected Content::Type, got {:?}", content); panic!("Expected Content::Type, got {:?}", content);
} }
}*/ }
#[test] #[test]
fn test_header() { fn test_header() {
@ -110,10 +116,7 @@ This is a multipart message.
.as_bytes(); .as_bytes();
assert_eq!( assert_eq!(
map(header::header_kv, |k| k map(header(content), |(k, _, _)| k)(fullmail),
.iter()
.flat_map(Content::try_from)
.collect())(fullmail),
Ok(( Ok((
&b"This is a multipart message.\n\n"[..], &b"This is a multipart message.\n\n"[..],
vec![ vec![

View file

@ -18,20 +18,6 @@ pub enum Mechanism<'a> {
Base64, Base64,
Other(&'a [u8]), Other(&'a [u8]),
} }
impl<'a> ToString for Mechanism<'a> {
fn to_string(&self) -> String {
use Mechanism::*;
let buf: &[u8] = match self {
_7Bit => b"7bit",
_8Bit => b"8bit",
Binary => b"binary",
QuotedPrintable => b"quoted-printable",
Base64 => b"base64",
Other(x) => x,
};
String::from_utf8_lossy(buf).to_string()
}
}
pub fn mechanism(input: &[u8]) -> IResult<&[u8], Mechanism> { pub fn mechanism(input: &[u8]) -> IResult<&[u8], Mechanism> {
use Mechanism::*; use Mechanism::*;

View file

@ -10,34 +10,33 @@ pub mod mechanism;
/// Content-Type representation /// Content-Type representation
pub mod r#type; pub mod r#type;
use std::fmt;
use std::marker::PhantomData; use std::marker::PhantomData;
use crate::header;
use crate::imf::identification::MessageID; use crate::imf::identification::MessageID;
use crate::mime::field::Content; use crate::mime::field::Content;
use crate::mime::mechanism::Mechanism; use crate::mime::mechanism::Mechanism;
use crate::mime::r#type::{AnyType, NaiveType}; use crate::mime::r#type::{AnyType, NaiveType};
use crate::header;
use crate::text::misc_token::Unstructured; //Multipart, Message, Text, Binary}; use crate::text::misc_token::Unstructured; //Multipart, Message, Text, Binary};
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct MIME<'a, T> { pub struct MIME<'a, T> {
pub interpreted_type: T, pub interpreted: T,
pub fields: NaiveMIME<'a>, pub parsed: NaiveMIME<'a>
} }
impl<'a> Default for MIME<'a, r#type::DeductibleText> { impl<'a> Default for MIME<'a, r#type::Text> {
fn default() -> Self { fn default() -> Self {
Self { Self {
interpreted_type: r#type::DeductibleText::default(), interpreted: r#type::Text::default(),
fields: NaiveMIME::default(), parsed: NaiveMIME::default(),
} }
} }
} }
impl<'a> Default for MIME<'a, r#type::DeductibleMessage> { impl<'a> Default for MIME<'a, r#type::Message> {
fn default() -> Self { fn default() -> Self {
Self { Self {
interpreted_type: r#type::DeductibleMessage::default(), interpreted: r#type::Message::default(),
fields: NaiveMIME::default(), parsed: NaiveMIME::default(),
} }
} }
} }
@ -45,20 +44,10 @@ impl<'a> Default for MIME<'a, r#type::DeductibleMessage> {
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum AnyMIME<'a> { pub enum AnyMIME<'a> {
Mult(MIME<'a, r#type::Multipart>), Mult(MIME<'a, r#type::Multipart>),
Msg(MIME<'a, r#type::DeductibleMessage>), Msg(MIME<'a, r#type::Message>),
Txt(MIME<'a, r#type::DeductibleText>), Txt(MIME<'a, r#type::Text>),
Bin(MIME<'a, r#type::Binary>), Bin(MIME<'a, r#type::Binary>),
} }
impl<'a> AnyMIME<'a> {
pub fn fields(&self) -> &NaiveMIME<'a> {
match self {
Self::Mult(v) => &v.fields,
Self::Msg(v) => &v.fields,
Self::Txt(v) => &v.fields,
Self::Bin(v) => &v.fields,
}
}
}
impl<'a, T: WithDefaultType> From<AnyMIMEWithDefault<'a, T>> for AnyMIME<'a> { impl<'a, T: WithDefaultType> From<AnyMIMEWithDefault<'a, T>> for AnyMIME<'a> {
fn from(a: AnyMIMEWithDefault<'a, T>) -> Self { fn from(a: AnyMIMEWithDefault<'a, T>) -> Self {
@ -66,32 +55,21 @@ impl<'a, T: WithDefaultType> From<AnyMIMEWithDefault<'a, T>> for AnyMIME<'a> {
} }
} }
#[derive(PartialEq, Default, Clone)] #[derive(Debug, PartialEq, Default, Clone)]
pub struct NaiveMIME<'a> { pub struct NaiveMIME<'a> {
pub ctype: Option<NaiveType<'a>>, pub ctype: Option<NaiveType<'a>>,
pub transfer_encoding: Mechanism<'a>, pub transfer_encoding: Mechanism<'a>,
pub id: Option<MessageID<'a>>, pub id: Option<MessageID<'a>>,
pub description: Option<Unstructured<'a>>, pub description: Option<Unstructured<'a>>,
pub kv: Vec<header::Field<'a>>, pub header_ext: Vec<header::Kv<'a>>,
pub raw: &'a [u8], pub header_bad: Vec<&'a [u8]>,
}
impl<'a> fmt::Debug for NaiveMIME<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("NaiveMime")
.field("ctype", &self.ctype)
.field("transfer_encoding", &self.transfer_encoding)
.field("id", &self.id)
.field("description", &self.description)
.field("kv", &self.kv)
.field("raw", &String::from_utf8_lossy(self.raw))
.finish()
}
} }
impl<'a> FromIterator<Content<'a>> for NaiveMIME<'a> { impl<'a> FromIterator<Content<'a>> for NaiveMIME<'a> {
fn from_iter<I: IntoIterator<Item = Content<'a>>>(it: I) -> Self { fn from_iter<I: IntoIterator<Item = Content<'a>>>(it: I) -> Self {
it.into_iter() it.into_iter().fold(
.fold(NaiveMIME::default(), |mut section, field| { NaiveMIME::default(),
|mut section, field| {
match field { match field {
Content::Type(v) => section.ctype = Some(v), Content::Type(v) => section.ctype = Some(v),
Content::TransferEncoding(v) => section.transfer_encoding = v, Content::TransferEncoding(v) => section.transfer_encoding = v,
@ -99,29 +77,25 @@ impl<'a> FromIterator<Content<'a>> for NaiveMIME<'a> {
Content::Description(v) => section.description = Some(v), Content::Description(v) => section.description = Some(v),
}; };
section section
}) },
)
} }
} }
impl<'a> NaiveMIME<'a> { impl<'a> NaiveMIME<'a> {
pub fn with_kv(mut self, fields: Vec<header::Field<'a>>) -> Self { pub fn with_opt(mut self, opt: Vec<header::Kv<'a>>) -> Self {
self.kv = fields; self.header_ext = opt; self
self
} }
pub fn with_raw(mut self, raw: &'a [u8]) -> Self { pub fn with_bad(mut self, bad: Vec<&'a [u8]>) -> Self {
self.raw = raw; self.header_bad = bad; self
self
} }
pub fn to_interpreted<T: WithDefaultType>(self) -> AnyMIME<'a> { pub fn to_interpreted<T: WithDefaultType>(self) -> AnyMIME<'a> {
self.ctype self.ctype.as_ref().map(|c| c.to_type()).unwrap_or(T::default_type()).to_mime(self).into()
.as_ref()
.map(|c| c.to_type())
.unwrap_or(T::default_type())
.to_mime(self)
.into()
} }
} }
pub trait WithDefaultType { pub trait WithDefaultType {
fn default_type() -> AnyType; fn default_type() -> AnyType;
} }
@ -129,13 +103,13 @@ pub trait WithDefaultType {
pub struct WithGenericDefault {} pub struct WithGenericDefault {}
impl WithDefaultType for WithGenericDefault { impl WithDefaultType for WithGenericDefault {
fn default_type() -> AnyType { fn default_type() -> AnyType {
AnyType::Text(r#type::DeductibleText::default()) AnyType::Text(r#type::Text::default())
} }
} }
pub struct WithDigestDefault {} pub struct WithDigestDefault {}
impl WithDefaultType for WithDigestDefault { impl WithDefaultType for WithDigestDefault {
fn default_type() -> AnyType { fn default_type() -> AnyType {
AnyType::Message(r#type::DeductibleMessage::default()) AnyType::Message(r#type::Message::default())
} }
} }

View file

@ -5,29 +5,19 @@ use nom::{
sequence::{preceded, terminated, tuple}, sequence::{preceded, terminated, tuple},
IResult, IResult,
}; };
use std::fmt;
use crate::mime::charset::EmailCharset; use crate::mime::charset::EmailCharset;
use crate::mime::{AnyMIME, NaiveMIME, MIME};
use crate::text::misc_token::{mime_word, MIMEWord}; use crate::text::misc_token::{mime_word, MIMEWord};
use crate::text::words::mime_atom; use crate::text::words::mime_atom;
use crate::mime::{AnyMIME, MIME, NaiveMIME};
// --------- NAIVE TYPE // --------- NAIVE TYPE
#[derive(PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct NaiveType<'a> { pub struct NaiveType<'a> {
pub main: &'a [u8], pub main: &'a [u8],
pub sub: &'a [u8], pub sub: &'a [u8],
pub params: Vec<Parameter<'a>>, pub params: Vec<Parameter<'a>>,
} }
impl<'a> fmt::Debug for NaiveType<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("mime::NaiveType")
.field("main", &String::from_utf8_lossy(self.main))
.field("sub", &String::from_utf8_lossy(self.sub))
.field("params", &self.params)
.finish()
}
}
impl<'a> NaiveType<'a> { impl<'a> NaiveType<'a> {
pub fn to_type(&self) -> AnyType { pub fn to_type(&self) -> AnyType {
self.into() self.into()
@ -40,20 +30,11 @@ pub fn naive_type(input: &[u8]) -> IResult<&[u8], NaiveType> {
)(input) )(input)
} }
#[derive(PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct Parameter<'a> { pub struct Parameter<'a> {
pub name: &'a [u8], pub name: &'a [u8],
pub value: MIMEWord<'a>, pub value: MIMEWord<'a>,
} }
impl<'a> fmt::Debug for Parameter<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("mime::Parameter")
.field("name", &String::from_utf8_lossy(self.name))
.field("value", &self.value)
.finish()
}
}
pub fn parameter(input: &[u8]) -> IResult<&[u8], Parameter> { pub fn parameter(input: &[u8]) -> IResult<&[u8], Parameter> {
map( map(
tuple((mime_atom, tag(b"="), mime_word)), tuple((mime_atom, tag(b"="), mime_word)),
@ -70,10 +51,10 @@ pub fn parameter_list(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
pub enum AnyType { pub enum AnyType {
// Composite types // Composite types
Multipart(Multipart), Multipart(Multipart),
Message(Deductible<Message>), Message(Message),
// Discrete types // Discrete types
Text(Deductible<Text>), Text(Text),
Binary(Binary), Binary(Binary),
} }
@ -82,60 +63,30 @@ impl<'a> From<&'a NaiveType<'a>> for AnyType {
match nt.main.to_ascii_lowercase().as_slice() { match nt.main.to_ascii_lowercase().as_slice() {
b"multipart" => Multipart::try_from(nt) b"multipart" => Multipart::try_from(nt)
.map(Self::Multipart) .map(Self::Multipart)
.unwrap_or(Self::Text(DeductibleText::default())), .unwrap_or(Self::Text(Text::default())),
b"message" => Self::Message(DeductibleMessage::Explicit(Message::from(nt))), b"message" => Self::Message(Message::from(nt)),
b"text" => Self::Text(DeductibleText::Explicit(Text::from(nt))), b"text" => Self::Text(Text::from(nt)),
_ => Self::Binary(Binary::default()), _ => Self::Binary(Binary::default()),
} }
} }
} }
impl<'a> AnyType { impl<'a> AnyType {
pub fn to_mime(self, fields: NaiveMIME<'a>) -> AnyMIME<'a> { pub fn to_mime(self, parsed: NaiveMIME<'a>) -> AnyMIME<'a> {
match self { match self {
Self::Multipart(interpreted_type) => AnyMIME::Mult(MIME::<Multipart> { Self::Multipart(interpreted) => AnyMIME::Mult(MIME::<Multipart> { interpreted, parsed }),
interpreted_type, Self::Message(interpreted) => AnyMIME::Msg(MIME::<Message> { interpreted, parsed }),
fields, Self::Text(interpreted) => AnyMIME::Txt(MIME::<Text> { interpreted, parsed }),
}), Self::Binary(interpreted) => AnyMIME::Bin(MIME::<Binary> { interpreted, parsed }),
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)] #[derive(Debug, PartialEq, Clone)]
pub struct Multipart { pub struct Multipart {
pub subtype: MultipartSubtype, pub subtype: MultipartSubtype,
pub boundary: String, pub boundary: String,
} }
impl Multipart {
pub fn main_type(&self) -> String {
"multipart".into()
}
}
impl<'a> TryFrom<&'a NaiveType<'a>> for Multipart { impl<'a> TryFrom<&'a NaiveType<'a>> for Multipart {
type Error = (); type Error = ();
@ -160,19 +111,6 @@ pub enum MultipartSubtype {
Report, Report,
Unknown, 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 { impl<'a> From<&NaiveType<'a>> for MultipartSubtype {
fn from(nt: &NaiveType<'a>) -> Self { fn from(nt: &NaiveType<'a>) -> Self {
match nt.sub.to_ascii_lowercase().as_slice() { match nt.sub.to_ascii_lowercase().as_slice() {
@ -187,61 +125,28 @@ impl<'a> From<&NaiveType<'a>> for MultipartSubtype {
} }
#[derive(Debug, PartialEq, Default, Clone)] #[derive(Debug, PartialEq, Default, Clone)]
pub enum MessageSubtype { pub enum Message {
#[default] #[default]
RFC822, RFC822,
Partial, Partial,
External, External,
Unknown, 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 { impl<'a> From<&NaiveType<'a>> for Message {
fn from(nt: &NaiveType<'a>) -> Self { fn from(nt: &NaiveType<'a>) -> Self {
match nt.sub.to_ascii_lowercase().as_slice() { match nt.sub.to_ascii_lowercase().as_slice() {
b"rfc822" => Self { b"rfc822" => Self::RFC822,
subtype: MessageSubtype::RFC822, b"partial" => Self::Partial,
}, b"external" => Self::External,
b"partial" => Self { _ => Self::Unknown,
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)] #[derive(Debug, PartialEq, Default, Clone)]
pub struct Text { pub struct Text {
pub subtype: TextSubtype, pub subtype: TextSubtype,
pub charset: Deductible<EmailCharset>, pub charset: EmailCharset,
} }
impl<'a> From<&NaiveType<'a>> for Text { impl<'a> From<&NaiveType<'a>> for Text {
fn from(nt: &NaiveType<'a>) -> Self { fn from(nt: &NaiveType<'a>) -> Self {
@ -251,15 +156,8 @@ impl<'a> From<&NaiveType<'a>> for Text {
.params .params
.iter() .iter()
.find(|x| x.name.to_ascii_lowercase().as_slice() == b"charset") .find(|x| x.name.to_ascii_lowercase().as_slice() == b"charset")
.map(|x| Deductible::Explicit(EmailCharset::from(x.value.to_string().as_bytes()))) .map(|x| EmailCharset::from(x.value.to_string().as_bytes()))
.unwrap_or(Deductible::Inferred(EmailCharset::US_ASCII)), .unwrap_or(EmailCharset::US_ASCII),
}
}
}
impl From<Deductible<Text>> for Text {
fn from(d: Deductible<Text>) -> Self {
match d {
Deductible::Inferred(t) | Deductible::Explicit(t) => t,
} }
} }
} }
@ -271,15 +169,6 @@ pub enum TextSubtype {
Html, Html,
Unknown, 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 { impl<'a> From<&NaiveType<'a>> for TextSubtype {
fn from(nt: &NaiveType<'a>) -> Self { fn from(nt: &NaiveType<'a>) -> Self {
match nt.sub.to_ascii_lowercase().as_slice() { match nt.sub.to_ascii_lowercase().as_slice() {
@ -297,7 +186,6 @@ pub struct Binary {}
mod tests { mod tests {
use super::*; use super::*;
use crate::mime::charset::EmailCharset; use crate::mime::charset::EmailCharset;
use crate::mime::r#type::Deductible;
use crate::text::quoted::QuotedString; use crate::text::quoted::QuotedString;
#[test] #[test]
@ -331,10 +219,10 @@ mod tests {
assert_eq!( assert_eq!(
nt.to_type(), nt.to_type(),
AnyType::Text(Deductible::Explicit(Text { AnyType::Text(Text {
charset: Deductible::Explicit(EmailCharset::UTF_8), charset: EmailCharset::UTF_8,
subtype: TextSubtype::Plain, subtype: TextSubtype::Plain,
})) })
); );
} }
@ -356,12 +244,7 @@ mod tests {
let (rest, nt) = naive_type(b"message/rfc822").unwrap(); let (rest, nt) = naive_type(b"message/rfc822").unwrap();
assert_eq!(rest, &[]); assert_eq!(rest, &[]);
assert_eq!( assert_eq!(nt.to_type(), AnyType::Message(Message::RFC822),);
nt.to_type(),
AnyType::Message(Deductible::Explicit(Message {
subtype: MessageSubtype::RFC822
}))
);
} }
#[test] #[test]

View file

@ -8,7 +8,7 @@ fn main() {
let mut rawmail = Vec::new(); let mut rawmail = Vec::new();
io::stdin().lock().read_to_end(&mut rawmail).unwrap(); io::stdin().lock().read_to_end(&mut rawmail).unwrap();
let (_, eml) = eml_codec::parse_message(&rawmail).unwrap(); let (_, eml) = eml_codec::email(&rawmail).unwrap();
println!("{:#?}", eml); println!("{:#?}", eml);
assert!(eml.imf.date.is_some()); assert!(eml.imf.date.is_some());
assert!(!eml.imf.from.is_empty()); assert!(!eml.imf.from.is_empty());

View file

@ -1,49 +1,23 @@
use nom::IResult; use nom::IResult;
use std::fmt;
use crate::header; use crate::header::{header, self};
use crate::imf; use crate::imf;
use crate::mime; use crate::mime;
use crate::part::{self, AnyPart}; use crate::part::{self, AnyPart, field::MixedField};
use crate::pointers;
use crate::text::boundary::{boundary, Delimiter}; use crate::text::boundary::{boundary, Delimiter};
//--- Multipart //--- Multipart
#[derive(PartialEq)] #[derive(Debug, PartialEq)]
pub struct Multipart<'a> { pub struct Multipart<'a> {
pub mime: mime::MIME<'a, mime::r#type::Multipart>, pub interpreted: mime::MIME<'a, mime::r#type::Multipart>,
pub children: Vec<AnyPart<'a>>, pub children: Vec<AnyPart<'a>>,
pub raw_part_inner: &'a [u8], pub preamble: &'a [u8],
pub raw_part_outer: &'a [u8], pub epilogue: &'a [u8],
}
impl<'a> fmt::Debug for Multipart<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("part::Multipart")
.field("mime", &self.mime)
.field("children", &self.children)
.field(
"raw_part_inner",
&String::from_utf8_lossy(self.raw_part_inner),
)
.field(
"raw_part_outer",
&String::from_utf8_lossy(self.raw_part_outer),
)
.finish()
}
} }
impl<'a> Multipart<'a> { impl<'a> Multipart<'a> {
pub fn preamble(&self) -> &'a [u8] { pub fn with_epilogue(mut self, e: &'a [u8]) -> Self {
pointers::parsed(self.raw_part_outer, self.raw_part_inner) self.epilogue = e;
} self
pub fn epilogue(&self) -> &'a [u8] {
pointers::rest(self.raw_part_outer, self.raw_part_inner)
}
pub fn preamble_and_body(&self) -> &'a [u8] {
pointers::with_preamble(self.raw_part_outer, self.raw_part_inner)
}
pub fn body_and_epilogue(&self) -> &'a [u8] {
pointers::with_epilogue(self.raw_part_outer, self.raw_part_inner)
} }
} }
@ -53,25 +27,19 @@ pub fn multipart<'a>(
let m = m.clone(); let m = m.clone();
move |input| { move |input| {
// init let bound = m.interpreted.boundary.as_bytes();
let outer_orig = input; let (mut input_loop, preamble) = part::part_raw(bound)(input)?;
let bound = m.interpreted_type.boundary.as_bytes();
let mut mparts: Vec<AnyPart> = vec![]; let mut mparts: Vec<AnyPart> = vec![];
// skip preamble
let (mut input_loop, _) = part::part_raw(bound)(input)?;
let inner_orig = input_loop;
loop { loop {
let input = match boundary(bound)(input_loop) { let input = match boundary(bound)(input_loop) {
Err(_) => { Err(_) => {
return Ok(( return Ok((
input_loop, input_loop,
Multipart { Multipart {
mime: m.clone(), interpreted: m.clone(),
children: mparts, children: mparts,
raw_part_inner: pointers::parsed(inner_orig, input_loop), preamble,
raw_part_outer: pointers::parsed(outer_orig, input_loop), epilogue: &[],
}, },
)) ))
} }
@ -79,13 +47,10 @@ pub fn multipart<'a>(
return Ok(( return Ok((
inp, inp,
Multipart { Multipart {
mime: m.clone(), interpreted: m.clone(),
children: mparts, children: mparts,
raw_part_inner: pointers::parsed(inner_orig, inp), preamble,
raw_part_outer: pointers::parsed( epilogue: &[],
outer_orig,
&outer_orig[outer_orig.len()..],
),
}, },
)) ))
} }
@ -93,40 +58,23 @@ pub fn multipart<'a>(
}; };
// parse mime headers, otherwise pick default mime // parse mime headers, otherwise pick default mime
let (input, naive_mime) = match header::header_kv(input) { let (input, naive_mime) = match header(mime::field::content)(input) {
Ok((input_eom, fields)) => { Ok((input, (known, unknown, bad))) => (input, known.into_iter().collect::<mime::NaiveMIME>().with_opt(unknown).with_bad(bad)),
let raw_hdrs = pointers::parsed(input, input_eom);
let mime = fields
.iter()
.flat_map(mime::field::Content::try_from)
.into_iter()
.collect::<mime::NaiveMIME>();
let mime = mime.with_kv(fields).with_raw(raw_hdrs);
(input_eom, mime)
}
Err(_) => (input, mime::NaiveMIME::default()), Err(_) => (input, mime::NaiveMIME::default()),
}; };
// interpret mime according to context // interpret mime according to context
let mime = match m.interpreted_type.subtype { let mime = match m.interpreted.subtype {
mime::r#type::MultipartSubtype::Digest => naive_mime mime::r#type::MultipartSubtype::Digest => naive_mime.to_interpreted::<mime::WithDigestDefault>().into(),
.to_interpreted::<mime::WithDigestDefault>() _ => naive_mime.to_interpreted::<mime::WithGenericDefault>().into(),
.into(),
_ => naive_mime
.to_interpreted::<mime::WithGenericDefault>()
.into(),
}; };
// parse raw part // parse raw part
let (input, rpart) = part::part_raw(bound)(input)?; let (input, rpart) = part::part_raw(bound)(input)?;
// parse mime body // parse mime body
// -- we do not keep the input as we are using the mparts.push(part::to_anypart(mime, rpart));
// part_raw function as our cursor here.
let (_, part) = part::anypart(mime)(rpart)?;
mparts.push(part);
input_loop = input; input_loop = input;
} }
@ -135,72 +83,47 @@ pub fn multipart<'a>(
//--- Message //--- Message
#[derive(PartialEq)] #[derive(Debug, PartialEq)]
pub struct Message<'a> { pub struct Message<'a> {
pub mime: mime::MIME<'a, mime::r#type::DeductibleMessage>, pub interpreted: mime::MIME<'a, mime::r#type::Message>,
pub imf: imf::Imf<'a>, pub imf: imf::Imf<'a>,
pub child: Box<AnyPart<'a>>, pub child: Box<AnyPart<'a>>,
pub epilogue: &'a [u8],
pub raw_part: &'a [u8],
pub raw_headers: &'a [u8],
pub raw_body: &'a [u8],
} }
impl<'a> fmt::Debug for Message<'a> { impl<'a> Message<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { pub fn with_epilogue(mut self, e: &'a [u8]) -> Self {
fmt.debug_struct("part::Message") self.epilogue = e;
.field("mime", &self.mime) self
.field("imf", &self.imf)
.field("child", &self.child)
.field("raw_part", &String::from_utf8_lossy(self.raw_part))
.field("raw_headers", &String::from_utf8_lossy(self.raw_headers))
.field("raw_body", &String::from_utf8_lossy(self.raw_body))
.finish()
} }
} }
pub fn message<'a>( pub fn message<'a>(
m: mime::MIME<'a, mime::r#type::DeductibleMessage>, m: mime::MIME<'a, mime::r#type::Message>,
) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], Message<'a>> { ) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], Message<'a>> {
move |input: &[u8]| { move |input: &[u8]| {
let orig = input;
// parse header fields // parse header fields
let (input, headers) = header::header_kv(input)?; let (input, (known, unknown, bad)): (_, (Vec::<MixedField>, Vec<header::Kv>, Vec<&[u8]>)) =
header(part::field::mixed_field)(input)?;
// extract raw parts 1/2
let raw_headers = pointers::parsed(orig, input);
let body_orig = input;
//---------------
// aggregate header fields // aggregate header fields
let (naive_mime, imf) = part::field::split_and_build(&headers); let (naive_mime, imf) = part::field::sections(known);
// Bind headers to mime // attach bad headers to imf
let naive_mime = naive_mime.with_kv(headers); let imf = imf.with_opt(unknown).with_bad(bad);
// interpret headers to choose the child mime type // interpret headers to choose a mime type
let in_mime = naive_mime let in_mime = naive_mime.to_interpreted::<mime::WithGenericDefault>().into();
.with_raw(raw_headers)
.to_interpreted::<mime::WithGenericDefault>()
.into();
//---------------
// parse a part following this mime specification // parse this mimetype
let (input, part) = part::anypart(in_mime)(input)?; let part = part::to_anypart(in_mime, input);
// extract raw parts 2/2
let raw_body = pointers::parsed(body_orig, input);
let raw_part = pointers::parsed(orig, input);
Ok(( Ok((
input, &[],
Message { Message {
mime: m.clone(), interpreted: m.clone(),
imf, imf,
raw_part,
raw_headers,
raw_body,
child: Box::new(part), child: Box::new(part),
epilogue: &[],
}, },
)) ))
} }
@ -212,21 +135,22 @@ mod tests {
use crate::part::discrete::Text; use crate::part::discrete::Text;
use crate::part::AnyPart; use crate::part::AnyPart;
use crate::text::encoding::{Base64Word, EncodedWord, QuotedChunk, QuotedWord}; use crate::text::encoding::{Base64Word, EncodedWord, QuotedChunk, QuotedWord};
use crate::text::misc_token::{MIMEWord, Phrase, UnstrToken, Unstructured, Word}; use crate::text::misc_token::{Phrase, UnstrToken, Unstructured, Word, MIMEWord};
use crate::text::quoted::QuotedString; use crate::text::quoted::QuotedString;
use chrono::{FixedOffset, TimeZone}; use chrono::{FixedOffset, TimeZone};
#[test] #[test]
fn test_multipart() { fn test_multipart() {
let base_mime = mime::MIME { let base_mime = mime::MIME {
interpreted_type: mime::r#type::Multipart { interpreted: mime::r#type::Multipart {
subtype: mime::r#type::MultipartSubtype::Alternative, subtype: mime::r#type::MultipartSubtype::Alternative,
boundary: "simple boundary".to_string(), boundary: "simple boundary".to_string(),
}, },
fields: mime::NaiveMIME::default(), parsed: mime::NaiveMIME::default(),
}; };
let input = b"This is the preamble. It is to be ignored, though it assert_eq!(
multipart(base_mime.clone())(b"This is the preamble. It is to be ignored, though it
is a handy place for composition agents to include an is a handy place for composition agents to include an
explanatory note to non-MIME conformant readers. explanatory note to non-MIME conformant readers.
@ -243,50 +167,30 @@ It DOES end with a linebreak.
--simple boundary-- --simple boundary--
This is the epilogue. It is also to be ignored. This is the epilogue. It is also to be ignored.
"; "),
let inner = b"
--simple boundary
This is implicitly typed plain US-ASCII text.
It does NOT end with a linebreak.
--simple boundary
Content-type: text/plain; charset=us-ascii
This is explicitly typed plain US-ASCII text.
It DOES end with a linebreak.
--simple boundary--
";
assert_eq!(
multipart(base_mime.clone())(input),
Ok((&b"\nThis is the epilogue. It is also to be ignored.\n"[..], Ok((&b"\nThis is the epilogue. It is also to be ignored.\n"[..],
Multipart { Multipart {
mime: base_mime, interpreted: base_mime,
raw_part_outer: input, preamble: &b"This is the preamble. It is to be ignored, though it\nis a handy place for composition agents to include an\nexplanatory note to non-MIME conformant readers.\n"[..],
raw_part_inner: inner, epilogue: &b""[..],
children: vec![ children: vec![
AnyPart::Txt(Text { AnyPart::Txt(Text {
mime: mime::MIME { interpreted: mime::MIME {
interpreted_type: mime::r#type::Deductible::Inferred(mime::r#type::Text { interpreted: mime::r#type::Text {
subtype: mime::r#type::TextSubtype::Plain, subtype: mime::r#type::TextSubtype::Plain,
charset: mime::r#type::Deductible::Inferred(mime::charset::EmailCharset::US_ASCII), charset: mime::charset::EmailCharset::US_ASCII,
}),
fields: mime::NaiveMIME {
raw: &b"\n"[..],
..mime::NaiveMIME::default()
}, },
parsed: mime::NaiveMIME::default(),
}, },
body: &b"This is implicitly typed plain US-ASCII text.\nIt does NOT end with a linebreak."[..], body: &b"This is implicitly typed plain US-ASCII text.\nIt does NOT end with a linebreak."[..],
}), }),
AnyPart::Txt(Text { AnyPart::Txt(Text {
mime: mime::MIME { interpreted: mime::MIME {
interpreted_type: mime::r#type::Deductible::Explicit(mime::r#type::Text { interpreted: mime::r#type::Text {
subtype: mime::r#type::TextSubtype::Plain, subtype: mime::r#type::TextSubtype::Plain,
charset: mime::r#type::Deductible::Explicit(mime::charset::EmailCharset::US_ASCII), charset: mime::charset::EmailCharset::US_ASCII,
}), },
fields: mime::NaiveMIME { parsed: mime::NaiveMIME {
ctype: Some(mime::r#type::NaiveType { ctype: Some(mime::r#type::NaiveType {
main: &b"text"[..], main: &b"text"[..],
sub: &b"plain"[..], sub: &b"plain"[..],
@ -297,10 +201,6 @@ It DOES end with a linebreak.
} }
] ]
}), }),
raw: &b"Content-type: text/plain; charset=us-ascii\n\n"[..],
kv: vec![
header::Field::Good(header::Kv2(&b"Content-type"[..], &b"text/plain; charset=us-ascii"[..]))
],
..mime::NaiveMIME::default() ..mime::NaiveMIME::default()
}, },
}, },
@ -359,90 +259,14 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
"# "#
.as_bytes(); .as_bytes();
let hdrs = br#"Date: Sat, 8 Jul 2023 07:14:29 +0200 let base_mime = mime::MIME::<mime::r#type::Message>::default();
From: Grrrnd Zero <grrrndzero@example.org>
To: John Doe <jdoe@machine.example>
CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>
Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=
=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=
X-Unknown: something something
Bad entry
on multiple lines
Message-ID: <NTAxNzA2AC47634Y366BAMTY4ODc5MzQyODY0ODY5@www.grrrndzero.org>
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="b1_e376dc71bafc953c0b0fdeb9983a9956"
Content-Transfer-Encoding: 7bit
"#;
let body = br#"This is a multi-part message in MIME format.
--b1_e376dc71bafc953c0b0fdeb9983a9956
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
GZ
OoOoO
oOoOoOoOo
oOoOoOoOoOoOoOoOo
oOoOoOoOoOoOoOoOoOoOoOo
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
--b1_e376dc71bafc953c0b0fdeb9983a9956
Content-Type: text/html; charset=us-ascii
<div style="text-align: center;"><strong>GZ</strong><br />
OoOoO<br />
oOoOoOoOo<br />
oOoOoOoOoOoOoOoOo<br />
oOoOoOoOoOoOoOoOoOoOoOo<br />
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo<br />
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
</div>
--b1_e376dc71bafc953c0b0fdeb9983a9956--
"#;
let inner = br#"
--b1_e376dc71bafc953c0b0fdeb9983a9956
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
GZ
OoOoO
oOoOoOoOo
oOoOoOoOoOoOoOoOo
oOoOoOoOoOoOoOoOoOoOoOo
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
--b1_e376dc71bafc953c0b0fdeb9983a9956
Content-Type: text/html; charset=us-ascii
<div style="text-align: center;"><strong>GZ</strong><br />
OoOoO<br />
oOoOoOoOo<br />
oOoOoOoOoOoOoOoOo<br />
oOoOoOoOoOoOoOoOoOoOoOo<br />
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo<br />
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
</div>
--b1_e376dc71bafc953c0b0fdeb9983a9956--
"#;
let base_mime = mime::MIME::<mime::r#type::DeductibleMessage>::default();
assert_eq!( assert_eq!(
message(base_mime.clone())(fullmail), message(base_mime.clone())(fullmail),
Ok(( Ok((
&[][..], &[][..],
Message { Message {
mime: base_mime, interpreted: base_mime,
raw_part: fullmail, epilogue: &b""[..],
raw_headers: hdrs,
raw_body: body,
imf: imf::Imf { imf: imf::Imf {
date: Some(FixedOffset::east_opt(2 * 3600) date: Some(FixedOffset::east_opt(2 * 3600)
.unwrap() .unwrap()
@ -506,15 +330,24 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
right: &b"www.grrrndzero.org"[..], right: &b"www.grrrndzero.org"[..],
}), }),
mime_version: Some(imf::mime::Version { major: 1, minor: 0}), mime_version: Some(imf::mime::Version { major: 1, minor: 0}),
header_ext: vec![
header::Kv(&b"X-Unknown"[..], Unstructured(vec![
UnstrToken::Plain(&b"something"[..]),
UnstrToken::Plain(&b"something"[..]),
]))
],
header_bad: vec![
&b"Bad entry\n on multiple lines\n"[..],
],
..imf::Imf::default() ..imf::Imf::default()
}, },
child: Box::new(AnyPart::Mult(Multipart { child: Box::new(AnyPart::Mult(Multipart {
mime: mime::MIME { interpreted: mime::MIME {
interpreted_type: mime::r#type::Multipart { interpreted: mime::r#type::Multipart {
subtype: mime::r#type::MultipartSubtype::Alternative, subtype: mime::r#type::MultipartSubtype::Alternative,
boundary: "b1_e376dc71bafc953c0b0fdeb9983a9956".to_string(), boundary: "b1_e376dc71bafc953c0b0fdeb9983a9956".to_string(),
}, },
fields: mime::NaiveMIME { parsed: mime::NaiveMIME {
ctype: Some(mime::r#type::NaiveType { ctype: Some(mime::r#type::NaiveType {
main: &b"multipart"[..], main: &b"multipart"[..],
sub: &b"alternative"[..], sub: &b"alternative"[..],
@ -525,33 +358,19 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
} }
] ]
}), }),
raw: hdrs,
kv: vec![
header::Field::Good(header::Kv2(&b"Date"[..], &b"Sat, 8 Jul 2023 07:14:29 +0200"[..])),
header::Field::Good(header::Kv2(&b"From"[..], &b"Grrrnd Zero <grrrndzero@example.org>"[..])),
header::Field::Good(header::Kv2(&b"To"[..], &b"John Doe <jdoe@machine.example>"[..])),
header::Field::Good(header::Kv2(&b"CC"[..], &b"=?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>"[..])),
header::Field::Good(header::Kv2(&b"Subject"[..], &b"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?="[..])),
header::Field::Good(header::Kv2(&b"X-Unknown"[..], &b"something something"[..])),
header::Field::Bad(&b"Bad entry\n on multiple lines\n"[..]),
header::Field::Good(header::Kv2(&b"Message-ID"[..], &b"<NTAxNzA2AC47634Y366BAMTY4ODc5MzQyODY0ODY5@www.grrrndzero.org>"[..])),
header::Field::Good(header::Kv2(&b"MIME-Version"[..], &b"1.0"[..])),
header::Field::Good(header::Kv2(&b"Content-Type"[..], &b"multipart/alternative;\n boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\""[..])),
header::Field::Good(header::Kv2(&b"Content-Transfer-Encoding"[..], &b"7bit"[..])),
],
..mime::NaiveMIME::default() ..mime::NaiveMIME::default()
}, },
}, },
raw_part_inner: inner, preamble: &b"This is a multi-part message in MIME format.\n"[..],
raw_part_outer: body, epilogue: &b""[..],
children: vec![ children: vec![
AnyPart::Txt(Text { AnyPart::Txt(Text {
mime: mime::MIME { interpreted: mime::MIME {
interpreted_type: mime::r#type::Deductible::Explicit(mime::r#type::Text { interpreted: mime::r#type::Text {
subtype: mime::r#type::TextSubtype::Plain, subtype: mime::r#type::TextSubtype::Plain,
charset: mime::r#type::Deductible::Explicit(mime::charset::EmailCharset::UTF_8), charset: mime::charset::EmailCharset::UTF_8,
}), },
fields: mime::NaiveMIME { parsed: mime::NaiveMIME {
ctype: Some(mime::r#type::NaiveType { ctype: Some(mime::r#type::NaiveType {
main: &b"text"[..], main: &b"text"[..],
sub: &b"plain"[..], sub: &b"plain"[..],
@ -563,24 +382,19 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
] ]
}), }),
transfer_encoding: mime::mechanism::Mechanism::QuotedPrintable, transfer_encoding: mime::mechanism::Mechanism::QuotedPrintable,
kv: vec![
header::Field::Good(header::Kv2(&b"Content-Type"[..], &b"text/plain; charset=utf-8"[..])),
header::Field::Good(header::Kv2(&b"Content-Transfer-Encoding"[..], &b"quoted-printable"[..])),
],
raw: &b"Content-Type: text/plain; charset=utf-8\nContent-Transfer-Encoding: quoted-printable\n\n"[..],
..mime::NaiveMIME::default() ..mime::NaiveMIME::default()
} }
}, },
body: &b"GZ\nOoOoO\noOoOoOoOo\noOoOoOoOoOoOoOoOo\noOoOoOoOoOoOoOoOoOoOoOo\noOoOoOoOoOoOoOoOoOoOoOoOoOoOo\nOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\n"[..], body: &b"GZ\nOoOoO\noOoOoOoOo\noOoOoOoOoOoOoOoOo\noOoOoOoOoOoOoOoOoOoOoOo\noOoOoOoOoOoOoOoOoOoOoOoOoOoOo\nOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\n"[..],
}), }),
AnyPart::Txt(Text { AnyPart::Txt(Text {
mime: mime::MIME { interpreted: mime::MIME {
interpreted_type: mime::r#type::Deductible::Explicit(mime::r#type::Text { interpreted: mime::r#type::Text {
subtype: mime::r#type::TextSubtype::Html, subtype: mime::r#type::TextSubtype::Html,
charset: mime::r#type::Deductible::Explicit(mime::charset::EmailCharset::US_ASCII), charset: mime::charset::EmailCharset::US_ASCII,
}), },
fields: mime::NaiveMIME { parsed: mime::NaiveMIME {
ctype: Some(mime::r#type::NaiveType { ctype: Some(mime::r#type::NaiveType {
main: &b"text"[..], main: &b"text"[..],
sub: &b"html"[..], sub: &b"html"[..],
@ -591,10 +405,6 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
} }
] ]
}), }),
kv: vec![
header::Field::Good(header::Kv2(&b"Content-Type"[..], &b"text/html; charset=us-ascii"[..])),
],
raw: &b"Content-Type: text/html; charset=us-ascii\n\n"[..],
..mime::NaiveMIME::default() ..mime::NaiveMIME::default()
}, },
}, },

View file

@ -4,30 +4,36 @@ use crate::mime;
#[derive(PartialEq)] #[derive(PartialEq)]
pub struct Text<'a> { pub struct Text<'a> {
pub mime: mime::MIME<'a, mime::r#type::DeductibleText>, pub interpreted: mime::MIME<'a, mime::r#type::Text>,
pub body: &'a [u8], pub body: &'a [u8],
} }
impl<'a> fmt::Debug for Text<'a> { impl<'a> fmt::Debug for Text<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("part::Text") fmt.debug_struct("part::Text")
.field("mime", &self.mime) .field("mime", &self.interpreted)
.field("body", &String::from_utf8_lossy(self.body)) .field(
"body",
&format_args!("\"{}\"", String::from_utf8_lossy(self.body)),
)
.finish() .finish()
} }
} }
#[derive(PartialEq)] #[derive(PartialEq)]
pub struct Binary<'a> { pub struct Binary<'a> {
pub mime: mime::MIME<'a, mime::r#type::Binary>, pub interpreted: mime::MIME<'a, mime::r#type::Binary>,
pub body: &'a [u8], pub body: &'a [u8],
} }
impl<'a> fmt::Debug for Binary<'a> { impl<'a> fmt::Debug for Binary<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("part::Binary") fmt.debug_struct("part::Binary")
.field("mime", &self.mime) .field("mime", &self.interpreted)
.field("body", &String::from_utf8_lossy(self.body)) .field(
"body",
&format_args!("\"{}\"", String::from_utf8_lossy(self.body)),
)
.finish() .finish()
} }
} }

View file

@ -1,24 +1,50 @@
use crate::header; use nom::{branch::alt, combinator::map, IResult};
use crate::imf; use crate::imf;
use crate::mime; use crate::mime;
pub fn split_and_build<'a>(v: &Vec<header::Field<'a>>) -> (mime::NaiveMIME<'a>, imf::Imf<'a>) { pub enum MixedField<'a> {
let (mimev, imfv) = v.iter().fold( MIME(mime::field::Content<'a>),
( IMF(imf::field::Field<'a>),
Vec::<mime::field::Content>::new(), }
Vec::<imf::field::Field>::new(), #[allow(dead_code)]
), impl<'a> MixedField<'a> {
|(mut mime, mut imf), f| { pub fn mime(&self) -> Option<&mime::field::Content<'a>> {
if let Ok(m) = mime::field::Content::try_from(f) { match self {
mime.push(m); Self::MIME(v) => Some(v),
} else if let Ok(i) = imf::field::Field::try_from(f) { _ => None,
imf.push(i); }
}
pub fn to_mime(self) -> Option<mime::field::Content<'a>> {
match self {
Self::MIME(v) => Some(v),
_ => None,
}
}
pub fn imf(&self) -> Option<&imf::field::Field<'a>> {
match self {
Self::IMF(v) => Some(v),
_ => None,
}
}
pub fn to_imf(self) -> Option<imf::field::Field<'a>> {
match self {
Self::IMF(v) => Some(v),
_ => None,
}
}
} }
(mime, imf)
},
);
let fmime = mimev.into_iter().collect::<mime::NaiveMIME>(); pub fn sections<'a>(list: Vec<MixedField<'a>>) -> (mime::NaiveMIME<'a>, imf::Imf<'a>) {
let fimf = imfv.into_iter().collect::<imf::Imf>(); let (v1, v2): (Vec<MixedField>, Vec<_>) = list.into_iter().partition(|v| v.mime().is_some());
(fmime, fimf) let mime = v1.into_iter().flat_map(MixedField::to_mime).collect::<mime::NaiveMIME>();
let imf = v2.into_iter().flat_map(MixedField::to_imf).collect::<imf::Imf>();
(mime, imf)
}
pub fn mixed_field(input: &[u8]) -> IResult<&[u8], MixedField> {
alt((
map(mime::field::content, MixedField::MIME),
map(imf::field::field, MixedField::IMF),
))(input)
} }

View file

@ -17,7 +17,7 @@ use nom::{
}; };
use crate::mime; use crate::mime;
use crate::mime::{AnyMIME, NaiveMIME}; use crate::mime::AnyMIME;
use crate::part::{ use crate::part::{
composite::{message, multipart, Message, Multipart}, composite::{message, multipart, Message, Multipart},
discrete::{Binary, Text}, discrete::{Binary, Text},
@ -58,61 +58,30 @@ impl<'a> AnyPart<'a> {
_ => None, _ => None,
} }
} }
pub fn mime(&self) -> &NaiveMIME<'a> {
match self {
Self::Mult(v) => &v.mime.fields,
Self::Msg(v) => &v.mime.fields,
Self::Txt(v) => &v.mime.fields,
Self::Bin(v) => &v.mime.fields,
}
}
}
impl<'a> From<Multipart<'a>> for AnyPart<'a> {
fn from(m: Multipart<'a>) -> Self {
Self::Mult(m)
}
}
impl<'a> From<Message<'a>> for AnyPart<'a> {
fn from(m: Message<'a>) -> Self {
Self::Msg(m)
}
} }
/// Parse any type of part pub fn to_anypart<'a>(m: AnyMIME<'a>, rpart: &'a [u8]) -> AnyPart<'a> {
/// match m {
/// ## Note AnyMIME::Mult(a) => multipart(a)(rpart)
/// .map(|(rest, multi)| AnyPart::Mult(multi.with_epilogue(rest)))
/// Multiparts are a bit special as they have a clearly delimited beginning
/// and end contrary to all the other parts that are going up to the end of the buffer
pub fn anypart<'a>(m: AnyMIME<'a>) -> impl FnOnce(&'a [u8]) -> IResult<&'a [u8], AnyPart<'a>> {
move |input| {
let part = match m {
AnyMIME::Mult(a) => multipart(a)(input)
.map(|(_, multi)| multi.into())
.unwrap_or(AnyPart::Txt(Text { .unwrap_or(AnyPart::Txt(Text {
mime: mime::MIME::<mime::r#type::DeductibleText>::default(), interpreted: mime::MIME::<mime::r#type::Text>::default(),
body: input, body: rpart,
})), })),
AnyMIME::Msg(a) => { AnyMIME::Msg(a) => message(a)(rpart)
message(a)(input) .map(|(rest, msg)| AnyPart::Msg(msg.with_epilogue(rest)))
.map(|(_, msg)| msg.into())
.unwrap_or(AnyPart::Txt(Text { .unwrap_or(AnyPart::Txt(Text {
mime: mime::MIME::<mime::r#type::DeductibleText>::default(), interpreted: mime::MIME::<mime::r#type::Text>::default(),
body: input, body: rpart,
})) })),
}
AnyMIME::Txt(a) => AnyPart::Txt(Text { AnyMIME::Txt(a) => AnyPart::Txt(Text {
mime: a, interpreted: a,
body: input, body: rpart,
}), }),
AnyMIME::Bin(a) => AnyPart::Bin(Binary { AnyMIME::Bin(a) => AnyPart::Bin(Binary {
mime: a, interpreted: a,
body: input, body: rpart,
}), }),
};
// This function always consumes the whole input
Ok((&input[input.len()..], part))
} }
} }

View file

@ -1,55 +0,0 @@
pub fn parsed<'a>(input: &'a [u8], rest: &'a [u8]) -> &'a [u8] {
let start = input.as_ptr();
let offset = rest.as_ptr();
let idx = (offset as usize - start as usize) / std::mem::size_of::<u8>();
assert!(idx <= input.len());
&input[..idx]
}
pub fn rest<'a>(input: &'a [u8], parsed: &'a [u8]) -> &'a [u8] {
let start = input.as_ptr();
let offset = (&parsed[parsed.len()..]).as_ptr();
let idx = (offset as usize - start as usize) / std::mem::size_of::<u8>();
assert!(idx <= input.len());
&input[idx..]
}
pub fn with_preamble<'a>(input: &'a [u8], parsed: &'a [u8]) -> &'a [u8] {
let start = input.as_ptr();
let offset = (&parsed[parsed.len()..]).as_ptr();
let idx = (offset as usize - start as usize) / std::mem::size_of::<u8>();
assert!(idx <= input.len());
&input[..idx]
}
pub fn with_epilogue<'a>(input: &'a [u8], rest: &'a [u8]) -> &'a [u8] {
let start = input.as_ptr();
let offset = rest.as_ptr();
let idx = (offset as usize - start as usize) / std::mem::size_of::<u8>();
assert!(idx <= input.len());
&input[idx..]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all() {
let outer = b"aa bb cc";
let inner = &outer[3..5];
assert_eq!(inner, b"bb");
let p = parsed(outer, inner);
assert_eq!(p, b"aa ");
let r = rest(outer, inner);
assert_eq!(r, b" cc");
let wp = with_preamble(outer, inner);
assert_eq!(wp, b"aa bb");
let we = with_epilogue(outer, inner);
assert_eq!(we, b"bb cc");
}
}

View file

@ -136,11 +136,6 @@ pub const TILDE: u8 = 0x7E; // ~
// GROUP OF CHARACTERS // GROUP OF CHARACTERS
// -- CRLF // -- CRLF
pub const CRLF: &[u8] = &[CR, LF]; pub const CRLF: &[u8] = &[CR, LF];
// -- CRCRLF
// Sometimes portable libraries replace transparently
// the "\n" with "\r\n" on Windows. When developpers
// explicitly write "\r\n", the library generates "\r\r\n".
pub const CRCRLF: &[u8] = &[CR, CR, LF];
// -- WHITESPACE // -- WHITESPACE
pub const WS: &[u8] = &[HT, SP]; pub const WS: &[u8] = &[HT, SP];

View file

@ -22,12 +22,7 @@ use nom::{
/// \r or \n is allowed nowhere else, so we also add this support. /// \r or \n is allowed nowhere else, so we also add this support.
pub fn obs_crlf(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn obs_crlf(input: &[u8]) -> IResult<&[u8], &[u8]> {
alt(( alt((tag(ascii::CRLF), tag(&[ascii::CR]), tag(&[ascii::LF])))(input)
tag(ascii::CRLF),
tag(ascii::CRCRLF),
tag(&[ascii::CR]),
tag(&[ascii::LF]),
))(input)
} }
/// ```abnf /// ```abnf