Compare commits

...

26 commits
0.1.0 ... main

Author SHA1 Message Date
a7bd3c475a
fix mime() method 2023-09-18 18:08:07 +02:00
df0c6fa34f Merge pull request 'expose naive fields' (#27) from fetch-mime-from-anypart into main
Reviewed-on: #27
2023-09-18 16:07:16 +00:00
7920955ee5 Merge branch 'main' into fetch-mime-from-anypart 2023-09-18 16:07:07 +00:00
0a7179a17c
expose naive fields 2023-09-18 18:06:37 +02:00
303fdc3e91 Merge pull request 'attach to child' (#26) from header2 into main
Reviewed-on: #26
2023-09-18 15:25:55 +00:00
f5f8b8e018
attach to child 2023-09-18 17:24:58 +02:00
8b17af73fa Merge pull request 'always attach headers to naivemime' (#25) from headers_kv2 into main
Reviewed-on: #25
2023-09-18 14:38:47 +00:00
9eb44b03f7
always attach headers to naivemime 2023-09-18 16:38:04 +02:00
5ccc212d15 Merge pull request 'Access headers as key/values' (#24) from headers_map into main
Reviewed-on: #24
2023-08-30 17:50:25 +00:00
d9285c9ddf
format code 2023-08-30 19:49:04 +02:00
2529b0145e
fixed tests! 2023-08-30 19:48:23 +02:00
9b828ad6ad
better debug 2023-08-30 19:30:10 +02:00
18bb04340a
refactor headers 2023-08-30 19:00:08 +02:00
d9cf6b225d
fix raw mime test 2/2 2023-08-30 13:31:24 +02:00
628fbc507d
fix raw mime test 1/2 2023-08-30 11:46:23 +02:00
dfb5b9fe0f
refactor imf parsing 2023-08-30 11:35:46 +02:00
ba59b037ef
add an header kv function 2023-08-30 11:35:29 +02:00
5cff5510ac Merge pull request 'add a raw field to mime' (#22) from better_access_to_bytes into main
Reviewed-on: #22
2023-08-16 14:18:51 +00:00
8aa23ac5f2
add a raw field to mime 2023-08-16 16:15:57 +02:00
32ca628358
prepare v0.1.1 release 2023-07-25 18:33:00 +02:00
b64c032bff
add compatibility for \r\r\n 2023-07-25 18:27:19 +02:00
987024430b
collect raw stuff 2023-07-25 16:20:36 +02:00
91fa0d38c3
implement to_string for some types 2023-07-25 14:39:30 +02:00
64407b6bee
Add a to_string for mechanism 2023-07-25 14:06:40 +02:00
6e3b12c11a
add info about deductible fields 2023-07-25 14:00:01 +02:00
7b7d9de92d
improve cargo.toml 2023-07-24 22:14:51 +02:00
21 changed files with 850 additions and 442 deletions

2
Cargo.lock generated
View file

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

View file

@ -1,9 +1,16 @@
[package]
name = "eml-codec"
version = "0.1.0"
version = "0.1.2"
edition = "2021"
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
[lib]
@ -23,3 +30,4 @@ encoding_rs = "0.8"
[dev-dependencies]
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
between the header information and the body of the message."#;
let (_, email) = eml_codec::email(input).unwrap();
let (_, email) = eml_codec::parse_message(input).unwrap();
println!(
"{} just sent you an email with subject \"{}\"",
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."#;
// if you are only interested in email metadata/headers
let (_, imf) = eml_codec::imf(input).unwrap();
let (_, imf) = eml_codec::parse_imf(input).unwrap();
println!(
"{} just sent you an email with subject \"{}\"",
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
let (_, email) = eml_codec::email(input).unwrap();
let (_, email) = eml_codec::parse_message(input).unwrap();
println!(
"{} raw message is:\n{}",
email.imf.from[0].to_string(),

View file

@ -1,55 +1,62 @@
use crate::text::misc_token::{unstructured, Unstructured};
use crate::text::whitespace::{foldable_line, obs_crlf};
use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case, take_while1},
bytes::complete::{tag, take_while1},
character::complete::space0,
combinator::map,
multi::{fold_many0},
combinator::{into, recognize},
multi::many0,
sequence::{pair, terminated, tuple},
IResult,
};
use std::fmt;
#[derive(Debug, PartialEq)]
pub enum CompField<'a, T> {
Known(T),
Unknown(Kv<'a>),
Bad(&'a [u8]),
use crate::text::misc_token::unstructured;
use crate::text::whitespace::{foldable_line, obs_crlf};
#[derive(PartialEq, Clone)]
pub struct Kv2<'a>(pub &'a [u8], pub &'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)
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Kv<'a>(pub &'a [u8], pub Unstructured<'a>);
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(
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,
)(input)
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()
}
}
pub fn field_name<'a>(name: &'static [u8]) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], &'a [u8]> {
move |input| terminated(tag_no_case(name), tuple((space0, tag(b":"), space0)))(input)
#[derive(Debug, PartialEq, Clone)]
pub enum Field<'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>> {
terminated(
many0(alt((into(correct_field), into(foldable_line)))),
obs_crlf,
)(input)
}
pub fn field_any(input: &[u8]) -> IResult<&[u8], &[u8]> {
terminated(
take_while1(|c| (0x21..=0x7E).contains(&c) && c != 0x3A),
tuple((space0, tag(b":"), space0)),
)(input)
}
/// Optional field
@ -61,15 +68,6 @@ pub fn field_name<'a>(name: &'static [u8]) -> impl Fn(&'a [u8]) -> IResult<&'a [
/// %d59-126 ; characters not including
/// ; ":".
/// ```
pub fn opt_field(input: &[u8]) -> IResult<&[u8], (&[u8], Unstructured)> {
terminated(
pair(
terminated(
take_while1(|c| (0x21..=0x7E).contains(&c) && c != 0x3A),
tuple((space0, tag(b":"), space0)),
),
unstructured,
),
obs_crlf,
)(input)
pub fn correct_field(input: &[u8]) -> IResult<&[u8], Kv2> {
terminated(into(pair(field_any, recognize(unstructured))), obs_crlf)(input)
}

View file

@ -1,21 +1,14 @@
use chrono::{DateTime, FixedOffset};
use nom::{
branch::alt,
combinator::map,
sequence::{preceded, terminated},
IResult,
};
use nom::combinator::map;
use crate::header::{field_name, header};
use crate::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};
use crate::imf::mailbox::{mailbox, AddrSpec, MailboxList, MailboxRef};
use crate::imf::mime::{version, Version};
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::whitespace::obs_crlf;
#[derive(Debug, PartialEq)]
pub enum Field<'a> {
@ -49,94 +42,34 @@ pub enum Field<'a> {
MIMEVersion(Version),
}
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()
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(()))
}
}

View file

@ -1,5 +1,4 @@
/// Parse and represent IMF (Internet Message Format) headers (RFC822, RFC5322)
pub mod address;
pub mod datetime;
pub mod field;
@ -8,13 +7,15 @@ pub mod mailbox;
pub mod mime;
pub mod trace;
use nom::{combinator::map, IResult};
use crate::header;
use crate::imf::address::AddressRef;
use crate::imf::field::Field;
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};
@ -50,19 +51,6 @@ 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,
@ -92,3 +80,65 @@ 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,7 +15,10 @@ pub mod header;
/// Low-level email-specific text-based representation for data
pub mod text;
use nom::IResult;
/// Manipulate buffer of bytes
mod pointers;
use nom::{combinator::into, IResult};
/// Parse a whole email including its (MIME) body
///
@ -34,7 +37,7 @@ use nom::IResult;
/// * `msg` - The parsed message
///
/// # Examples
///
///
/// ```
/// let input = br#"Date: 7 Mar 2023 08:00:00 +0200
/// From: deuxfleurs@example.com
@ -46,15 +49,17 @@ use nom::IResult;
/// This is the plain text body of the message. Note the blank line
/// between the header information and the body of the message."#;
///
/// let (_, email) = eml_codec::email(input).unwrap();
/// let (_, email) = eml_codec::parse_message(input).unwrap();
/// println!(
/// "{} raw message is:\n{}",
/// email.imf.from[0].to_string(),
/// String::from_utf8_lossy(email.child.as_text().unwrap().body),
/// );
/// ```
pub fn email(input: &[u8]) -> IResult<&[u8], part::composite::Message> {
part::composite::message(mime::MIME::<mime::r#type::Message>::default())(input)
pub fn parse_message(input: &[u8]) -> IResult<&[u8], part::composite::Message> {
into(part::composite::message(mime::MIME::<
mime::r#type::DeductibleMessage,
>::default()))(input)
}
/// Only extract the headers of the email that are part of the Internet Message Format spec
@ -87,13 +92,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 (_, imf) = eml_codec::imf(input).unwrap();
/// let (_, imf) = eml_codec::parse_imf(input).unwrap();
/// println!(
/// "{} just sent you an email with subject \"{}\"",
/// imf.from[0].to_string(),
/// imf.subject.unwrap().to_string(),
/// );
/// ```
pub fn imf(input: &[u8]) -> IResult<&[u8], imf::Imf> {
imf::field::imf(input)
pub fn parse_imf(input: &[u8]) -> IResult<&[u8], imf::Imf> {
imf::imf(input)
}

View file

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

View file

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

View file

@ -18,6 +18,20 @@ pub enum Mechanism<'a> {
Base64,
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> {
use Mechanism::*;

View file

@ -1,4 +1,4 @@
/// Parsed and represent an email character set
/// Parsed and represent an email character set
pub mod charset;
/// MIME specific headers
@ -10,33 +10,34 @@ pub mod mechanism;
/// Content-Type representation
pub mod r#type;
use std::fmt;
use std::marker::PhantomData;
use crate::header;
use crate::imf::identification::MessageID;
use crate::mime::field::Content;
use crate::mime::mechanism::Mechanism;
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 MIME<'a, T> {
pub interpreted: T,
pub parsed: NaiveMIME<'a>
pub interpreted_type: T,
pub fields: NaiveMIME<'a>,
}
impl<'a> Default for MIME<'a, r#type::Text> {
impl<'a> Default for MIME<'a, r#type::DeductibleText> {
fn default() -> Self {
Self {
interpreted: r#type::Text::default(),
parsed: NaiveMIME::default(),
interpreted_type: r#type::DeductibleText::default(),
fields: NaiveMIME::default(),
}
}
}
impl<'a> Default for MIME<'a, r#type::Message> {
impl<'a> Default for MIME<'a, r#type::DeductibleMessage> {
fn default() -> Self {
Self {
interpreted: r#type::Message::default(),
parsed: NaiveMIME::default(),
interpreted_type: r#type::DeductibleMessage::default(),
fields: NaiveMIME::default(),
}
}
}
@ -44,10 +45,20 @@ impl<'a> Default for MIME<'a, r#type::Message> {
#[derive(Debug, PartialEq, Clone)]
pub enum AnyMIME<'a> {
Mult(MIME<'a, r#type::Multipart>),
Msg(MIME<'a, r#type::Message>),
Txt(MIME<'a, r#type::Text>),
Msg(MIME<'a, r#type::DeductibleMessage>),
Txt(MIME<'a, r#type::DeductibleText>),
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> {
fn from(a: AnyMIMEWithDefault<'a, T>) -> Self {
@ -55,21 +66,32 @@ impl<'a, T: WithDefaultType> From<AnyMIMEWithDefault<'a, T>> for AnyMIME<'a> {
}
}
#[derive(Debug, PartialEq, Default, Clone)]
#[derive(PartialEq, Default, Clone)]
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]>,
pub kv: Vec<header::Field<'a>>,
pub raw: &'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> {
fn from_iter<I: IntoIterator<Item = Content<'a>>>(it: I) -> Self {
it.into_iter().fold(
NaiveMIME::default(),
|mut section, field| {
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,
@ -77,25 +99,29 @@ impl<'a> FromIterator<Content<'a>> for NaiveMIME<'a> {
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_kv(mut self, fields: Vec<header::Field<'a>>) -> Self {
self.kv = fields;
self
}
pub fn with_bad(mut self, bad: Vec<&'a [u8]>) -> Self {
self.header_bad = bad; self
pub fn with_raw(mut self, raw: &'a [u8]) -> Self {
self.raw = raw;
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()
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;
}
@ -103,13 +129,13 @@ pub trait WithDefaultType {
pub struct WithGenericDefault {}
impl WithDefaultType for WithGenericDefault {
fn default_type() -> AnyType {
AnyType::Text(r#type::Text::default())
AnyType::Text(r#type::DeductibleText::default())
}
}
pub struct WithDigestDefault {}
impl WithDefaultType for WithDigestDefault {
fn default_type() -> AnyType {
AnyType::Message(r#type::Message::default())
AnyType::Message(r#type::DeductibleMessage::default())
}
}

View file

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