implement mime headers

This commit is contained in:
Quentin 2023-07-14 10:43:31 +02:00
parent 4e82941fd0
commit 6b3343f137
Signed by: quentin
GPG key ID: E9602264D639FF68
5 changed files with 131 additions and 24 deletions

View file

@ -17,8 +17,8 @@ use crate::fragments::quoted::quoted_string;
#[derive(Debug, PartialEq)]
pub struct Version {
major: u32,
minor: u32,
pub major: u32,
pub minor: u32,
}
#[derive(Debug, PartialEq)]
@ -40,9 +40,9 @@ pub enum Type<'a> {
#[derive(Debug, PartialEq)]
pub struct MultipartDesc<'a> {
boundary: String,
subtype: MultipartSubtype<'a>,
unknown_parameters: Vec<Parameter<'a>>,
pub boundary: String,
pub subtype: MultipartSubtype<'a>,
pub unknown_parameters: Vec<Parameter<'a>>,
}
#[derive(Debug, PartialEq)]
@ -57,8 +57,8 @@ pub enum MultipartSubtype<'a> {
#[derive(Debug, PartialEq)]
pub struct MessageDesc<'a> {
subtype: MessageSubtype<'a>,
unknown_parameters: Vec<Parameter<'a>>,
pub subtype: MessageSubtype<'a>,
pub unknown_parameters: Vec<Parameter<'a>>,
}
#[derive(Debug, PartialEq)]
@ -71,9 +71,9 @@ pub enum MessageSubtype<'a> {
#[derive(Debug, PartialEq)]
pub struct TextDesc<'a> {
charset: Option<EmailCharset<'a>>,
subtype: TextSubtype<'a>,
unknown_parameters: Vec<Parameter<'a>>,
pub charset: Option<EmailCharset<'a>>,
pub subtype: TextSubtype<'a>,
pub unknown_parameters: Vec<Parameter<'a>>,
}
#[derive(Debug, PartialEq)]

View file

@ -2,7 +2,7 @@ use nom::{
branch::alt,
bytes::complete::{tag, take_while1},
character::complete::space0,
combinator::{into, opt},
combinator::{into, map, opt},
multi::{many0, many1, separated_list1},
sequence::tuple,
IResult,
@ -14,6 +14,7 @@ use crate::fragments::lazy;
use crate::fragments::quoted::quoted_string;
use crate::fragments::whitespace::{fws, is_obs_no_ws_ctl};
use crate::fragments::words::{atom, is_vchar};
use crate::fragments::encoding::encoded_word;
#[derive(Debug, PartialEq, Default)]
pub struct Unstructured(pub String);
@ -47,7 +48,7 @@ impl<'a> TryFrom<&'a lazy::PhraseList<'a>> for PhraseList {
/// word = atom / quoted-string
/// ```
pub fn word(input: &str) -> IResult<&str, Cow<str>> {
alt((into(quoted_string), into(atom)))(input)
alt((into(quoted_string), into(encoded_word), into(atom)))(input)
}
/// Phrase
@ -70,31 +71,46 @@ fn is_unstructured(c: char) -> bool {
is_vchar(c) || is_obs_no_ws_ctl(c) || c == '\x00'
}
enum UnstrToken {
Init,
Encoded,
Plain,
}
/// Unstructured header field body
///
/// ```abnf
/// unstructured = (*([FWS] VCHAR_SEQ) *WSP) / obs-unstruct
/// ```
pub fn unstructured(input: &str) -> IResult<&str, String> {
let (input, r) = many0(tuple((opt(fws), take_while1(is_unstructured))))(input)?;
let (input, r) = many0(tuple((opt(fws), alt((
map(encoded_word, |v| (Cow::Owned(v), UnstrToken::Encoded)),
map(take_while1(is_unstructured), |v| (Cow::Borrowed(v), UnstrToken::Plain)),
)))))(input)?;
let (input, _) = space0(input)?;
// Try to optimize for the most common cases
let body = match r.as_slice() {
[(None, content)] => content.to_string(),
[(Some(_), content)] => " ".to_string() + content,
lines => lines.iter().fold(String::with_capacity(255), |acc, item| {
let (may_ws, content) = item;
match may_ws {
Some(_) => acc + " " + content,
None => acc + content,
}
}),
// Optimization when there is only one line
[(None, (content, _))] | [(_, (content, UnstrToken::Encoded))] => content.to_string(),
[(Some(_), (content, _))] => " ".to_string() + content,
// Generic case, with multiple lines
lines => lines.iter().fold(
(&UnstrToken::Init, String::with_capacity(255)),
|(prev_token, result), (may_ws, (content, current_token))| {
let new_res = match (may_ws, prev_token, current_token) {
(_, UnstrToken::Encoded, UnstrToken::Encoded) | (None, _, _) => result + content,
_ => result + " " + content,
};
(current_token, new_res)
}).1,
};
Ok((input, body))
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -3,6 +3,7 @@ use std::collections::HashMap;
use crate::fragments::eager::Field;
use crate::fragments::lazy;
use crate::fragments::misc_token::{PhraseList, Unstructured};
use crate::fragments::mime::{Version,Type,Mechanism};
use crate::fragments::model::{AddressRef, MailboxRef, MessageId};
use crate::fragments::trace::ReceivedLog;
use chrono::{DateTime, FixedOffset};
@ -40,6 +41,13 @@ pub struct Section<'a> {
// 3.6.8. Optional Fields
pub optional: HashMap<&'a str, &'a Unstructured>,
// MIME
pub mime_version: Option<&'a Version>,
pub content_type: Option<&'a Type<'a>>,
pub content_transfer_encoding: Option<&'a Mechanism<'a>>,
pub content_id: Option<&'a MessageId<'a>>,
pub content_description: Option<&'a Unstructured>,
// Recovery
pub bad_fields: Vec<&'a lazy::Field<'a>>,
pub unparsed: Vec<&'a str>,
@ -71,7 +79,11 @@ impl<'a> FromIterator<&'a Field<'a>> for Section<'a> {
section.optional.insert(k, v);
}
Field::Rescue(v) => section.unparsed.push(v),
_ => todo!(),
Field::MIMEVersion(v) => section.mime_version = Some(v),
Field::ContentType(v) => section.content_type = Some(v),
Field::ContentTransferEncoding(v) => section.content_transfer_encoding = Some(v),
Field::ContentID(v) => section.content_id = Some(v),
Field::ContentDescription(v) => section.content_description = Some(v),
}
}
section

View file

@ -8,6 +8,7 @@ use nom::{
sequence::tuple,
IResult,
};
use crate::fragments::encoding::encoded_word;
// --- whitespaces and comments
@ -75,7 +76,7 @@ pub fn comment(input: &str) -> IResult<&str, ()> {
}
pub fn ccontent(input: &str) -> IResult<&str, &str> {
alt((recognize(ctext), recognize(quoted_pair), recognize(comment)))(input)
alt((recognize(ctext), recognize(quoted_pair), recognize(encoded_word), recognize(comment)))(input)
}
pub fn ctext(input: &str) -> IResult<&str, char> {
@ -155,4 +156,12 @@ mod tests {
Ok(("wouch", "(double (comment) is fun) "))
);
}
#[test]
fn test_cfws_encoded_word() {
assert_eq!(
cfws("(=?US-ASCII?Q?Keith_Moore?=)"),
Ok(("", "(=?US-ASCII?Q?Keith_Moore?=)")),
);
}
}

View file

@ -172,7 +172,77 @@ This is a reply to your hello.
"Héron: Raté\n Raté raté\n",
"Not a real header but should still recover\n",
],
..section::Section::default()
}
)
})
}
#[test]
fn test_headers_mime() {
use imf_codec::fragments::mime;
let fullmail: &[u8] = r#"From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>
To: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>
CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>
Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=
=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=
MIME-Version: 1.0
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
Content-ID: <a@example.com>
Content-Description: hello
Now's the time =
for all folk to come=
to the aid of their country.
"#
.as_bytes();
parser(fullmail, |parsed_section| {
assert_eq!(
parsed_section,
&section::Section {
from: vec![
&model::MailboxRef {
name: Some("Keith Moore".into()),
addrspec: model::AddrSpec {
local_part: "moore".into(),
domain: "cs.utk.edu".into(),
}
},
],
to: vec![&model::AddressRef::Single(model::MailboxRef {
name: Some("Keld Jørn Simonsen".into()),
addrspec: model::AddrSpec {
local_part: "keld".into(),
domain: "dkuug.dk".into(),
}
})],
cc: vec![&model::AddressRef::Single(model::MailboxRef {
name: Some("André Pirard".into()),
addrspec: model::AddrSpec {
local_part: "PIRARD".into(),
domain: "vm1.ulg.ac.be".into(),
}
})],
subject: Some(&misc_token::Unstructured("If you can read this you understand the example.".into())),
mime_version: Some(&mime::Version{ major: 1, minor: 0 }),
content_type: Some(&mime::Type::Text(mime::TextDesc {
charset: Some(mime::EmailCharset::ISO_8859_1),
subtype: mime::TextSubtype::Plain,
unknown_parameters: vec![]
})),
content_transfer_encoding: Some(&mime::Mechanism::QuotedPrintable),
content_id: Some(&model::MessageId {
left: "a",
right: "example.com"
}),
content_description: Some(&misc_token::Unstructured("hello".into())),
..section::Section::default()
}
);
})
}