report all parsed data

This commit is contained in:
Quentin 2023-07-24 22:08:13 +02:00
parent 07f969d109
commit 6ce280151b
Signed by: quentin
GPG key ID: E9602264D639FF68
14 changed files with 233 additions and 124 deletions

View file

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

View file

@ -18,8 +18,8 @@ pub enum CompField<'a, T> {
Bad(&'a [u8]),
}
#[derive(Debug, PartialEq)]
pub struct Kv<'a>(&'a [u8], Unstructured<'a>);
#[derive(Debug, PartialEq, Clone)]
pub struct Kv<'a>(pub &'a [u8], pub Unstructured<'a>);
pub fn header<'a, T>(

View file

@ -6,7 +6,7 @@ use nom::{
IResult,
};
use crate::header::{field_name, header, self};
use crate::header::{field_name, header};
use crate::imf::address::{address_list, mailbox_list, nullable_address_list, AddressList};
use crate::imf::datetime::section as date;
use crate::imf::identification::{msg_id, msg_list, MessageID, MessageIDList};
@ -80,18 +80,12 @@ pub fn field(input: &[u8]) -> IResult<&[u8], Field> {
)(input)
}
#[derive(Debug, PartialEq)]
pub struct Header<'a> {
pub imf: Imf<'a>,
pub ext: Vec<header::Kv<'a>>,
pub bad: Vec<&'a [u8]>,
}
pub fn imf(input: &[u8]) -> IResult<&[u8], Header> {
map(header(field), |(known, unknown, bad)| Header {
imf: Imf::from_iter(known),
ext: unknown,
bad
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)
}
@ -117,7 +111,7 @@ between the header information and the body of the message.";
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."[..],
Header { bad: vec![], ext: vec![], imf: Imf {
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,
@ -141,7 +135,7 @@ between the header information and the body of the message.";
UnstrToken::Plain(&b"message"[..]),
])),
..Imf::default()
}}
}
)),
)
}

View file

@ -14,6 +14,7 @@ use crate::imf::identification::MessageID;
use crate::imf::mailbox::{AddrSpec, MailboxRef};
use crate::imf::mime::Version;
use crate::imf::trace::ReceivedLog;
use crate::header;
use crate::text::misc_token::{PhraseList, Unstructured};
use chrono::{DateTime, FixedOffset};
@ -49,6 +50,19 @@ pub struct Imf<'a> {
// MIME
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,

View file

@ -54,7 +54,7 @@ use nom::IResult;
/// );
/// ```
pub fn email(input: &[u8]) -> IResult<&[u8], part::composite::Message> {
part::composite::message(mime::Message::default())(input)
part::composite::message(mime::MIME::<mime::r#type::Message>::default())(input)
}
/// Only extract the headers of the email that are part of the Internet Message Format spec
@ -87,13 +87,13 @@ pub fn email(input: &[u8]) -> IResult<&[u8], part::composite::Message> {
/// This is the plain text body of the message. Note the blank line
/// between the header information and the body of the message."#;
///
/// let (_, header) = eml_codec::imf(input).unwrap();
/// let (_, imf) = eml_codec::imf(input).unwrap();
/// println!(
/// "{} just sent you an email with subject \"{}\"",
/// header.imf.from[0].to_string(),
/// header.imf.subject.unwrap().to_string(),
/// imf.from[0].to_string(),
/// imf.subject.unwrap().to_string(),
/// );
/// ```
pub fn imf(input: &[u8]) -> IResult<&[u8], imf::field::Header> {
pub fn imf(input: &[u8]) -> IResult<&[u8], imf::Imf> {
imf::field::imf(input)
}

View file

@ -8,7 +8,6 @@ use nom::{
use crate::header::{field_name};
use crate::imf::identification::{msg_id, MessageID};
use crate::mime::mechanism::{mechanism, Mechanism};
use crate::mime::{AnyMIMEWithDefault, WithDefaultType};
use crate::mime::r#type::{naive_type, NaiveType};
use crate::text::misc_token::{unstructured, Unstructured};
use crate::text::whitespace::obs_crlf;
@ -48,14 +47,10 @@ impl<'a> Content<'a> {
}
}
/*impl<'a> CompFieldList<'a, Content<'a>> {
pub fn to_mime<T: WithDefaultType> (self) -> AnyMIMEWithDefault<'a, T> {
self.known().into_iter().collect::<AnyMIMEWithDefault<T>>()
}
}*/
/*
pub fn to_mime<'a, T: WithDefaultType>(list: Vec<Content<'a>>) -> AnyMIMEWithDefault<'a, T> {
list.into_iter().collect::<AnyMIMEWithDefault<T>>()
}
}*/
pub fn content(input: &[u8]) -> IResult<&[u8], Content> {
terminated(

View file

@ -15,38 +15,38 @@ use std::marker::PhantomData;
use crate::imf::identification::MessageID;
use crate::mime::field::Content;
use crate::mime::mechanism::Mechanism;
use crate::mime::r#type::{self as ctype, AnyType};
use crate::mime::r#type::{AnyType, NaiveType};
use crate::header;
use crate::text::misc_token::Unstructured; //Multipart, Message, Text, Binary};
#[derive(Debug, PartialEq, Clone)]
pub struct Multipart<'a>(pub ctype::Multipart, pub Generic<'a>);
#[derive(Debug, PartialEq, Clone, Default)]
pub struct Message<'a>(pub ctype::Message, pub Generic<'a>);
#[derive(Debug, PartialEq, Clone, Default)]
pub struct Text<'a>(pub ctype::Text, pub Generic<'a>);
#[derive(Debug, PartialEq, Clone)]
pub struct Binary<'a>(pub ctype::Binary, pub Generic<'a>);
pub struct MIME<'a, T> {
pub interpreted: T,
pub parsed: NaiveMIME<'a>
}
impl<'a> Default for MIME<'a, r#type::Text> {
fn default() -> Self {
Self {
interpreted: r#type::Text::default(),
parsed: NaiveMIME::default(),
}
}
}
impl<'a> Default for MIME<'a, r#type::Message> {
fn default() -> Self {
Self {
interpreted: r#type::Message::default(),
parsed: NaiveMIME::default(),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum AnyMIME<'a> {
Mult(Multipart<'a>),
Msg(Message<'a>),
Txt(Text<'a>),
Bin(Binary<'a>),
}
impl<'a> AnyMIME<'a> {
pub fn from_pair(at: AnyType, gen: Generic<'a>) -> Self {
match at {
AnyType::Multipart(m) => AnyMIME::Mult(Multipart(m, gen)),
AnyType::Message(m) => AnyMIME::Msg(Message(m, gen)),
AnyType::Text(m) => AnyMIME::Txt(Text(m, gen)),
AnyType::Binary(m) => AnyMIME::Bin(Binary(m, gen)),
}
}
Mult(MIME<'a, r#type::Multipart>),
Msg(MIME<'a, r#type::Message>),
Txt(MIME<'a, r#type::Text>),
Bin(MIME<'a, r#type::Binary>),
}
impl<'a, T: WithDefaultType> From<AnyMIMEWithDefault<'a, T>> for AnyMIME<'a> {
@ -56,12 +56,46 @@ impl<'a, T: WithDefaultType> From<AnyMIMEWithDefault<'a, T>> for AnyMIME<'a> {
}
#[derive(Debug, PartialEq, Default, Clone)]
pub struct Generic<'a> {
pub struct NaiveMIME<'a> {
pub ctype: Option<NaiveType<'a>>,
pub transfer_encoding: Mechanism<'a>,
pub id: Option<MessageID<'a>>,
pub description: Option<Unstructured<'a>>,
pub header_ext: Vec<header::Kv<'a>>,
pub header_bad: Vec<&'a [u8]>,
}
impl<'a> FromIterator<Content<'a>> for NaiveMIME<'a> {
fn from_iter<I: IntoIterator<Item = Content<'a>>>(it: I) -> Self {
it.into_iter().fold(
NaiveMIME::default(),
|mut section, field| {
match field {
Content::Type(v) => section.ctype = Some(v),
Content::TransferEncoding(v) => section.transfer_encoding = v,
Content::ID(v) => section.id = Some(v),
Content::Description(v) => section.description = Some(v),
};
section
},
)
}
}
impl<'a> NaiveMIME<'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
}
pub fn to_interpreted<T: WithDefaultType>(self) -> AnyMIME<'a> {
self.ctype.as_ref().map(|c| c.to_type()).unwrap_or(T::default_type()).to_mime(self).into()
}
}
pub trait WithDefaultType {
fn default_type() -> AnyType;
}
@ -82,21 +116,8 @@ impl WithDefaultType for WithDigestDefault {
#[derive(Debug, PartialEq)]
pub struct AnyMIMEWithDefault<'a, T: WithDefaultType>(pub AnyMIME<'a>, PhantomData<T>);
impl<'a, T: WithDefaultType> FromIterator<Content<'a>> for AnyMIMEWithDefault<'a, T> {
fn from_iter<I: IntoIterator<Item = Content<'a>>>(it: I) -> Self {
let (at, gen) = it.into_iter().fold(
(T::default_type(), Generic::default()),
|(mut at, mut section), field| {
match field {
Content::Type(v) => at = v.to_type(),
Content::TransferEncoding(v) => section.transfer_encoding = v,
Content::ID(v) => section.id = Some(v),
Content::Description(v) => section.description = Some(v),
};
(at, section)
},
);
Self(AnyMIME::from_pair(at, gen), PhantomData)
impl<'a, T: WithDefaultType> Default for AnyMIMEWithDefault<'a, T> {
fn default() -> Self {
Self(T::default_type().to_mime(NaiveMIME::default()), PhantomData)
}
}

View file

@ -9,9 +9,10 @@ use nom::{
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)]
#[derive(Debug, PartialEq, Clone)]
pub struct NaiveType<'a> {
pub main: &'a [u8],
pub sub: &'a [u8],
@ -29,7 +30,7 @@ pub fn naive_type(input: &[u8]) -> IResult<&[u8], NaiveType> {
)(input)
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub struct Parameter<'a> {
pub name: &'a [u8],
pub value: MIMEWord<'a>,
@ -70,6 +71,17 @@ impl<'a> From<&'a NaiveType<'a>> for AnyType {
}
}
impl<'a> AnyType {
pub fn to_mime(self, parsed: NaiveMIME<'a>) -> AnyMIME<'a> {
match self {
Self::Multipart(interpreted) => AnyMIME::Mult(MIME::<Multipart> { interpreted, parsed }),
Self::Message(interpreted) => AnyMIME::Msg(MIME::<Message> { interpreted, parsed }),
Self::Text(interpreted) => AnyMIME::Txt(MIME::<Text> { interpreted, parsed }),
Self::Binary(interpreted) => AnyMIME::Bin(MIME::<Binary> { interpreted, parsed }),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Multipart {
pub subtype: MultipartSubtype,

View file

@ -9,7 +9,7 @@ use crate::text::boundary::{boundary, Delimiter};
//--- Multipart
#[derive(Debug, PartialEq)]
pub struct Multipart<'a> {
pub interpreted: mime::Multipart<'a>,
pub interpreted: mime::MIME<'a, mime::r#type::Multipart>,
pub children: Vec<AnyPart<'a>>,
pub preamble: &'a [u8],
pub epilogue: &'a [u8],
@ -22,12 +22,12 @@ impl<'a> Multipart<'a> {
}
pub fn multipart<'a>(
m: mime::Multipart<'a>,
m: mime::MIME<'a, mime::r#type::Multipart>,
) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], Multipart<'a>> {
let m = m.clone();
move |input| {
let bound = m.0.boundary.as_bytes();
let bound = m.interpreted.boundary.as_bytes();
let (mut input_loop, preamble) = part::part_raw(bound)(input)?;
let mut mparts: Vec<AnyPart> = vec![];
loop {
@ -57,11 +57,16 @@ pub fn multipart<'a>(
Ok((inp, Delimiter::Next)) => inp,
};
// parse mime headers
let (input, (known, unknown, bad)) = header(mime::field::content)(input)?;
let mime = match m.0.subtype {
mime::r#type::MultipartSubtype::Digest => mime::field::to_mime::<mime::WithDigestDefault>(known).into(),
_ => mime::field::to_mime::<mime::WithGenericDefault>(known).into(),
// parse mime headers, otherwise pick default mime
let (input, naive_mime) = match header(mime::field::content)(input) {
Ok((input, (known, unknown, bad))) => (input, known.into_iter().collect::<mime::NaiveMIME>().with_opt(unknown).with_bad(bad)),
Err(_) => (input, mime::NaiveMIME::default()),
};
// interpret mime according to context
let mime = match m.interpreted.subtype {
mime::r#type::MultipartSubtype::Digest => naive_mime.to_interpreted::<mime::WithDigestDefault>().into(),
_ => naive_mime.to_interpreted::<mime::WithGenericDefault>().into(),
};
// parse raw part
@ -70,6 +75,7 @@ pub fn multipart<'a>(
// parse mime body
mparts.push(part::to_anypart(mime, rpart));
input_loop = input;
}
}
@ -79,7 +85,7 @@ pub fn multipart<'a>(
#[derive(Debug, PartialEq)]
pub struct Message<'a> {
pub interpreted: mime::Message<'a>,
pub interpreted: mime::MIME<'a, mime::r#type::Message>,
pub imf: imf::Imf<'a>,
pub child: Box<AnyPart<'a>>,
pub epilogue: &'a [u8],
@ -92,13 +98,23 @@ impl<'a> Message<'a> {
}
pub fn message<'a>(
m: mime::Message<'a>,
m: mime::MIME<'a, mime::r#type::Message>,
) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], Message<'a>> {
move |input: &[u8]| {
// parse header fields
let (input, (known, unknown, bad)): (_, (Vec::<MixedField>, Vec<header::Kv>, Vec<&[u8]>)) =
header(part::field::mixed_field)(input)?;
let (in_mime, imf) = part::field::sections::<mime::WithGenericDefault>(known);
// aggregate header fields
let (naive_mime, imf) = part::field::sections(known);
// attach bad headers to imf
let imf = imf.with_opt(unknown).with_bad(bad);
// interpret headers to choose a mime type
let in_mime = naive_mime.to_interpreted::<mime::WithGenericDefault>().into();
// parse this mimetype
let part = part::to_anypart(in_mime, input);
Ok((
@ -119,18 +135,19 @@ mod tests {
use crate::part::discrete::Text;
use crate::part::AnyPart;
use crate::text::encoding::{Base64Word, EncodedWord, QuotedChunk, QuotedWord};
use crate::text::misc_token::{Phrase, UnstrToken, Unstructured, Word};
use crate::text::misc_token::{Phrase, UnstrToken, Unstructured, Word, MIMEWord};
use crate::text::quoted::QuotedString;
use chrono::{FixedOffset, TimeZone};
#[test]
fn test_multipart() {
let base_mime = mime::Multipart(
mime::r#type::Multipart {
let base_mime = mime::MIME {
interpreted: mime::r#type::Multipart {
subtype: mime::r#type::MultipartSubtype::Alternative,
boundary: "simple boundary".to_string(),
},
mime::Generic::default(),
);
parsed: mime::NaiveMIME::default(),
};
assert_eq!(
multipart(base_mime.clone())(b"This is the preamble. It is to be ignored, though it
@ -158,23 +175,35 @@ This is the epilogue. It is also to be ignored.
epilogue: &b""[..],
children: vec![
AnyPart::Txt(Text {
interpreted: mime::Text(
mime::r#type::Text {
interpreted: mime::MIME {
interpreted: mime::r#type::Text {
subtype: mime::r#type::TextSubtype::Plain,
charset: mime::charset::EmailCharset::US_ASCII,
},
mime::Generic::default(),
),
parsed: mime::NaiveMIME::default(),
},
body: &b"This is implicitly typed plain US-ASCII text.\nIt does NOT end with a linebreak."[..],
}),
AnyPart::Txt(Text {
interpreted: mime::Text(
mime::r#type::Text {
interpreted: mime::MIME {
interpreted: mime::r#type::Text {
subtype: mime::r#type::TextSubtype::Plain,
charset: mime::charset::EmailCharset::US_ASCII,
},
mime::Generic::default(),
),
parsed: mime::NaiveMIME {
ctype: Some(mime::r#type::NaiveType {
main: &b"text"[..],
sub: &b"plain"[..],
params: vec![
mime::r#type::Parameter {
name: &b"charset"[..],
value: MIMEWord::Atom(&b"us-ascii"[..]),
}
]
}),
..mime::NaiveMIME::default()
},
},
body: &b"This is explicitly typed plain US-ASCII text.\nIt DOES end with a linebreak.\n"[..],
}),
],
@ -230,7 +259,7 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
"#
.as_bytes();
let base_mime = mime::Message::default();
let base_mime = mime::MIME::<mime::r#type::Message>::default();
assert_eq!(
message(base_mime.clone())(fullmail),
Ok((
@ -301,40 +330,84 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
right: &b"www.grrrndzero.org"[..],
}),
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()
},
child: Box::new(AnyPart::Mult(Multipart {
interpreted: mime::Multipart(
mime::r#type::Multipart {
interpreted: mime::MIME {
interpreted: mime::r#type::Multipart {
subtype: mime::r#type::MultipartSubtype::Alternative,
boundary: "b1_e376dc71bafc953c0b0fdeb9983a9956".to_string(),
},
mime::Generic::default(),
),
parsed: mime::NaiveMIME {
ctype: Some(mime::r#type::NaiveType {
main: &b"multipart"[..],
sub: &b"alternative"[..],
params: vec![
mime::r#type::Parameter {
name: &b"boundary"[..],
value: MIMEWord::Quoted(QuotedString(vec![&b"b1_e376dc71bafc953c0b0fdeb9983a9956"[..]])),
}
]
}),
..mime::NaiveMIME::default()
},
},
preamble: &b"This is a multi-part message in MIME format.\n"[..],
epilogue: &b""[..],
children: vec![
AnyPart::Txt(Text {
interpreted: mime::Text(
mime::r#type::Text {
interpreted: mime::MIME {
interpreted: mime::r#type::Text {
subtype: mime::r#type::TextSubtype::Plain,
charset: mime::charset::EmailCharset::UTF_8,
},
mime::Generic {
parsed: mime::NaiveMIME {
ctype: Some(mime::r#type::NaiveType {
main: &b"text"[..],
sub: &b"plain"[..],
params: vec![
mime::r#type::Parameter {
name: &b"charset"[..],
value: MIMEWord::Atom(&b"utf-8"[..]),
}
]
}),
transfer_encoding: mime::mechanism::Mechanism::QuotedPrintable,
..mime::Generic::default()
..mime::NaiveMIME::default()
}
),
},
body: &b"GZ\nOoOoO\noOoOoOoOo\noOoOoOoOoOoOoOoOo\noOoOoOoOoOoOoOoOoOoOoOo\noOoOoOoOoOoOoOoOoOoOoOoOoOoOo\nOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\n"[..],
}),
AnyPart::Txt(Text {
interpreted: mime::Text(
mime::r#type::Text {
interpreted: mime::MIME {
interpreted: mime::r#type::Text {
subtype: mime::r#type::TextSubtype::Html,
charset: mime::charset::EmailCharset::US_ASCII,
},
mime::Generic::default(),
),
parsed: mime::NaiveMIME {
ctype: Some(mime::r#type::NaiveType {
main: &b"text"[..],
sub: &b"html"[..],
params: vec![
mime::r#type::Parameter {
name: &b"charset"[..],
value: MIMEWord::Atom(&b"us-ascii"[..]),
}
]
}),
..mime::NaiveMIME::default()
},
},
body: &br#"<div style="text-align: center;"><strong>GZ</strong><br />
OoOoO<br />
oOoOoOoOo<br />

View file

@ -4,7 +4,7 @@ use crate::mime;
#[derive(PartialEq)]
pub struct Text<'a> {
pub interpreted: mime::Text<'a>,
pub interpreted: mime::MIME<'a, mime::r#type::Text>,
pub body: &'a [u8],
}
@ -22,7 +22,7 @@ impl<'a> fmt::Debug for Text<'a> {
#[derive(PartialEq)]
pub struct Binary<'a> {
pub interpreted: mime::Binary<'a>,
pub interpreted: mime::MIME<'a, mime::r#type::Binary>,
pub body: &'a [u8],
}

View file

@ -35,11 +35,11 @@ impl<'a> MixedField<'a> {
}
}
pub fn sections<'a, T: mime::WithDefaultType>(list: Vec<MixedField<'a>>) -> (mime::AnyMIME<'a>, imf::Imf<'a>) {
pub fn sections<'a>(list: Vec<MixedField<'a>>) -> (mime::NaiveMIME<'a>, imf::Imf<'a>) {
let (v1, v2): (Vec<MixedField>, Vec<_>) = list.into_iter().partition(|v| v.mime().is_some());
let mime = v1.into_iter().flat_map(MixedField::to_mime).collect::<mime::AnyMIMEWithDefault<T>>();
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.into(), imf)
(mime, imf)
}
pub fn mixed_field(input: &[u8]) -> IResult<&[u8], MixedField> {

View file

@ -65,13 +65,13 @@ pub fn to_anypart<'a>(m: AnyMIME<'a>, rpart: &'a [u8]) -> AnyPart<'a> {
AnyMIME::Mult(a) => multipart(a)(rpart)
.map(|(rest, multi)| AnyPart::Mult(multi.with_epilogue(rest)))
.unwrap_or(AnyPart::Txt(Text {
interpreted: mime::Text::default(),
interpreted: mime::MIME::<mime::r#type::Text>::default(),
body: rpart,
})),
AnyMIME::Msg(a) => message(a)(rpart)
.map(|(rest, msg)| AnyPart::Msg(msg.with_epilogue(rest)))
.unwrap_or(AnyPart::Txt(Text {
interpreted: mime::Text::default(),
interpreted: mime::MIME::<mime::r#type::Text>::default(),
body: rpart,
})),
AnyMIME::Txt(a) => AnyPart::Txt(Text {

View file

@ -23,7 +23,7 @@ pub fn phrase_list(input: &[u8]) -> IResult<&[u8], PhraseList> {
map(separated_list1(tag(","), phrase), PhraseList)(input)
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub enum MIMEWord<'a> {
Quoted(QuotedString<'a>),
Atom(&'a [u8]),

View file

@ -10,7 +10,7 @@ use nom::{
use crate::text::ascii;
use crate::text::whitespace::{cfws, fws, is_obs_no_ws_ctl};
#[derive(Debug, PartialEq, Default)]
#[derive(Debug, PartialEq, Default, Clone)]
pub struct QuotedString<'a>(pub Vec<&'a [u8]>);
impl<'a> QuotedString<'a> {