drop my early implementation of trace

This commit is contained in:
Quentin 2023-06-16 09:58:07 +02:00
parent b6c25a4676
commit 486ccf2de0
Signed by: quentin
GPG key ID: E9602264D639FF68
8 changed files with 494 additions and 735 deletions

View file

@ -19,3 +19,4 @@ Targeted RFC
|2048 | ↳ Multipurpose Internet Mail Extensions (MIME) Part Four: Registration Procedures | |2048 | ↳ Multipurpose Internet Mail Extensions (MIME) Part Four: Registration Procedures |
|2049 | ↳ Multipurpose Internet Mail Extensions (MIME) Part Five: Conformance Criteria and Examples | |2049 | ↳ Multipurpose Internet Mail Extensions (MIME) Part Five: Conformance Criteria and Examples |
|6532 | Internationalized Email Headers | |6532 | Internationalized Email Headers |
|9228 | Delivered-To Email Header Field |

View file

@ -1,428 +0,0 @@
use chrono::DateTime;
use nom::{
IResult,
branch::alt,
bytes::complete::take_while1,
bytes::complete::tag,
character::complete::space0,
combinator::opt,
multi::{many0, many1, fold_many0, separated_list1},
sequence::{terminated, preceded, pair, tuple},
};
use crate::whitespace::{fws, perm_crlf};
use crate::words::vchar_seq;
use crate::misc_token::{phrase, unstructured};
use crate::model::{CommonFields, HeaderDate, MailboxRef, AddressRef};
use crate::mailbox::mailbox;
use crate::address::{mailbox_list, address_list, address_list_cfws};
use crate::identification::msg_id;
use crate::model;
/// HEADERS
/// Header section
///
/// See: https://www.rfc-editor.org/rfc/rfc5322.html#section-2.2
pub fn section(input: &str) -> IResult<&str, CommonFields> {
let (input, headers) = fold_many0(
alt((header_field, unknown_field)),
CommonFields::default,
|mut section, head| {
match head {
//@FIXME min and max limits are not enforced,
// it may result in missing data or silently overriden data.
// 3.6.1. The Origination Date Field
HeaderField::Date(d) => {
// | orig-date | 1 | 1 | |
section.date = d;
}
// 3.6.2. Originator Fields
HeaderField::From(v) => {
// | from | 1 | 1 | See sender and 3.6.2 |
section.from = v;
}
HeaderField::Sender(mbx) => {
// | sender | 0* | 1 | MUST occur with multi-address from - see 3.6.2 |
section.sender = Some(mbx);
}
HeaderField::ReplyTo(addr_list) => {
// | reply-to | 0 | 1 | |
section.reply_to = addr_list;
}
// 3.6.3. Destination Address Fields
HeaderField::To(addr_list) => {
// | to | 0 | 1 | |
section.to = addr_list;
}
HeaderField::Cc(addr_list) => {
// | cc | 0 | 1 | |
section.cc = addr_list;
}
HeaderField::Bcc(addr_list) => {
// | bcc | 0 | 1 | |
section.bcc = addr_list;
}
// 3.6.4. Identification Fields
HeaderField::MessageID(msg_id) => {
// | message-id | 0* | 1 | SHOULD be present - see 3.6.4 |
section.msg_id = Some(msg_id);
}
HeaderField::InReplyTo(id_list) => {
// | in-reply-to | 0* | 1 | SHOULD occur in some replies - see 3.6.4 |
section.in_reply_to = id_list;
}
HeaderField::References(id_list) => {
// | in-reply-to | 0* | 1 | SHOULD occur in some replies - see 3.6.4 |
section.references = id_list;
}
// 3.6.5. Informational Fields
HeaderField::Subject(title) => {
// | subject | 0 | 1 | |
section.subject = Some(title);
}
HeaderField::Comments(coms) => {
// | comments | 0 | unlimited | |
section.comments.push(coms);
}
HeaderField::Keywords(mut kws) => {
// | keywords | 0 | unlimited | |
section.keywords.append(&mut kws);
}
// 3.6.8. Optional Fields
HeaderField::Optional(name, body) => {
section.optional.insert(name, body);
}
};
section
}
)(input)?;
let (input, _) = perm_crlf(input)?;
Ok((input, headers))
}
#[derive(Debug, PartialEq)]
pub enum HeaderField<'a> {
// 3.6.1. The Origination Date Field
Date(HeaderDate),
// 3.6.2. Originator Fields
From(Vec<MailboxRef>),
Sender(MailboxRef),
ReplyTo(Vec<AddressRef>),
// 3.6.3. Destination Address Fields
To(Vec<AddressRef>),
Cc(Vec<AddressRef>),
Bcc(Vec<AddressRef>),
// 3.6.4. Identification Fields
MessageID(model::MessageId<'a>),
InReplyTo(Vec<model::MessageId<'a>>),
References(Vec<model::MessageId<'a>>),
// 3.6.5. Informational Fields
Subject(String),
Comments(String),
Keywords(Vec<String>),
// 3.6.8. Optional Fields
Optional(&'a str, String)
}
/// Parse one known header field
///
/// RFC5322 optional-field seems to be a generalization of the field terminology.
/// We use it to parse all header names:
pub fn header_field(input: &str) -> IResult<&str, HeaderField> {
terminated(
alt((
// 3.6.1. The Origination Date Field
date,
// 3.6.2. Originator Fields
alt((from, sender, reply_to)),
// 3.6.3. Destination Address Fields
alt((to, cc, bcc)),
// 3.6.4. Identification Fields
alt((msg_id_field, in_reply_to, references)),
// 3.6.5. Informational Fields
alt((subject, comments, keywords)),
)),
perm_crlf,
)(input)
}
// 3.6.1. The Origination Date Field
fn date(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Date:"), space0), datetime)(input)?;
Ok((input, HeaderField::Date(body)))
}
// 3.6.2. Originator Fields
fn from(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("From:"), space0), mailbox_list)(input)?;
Ok((input, HeaderField::From(body)))
}
fn sender(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Sender:"), space0), mailbox)(input)?;
Ok((input, HeaderField::Sender(body)))
}
fn reply_to(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Reply-To:"), space0), address_list)(input)?;
Ok((input, HeaderField::ReplyTo(body)))
}
// 3.6.3. Destination Address Fields
fn to(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("To:"), space0), address_list)(input)?;
Ok((input, HeaderField::To(body)))
}
fn cc(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Cc:"), space0), address_list)(input)?;
Ok((input, HeaderField::Cc(body)))
}
fn bcc(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(
pair(tag("Bcc:"), space0),
opt(alt((address_list, address_list_cfws))),
)(input)?;
Ok((input, HeaderField::Bcc(body.unwrap_or(vec![]))))
}
// 3.6.4. Identification Fields
fn msg_id_field(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Message-ID:"), space0), msg_id)(input)?;
Ok((input, HeaderField::MessageID(body)))
}
fn in_reply_to(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("In-Reply-To:"), space0), many1(msg_id))(input)?;
Ok((input, HeaderField::InReplyTo(body)))
}
fn references(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("References:"), space0), many1(msg_id))(input)?;
Ok((input, HeaderField::References(body)))
}
// 3.6.5. Informational Fields
fn subject(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Subject:"), space0), unstructured)(input)?;
Ok((input, HeaderField::Subject(body)))
}
fn comments(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Comments:"), space0), unstructured)(input)?;
Ok((input, HeaderField::Comments(body)))
}
fn keywords(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(
pair(tag("Keywords:"), space0),
separated_list1(tag(","), phrase),
)(input)?;
Ok((input, HeaderField::Keywords(body)))
}
/// Optional field
///
/// ```abnf
/// field = field-name ":" unstructured CRLF
/// field-name = 1*ftext
/// ftext = %d33-57 / ; Printable US-ASCII
/// %d59-126 ; characters not including
/// ; ":".
/// ```
fn unknown_field(input: &str) -> IResult<&str, HeaderField> {
// Extract field name
let (input, field_name) = field_name(input)?;
let (input, body) = unstructured(input)?;
Ok((input, HeaderField::Optional(field_name, body)))
}
pub fn field_name(input: &str) -> IResult<&str, &str> {
terminated(
take_while1(|c| c >= '\x21' && c <= '\x7E' && c != '\x3A'),
pair(tag(":"), space0)
)(input)
}
pub fn datetime(input: &str) -> IResult<&str, HeaderDate> {
// @FIXME want to extract datetime our way in the future
// to better handle obsolete/bad cases instead of returning raw text.
let (input, raw_date) = unstructured(input)?;
match DateTime::parse_from_rfc2822(&raw_date) {
Ok(chronodt) => Ok((input, HeaderDate::Parsed(chronodt))),
Err(e) => Ok((input, HeaderDate::Unknown(raw_date, e))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{GroupRef, AddrSpec};
// 3.6.1. The Origination Date Field
#[test]
fn test_datetime() {
let datefield = "Date: Thu,\r\n 13\r\n Feb\r\n 1969\r\n 23:32\r\n -0330 (Newfoundland Time)\r\n";
let (input, v) = header_field(datefield).unwrap();
assert_eq!(input, "");
match v {
HeaderField::Date(HeaderDate::Parsed(_)) => (),
_ => panic!("Date has not been parsed"),
};
}
// 3.6.2. Originator Fields
#[test]
fn test_from() {
assert_eq!(
header_field("From: \"Joe Q. Public\" <john.q.public@example.com>\r\n"),
Ok(("", HeaderField::From(vec![MailboxRef {
name: Some("Joe Q. Public".into()),
addrspec: AddrSpec {
local_part: "john.q.public".into(),
domain: "example.com".into(),
}
}]))),
);
}
#[test]
fn test_sender() {
assert_eq!(
header_field("Sender: Michael Jones <mjones@machine.example>\r\n"),
Ok(("", HeaderField::Sender(MailboxRef {
name: Some("Michael Jones".into()),
addrspec: AddrSpec {
local_part: "mjones".into(),
domain: "machine.example".into(),
},
}))),
);
}
#[test]
fn test_reply_to() {
assert_eq!(
header_field("Reply-To: \"Mary Smith: Personal Account\" <smith@home.example>\r\n"),
Ok(("", HeaderField::ReplyTo(vec![AddressRef::Single(MailboxRef {
name: Some("Mary Smith: Personal Account".into()),
addrspec: AddrSpec {
local_part: "smith".into(),
domain: "home.example".into(),
},
})])))
);
}
// 3.6.3. Destination Address Fields
#[test]
fn test_to() {
assert_eq!(
header_field("To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;\r\n"),
Ok(("", HeaderField::To(vec![AddressRef::Many(GroupRef {
name: "A Group".into(),
participants: vec![
MailboxRef {
name: Some("Ed Jones".into()),
addrspec: AddrSpec { local_part: "c".into(), domain: "a.test".into() },
},
MailboxRef {
name: None,
addrspec: AddrSpec { local_part: "joe".into(), domain: "where.test".into() },
},
MailboxRef {
name: Some("John".into()),
addrspec: AddrSpec { local_part: "jdoe".into(), domain: "one.test".into() },
},
]
})])))
);
}
#[test]
fn test_cc() {
assert_eq!(
header_field("Cc: Undisclosed recipients:;\r\n"),
Ok(("", HeaderField::Cc(vec![AddressRef::Many(GroupRef {
name: "Undisclosed recipients".into(),
participants: vec![],
})])))
);
}
#[test]
fn test_bcc() {
assert_eq!(
header_field("Bcc: (empty)\r\n"),
Ok(("", HeaderField::Bcc(vec![])))
);
assert_eq!(
header_field("Bcc: \r\n"),
Ok(("", HeaderField::Bcc(vec![])))
);
}
// 3.6.4. Identification Fields
#[test]
fn test_message_id() {
assert_eq!(
header_field("Message-ID: <310@[127.0.0.1]>\r\n"),
Ok(("", HeaderField::MessageID(model::MessageId { left: "310", right: "127.0.0.1" })))
);
}
#[test]
fn test_in_reply_to() {
assert_eq!(
header_field("In-Reply-To: <a@b> <c@example.com>\r\n"),
Ok(("", HeaderField::InReplyTo(vec![
model::MessageId { left: "a", right: "b" },
model::MessageId { left: "c", right: "example.com" },
])))
);
}
#[test]
fn test_references() {
assert_eq!(
header_field("References: <1234@local.machine.example> <3456@example.net>\r\n"),
Ok(("", HeaderField::References(vec![
model::MessageId { left: "1234", right: "local.machine.example" },
model::MessageId { left: "3456", right: "example.net" },
])))
);
}
// 3.6.5. Informational Fields
#[test]
fn test_subject() {
assert_eq!(
header_field("Subject: Aérogramme\r\n"),
Ok(("", HeaderField::Subject("Aérogramme".into())))
);
}
#[test]
fn test_comments() {
assert_eq!(
header_field("Comments: 😛 easter egg!\r\n"),
Ok(("", HeaderField::Comments("😛 easter egg!".into())))
);
}
#[test]
fn test_keywords() {
assert_eq!(
header_field("Keywords: fantasque, farfelu, fanfreluche\r\n"),
Ok(("", HeaderField::Keywords(vec!["fantasque".into(), "farfelu".into(), "fanfreluche".into()])))
);
}
// Test invalid field name
#[test]
fn test_invalid_field_name() {
assert!(header_field("Unknown: unknown\r\n").is_err());
}
}

13
src/datetime.rs Normal file
View file

@ -0,0 +1,13 @@
use chrono::DateTime;
use nom::IResult;
use crate::{model,misc_token};
pub fn section(input: &str) -> IResult<&str, model::HeaderDate> {
// @FIXME want to extract datetime our way in the future
// to better handle obsolete/bad cases instead of returning raw text.
let (input, raw_date) = misc_token::unstructured(input)?;
match DateTime::parse_from_rfc2822(&raw_date) {
Ok(chronodt) => Ok((input, model::HeaderDate::Parsed(chronodt))),
Err(e) => Ok((input, model::HeaderDate::Unknown(raw_date, e))),
}
}

View file

@ -1,16 +1,451 @@
use nom::{ use nom::{
IResult, IResult,
multi::many0, branch::alt,
bytes::complete::take_while1,
bytes::complete::tag,
character::complete::space0,
combinator::{map, opt},
multi::{many0, many1, fold_many0, separated_list1},
sequence::{terminated, preceded, pair, tuple},
}; };
use crate::{common_fields, trace, whitespace, model}; use crate::whitespace::{fws, perm_crlf};
use crate::words::vchar_seq;
use crate::misc_token::{phrase, unstructured};
use crate::model::{HeaderSection, HeaderDate, MailboxRef, AddressRef};
use crate::mailbox::mailbox;
use crate::address::{mailbox_list, address_list, address_list_cfws};
use crate::identification::msg_id;
use crate::{datetime, trace, model};
pub fn section(input: &str) -> IResult<&str, model::HeaderSection> { /// HEADERS
let (input, traces) = many0(trace::section)(input)?;
let (input, common) = common_fields::section(input)?;
let (input, _) = whitespace::perm_crlf(input)?;
Ok((input, model::HeaderSection { traces, common })) /// Header section
///
/// See: https://www.rfc-editor.org/rfc/rfc5322.html#section-2.2
pub fn section(input: &str) -> IResult<&str, HeaderSection> {
let (input, headers) = fold_many0(
alt((header_field, unknown_field)),
HeaderSection::default,
|mut section, head| {
match head {
//@FIXME min and max limits are not enforced,
// it may result in missing data or silently overriden data.
// 3.6.1. The Origination Date Field
HeaderField::Date(d) => {
// | orig-date | 1 | 1 | |
section.date = d;
}
// 3.6.2. Originator Fields
HeaderField::From(v) => {
// | from | 1 | 1 | See sender and 3.6.2 |
section.from = v;
}
HeaderField::Sender(mbx) => {
// | sender | 0* | 1 | MUST occur with multi-address from - see 3.6.2 |
section.sender = Some(mbx);
}
HeaderField::ReplyTo(addr_list) => {
// | reply-to | 0 | 1 | |
section.reply_to = addr_list;
}
// 3.6.3. Destination Address Fields
HeaderField::To(addr_list) => {
// | to | 0 | 1 | |
section.to = addr_list;
}
HeaderField::Cc(addr_list) => {
// | cc | 0 | 1 | |
section.cc = addr_list;
}
HeaderField::Bcc(addr_list) => {
// | bcc | 0 | 1 | |
section.bcc = addr_list;
}
// 3.6.4. Identification Fields
HeaderField::MessageID(msg_id) => {
// | message-id | 0* | 1 | SHOULD be present - see 3.6.4 |
section.msg_id = Some(msg_id);
}
HeaderField::InReplyTo(id_list) => {
// | in-reply-to | 0* | 1 | SHOULD occur in some replies - see 3.6.4 |
section.in_reply_to = id_list;
}
HeaderField::References(id_list) => {
// | in-reply-to | 0* | 1 | SHOULD occur in some replies - see 3.6.4 |
section.references = id_list;
}
// 3.6.5. Informational Fields
HeaderField::Subject(title) => {
// | subject | 0 | 1 | |
section.subject = Some(title);
}
HeaderField::Comments(coms) => {
// | comments | 0 | unlimited | |
section.comments.push(coms);
}
HeaderField::Keywords(mut kws) => {
// | keywords | 0 | unlimited | |
section.keywords.append(&mut kws);
}
// 3.6.6 Resent Fields are not implemented
// 3.6.7 Trace Fields
HeaderField::ReturnPath(maybe_mbx) => {
if let Some(mbx) = maybe_mbx {
section.return_path.push(mbx);
}
}
HeaderField::Received(log) => {
section.received.push(log);
}
// 3.6.8. Optional Fields
HeaderField::Optional(name, body) => {
section.optional.insert(name, body);
}
};
section
}
)(input)?;
let (input, _) = perm_crlf(input)?;
Ok((input, headers))
}
#[derive(Debug, PartialEq)]
pub enum HeaderField<'a> {
// 3.6.1. The Origination Date Field
Date(HeaderDate),
// 3.6.2. Originator Fields
From(Vec<MailboxRef>),
Sender(MailboxRef),
ReplyTo(Vec<AddressRef>),
// 3.6.3. Destination Address Fields
To(Vec<AddressRef>),
Cc(Vec<AddressRef>),
Bcc(Vec<AddressRef>),
// 3.6.4. Identification Fields
MessageID(model::MessageId<'a>),
InReplyTo(Vec<model::MessageId<'a>>),
References(Vec<model::MessageId<'a>>),
// 3.6.5. Informational Fields
Subject(String),
Comments(String),
Keywords(Vec<String>),
// 3.6.6 Resent Fields (not implemented)
// 3.6.7 Trace Fields
Received(&'a str),
ReturnPath(Option<model::MailboxRef>),
// 3.6.8. Optional Fields
Optional(&'a str, String)
}
/// Parse one known header field
///
/// RFC5322 optional-field seems to be a generalization of the field terminology.
/// We use it to parse all header names:
pub fn header_field(input: &str) -> IResult<&str, HeaderField> {
terminated(
alt((
// 3.6.1. The Origination Date Field
date,
// 3.6.2. Originator Fields
alt((from, sender, reply_to)),
// 3.6.3. Destination Address Fields
alt((to, cc, bcc)),
// 3.6.4. Identification Fields
alt((msg_id_field, in_reply_to, references)),
// 3.6.5. Informational Fields
alt((subject, comments, keywords)),
// 3.6.7 Trace field
alt((return_path, received)),
)),
perm_crlf,
)(input)
}
// 3.6.1. The Origination Date Field
fn date(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Date:"), space0), datetime::section)(input)?;
Ok((input, HeaderField::Date(body)))
}
// 3.6.2. Originator Fields
fn from(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("From:"), space0), mailbox_list)(input)?;
Ok((input, HeaderField::From(body)))
}
fn sender(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Sender:"), space0), mailbox)(input)?;
Ok((input, HeaderField::Sender(body)))
}
fn reply_to(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Reply-To:"), space0), address_list)(input)?;
Ok((input, HeaderField::ReplyTo(body)))
}
// 3.6.3. Destination Address Fields
fn to(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("To:"), space0), address_list)(input)?;
Ok((input, HeaderField::To(body)))
}
fn cc(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Cc:"), space0), address_list)(input)?;
Ok((input, HeaderField::Cc(body)))
}
fn bcc(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(
pair(tag("Bcc:"), space0),
opt(alt((address_list, address_list_cfws))),
)(input)?;
Ok((input, HeaderField::Bcc(body.unwrap_or(vec![]))))
}
// 3.6.4. Identification Fields
fn msg_id_field(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Message-ID:"), space0), msg_id)(input)?;
Ok((input, HeaderField::MessageID(body)))
}
fn in_reply_to(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("In-Reply-To:"), space0), many1(msg_id))(input)?;
Ok((input, HeaderField::InReplyTo(body)))
}
fn references(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("References:"), space0), many1(msg_id))(input)?;
Ok((input, HeaderField::References(body)))
}
// 3.6.5. Informational Fields
fn subject(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Subject:"), space0), unstructured)(input)?;
Ok((input, HeaderField::Subject(body)))
}
fn comments(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(pair(tag("Comments:"), space0), unstructured)(input)?;
Ok((input, HeaderField::Comments(body)))
}
fn keywords(input: &str) -> IResult<&str, HeaderField> {
let (input, body) = preceded(
pair(tag("Keywords:"), space0),
separated_list1(tag(","), phrase),
)(input)?;
Ok((input, HeaderField::Keywords(body)))
}
// 3.6.6 Resent fields
// Not implemented
// 3.6.7 Trace fields
fn return_path(input: &str) -> IResult<&str, HeaderField> {
map(
preceded(pair(tag("Return-Path:"), space0), trace::return_path_body),
|body| HeaderField::ReturnPath(body),
)(input)
}
fn received(input: &str) -> IResult<&str, HeaderField> {
map(
preceded(pair(tag("Received:"), space0), trace::received_body),
|body| HeaderField::Received(body),
)(input)
}
/// Optional field
///
/// ```abnf
/// field = field-name ":" unstructured CRLF
/// field-name = 1*ftext
/// ftext = %d33-57 / ; Printable US-ASCII
/// %d59-126 ; characters not including
/// ; ":".
/// ```
fn unknown_field(input: &str) -> IResult<&str, HeaderField> {
// Extract field name
let (input, field_name) = field_name(input)?;
let (input, body) = unstructured(input)?;
Ok((input, HeaderField::Optional(field_name, body)))
}
pub fn field_name(input: &str) -> IResult<&str, &str> {
terminated(
take_while1(|c| c >= '\x21' && c <= '\x7E' && c != '\x3A'),
pair(tag(":"), space0)
)(input)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{GroupRef, AddrSpec};
// 3.6.1. The Origination Date Field
#[test]
fn test_datetime() {
let datefield = "Date: Thu,\r\n 13\r\n Feb\r\n 1969\r\n 23:32\r\n -0330 (Newfoundland Time)\r\n";
let (input, v) = header_field(datefield).unwrap();
assert_eq!(input, "");
match v {
HeaderField::Date(HeaderDate::Parsed(_)) => (),
_ => panic!("Date has not been parsed"),
};
}
// 3.6.2. Originator Fields
#[test]
fn test_from() {
assert_eq!(
header_field("From: \"Joe Q. Public\" <john.q.public@example.com>\r\n"),
Ok(("", HeaderField::From(vec![MailboxRef {
name: Some("Joe Q. Public".into()),
addrspec: AddrSpec {
local_part: "john.q.public".into(),
domain: "example.com".into(),
}
}]))),
);
}
#[test]
fn test_sender() {
assert_eq!(
header_field("Sender: Michael Jones <mjones@machine.example>\r\n"),
Ok(("", HeaderField::Sender(MailboxRef {
name: Some("Michael Jones".into()),
addrspec: AddrSpec {
local_part: "mjones".into(),
domain: "machine.example".into(),
},
}))),
);
}
#[test]
fn test_reply_to() {
assert_eq!(
header_field("Reply-To: \"Mary Smith: Personal Account\" <smith@home.example>\r\n"),
Ok(("", HeaderField::ReplyTo(vec![AddressRef::Single(MailboxRef {
name: Some("Mary Smith: Personal Account".into()),
addrspec: AddrSpec {
local_part: "smith".into(),
domain: "home.example".into(),
},
})])))
);
}
// 3.6.3. Destination Address Fields
#[test]
fn test_to() {
assert_eq!(
header_field("To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;\r\n"),
Ok(("", HeaderField::To(vec![AddressRef::Many(GroupRef {
name: "A Group".into(),
participants: vec![
MailboxRef {
name: Some("Ed Jones".into()),
addrspec: AddrSpec { local_part: "c".into(), domain: "a.test".into() },
},
MailboxRef {
name: None,
addrspec: AddrSpec { local_part: "joe".into(), domain: "where.test".into() },
},
MailboxRef {
name: Some("John".into()),
addrspec: AddrSpec { local_part: "jdoe".into(), domain: "one.test".into() },
},
]
})])))
);
}
#[test]
fn test_cc() {
assert_eq!(
header_field("Cc: Undisclosed recipients:;\r\n"),
Ok(("", HeaderField::Cc(vec![AddressRef::Many(GroupRef {
name: "Undisclosed recipients".into(),
participants: vec![],
})])))
);
}
#[test]
fn test_bcc() {
assert_eq!(
header_field("Bcc: (empty)\r\n"),
Ok(("", HeaderField::Bcc(vec![])))
);
assert_eq!(
header_field("Bcc: \r\n"),
Ok(("", HeaderField::Bcc(vec![])))
);
}
// 3.6.4. Identification Fields
#[test]
fn test_message_id() {
assert_eq!(
header_field("Message-ID: <310@[127.0.0.1]>\r\n"),
Ok(("", HeaderField::MessageID(model::MessageId { left: "310", right: "127.0.0.1" })))
);
}
#[test]
fn test_in_reply_to() {
assert_eq!(
header_field("In-Reply-To: <a@b> <c@example.com>\r\n"),
Ok(("", HeaderField::InReplyTo(vec![
model::MessageId { left: "a", right: "b" },
model::MessageId { left: "c", right: "example.com" },
])))
);
}
#[test]
fn test_references() {
assert_eq!(
header_field("References: <1234@local.machine.example> <3456@example.net>\r\n"),
Ok(("", HeaderField::References(vec![
model::MessageId { left: "1234", right: "local.machine.example" },
model::MessageId { left: "3456", right: "example.net" },
])))
);
}
// 3.6.5. Informational Fields
#[test]
fn test_subject() {
assert_eq!(
header_field("Subject: Aérogramme\r\n"),
Ok(("", HeaderField::Subject("Aérogramme".into())))
);
}
#[test]
fn test_comments() {
assert_eq!(
header_field("Comments: 😛 easter egg!\r\n"),
Ok(("", HeaderField::Comments("😛 easter egg!".into())))
);
}
#[test]
fn test_keywords() {
assert_eq!(
header_field("Keywords: fantasque, farfelu, fanfreluche\r\n"),
Ok(("", HeaderField::Keywords(vec!["fantasque".into(), "farfelu".into(), "fanfreluche".into()])))
);
}
// Test invalid field name
#[test]
fn test_invalid_field_name() {
assert!(header_field("Unknown: unknown\r\n").is_err());
}
} }

View file

@ -11,12 +11,8 @@ mod misc_token;
mod mailbox; mod mailbox;
mod address; mod address;
mod identification; mod identification;
mod trace;
mod datetime;
// Header blocks // Header blocks
pub mod common_fields;
pub mod trace;
// Global mail
pub mod header; pub mod header;

View file

@ -63,27 +63,6 @@ pub struct MessageId<'a> {
pub right: &'a str, pub right: &'a str,
} }
#[derive(Debug, PartialEq, Default)]
pub struct Trace<'a> {
// 3.6.7 Traces
pub received: Vec<&'a str>,
pub return_path: Option<MailboxRef>,
// 3.6.6. Resent Fields
pub resent_date: HeaderDate,
pub resent_from: Vec<MailboxRef>,
pub resent_sender: Option<MailboxRef>,
pub resent_to: Vec<AddressRef>,
pub resent_cc: Vec<AddressRef>,
pub resent_bcc: Vec<AddressRef>,
pub resent_msg_id: Option<MessageId<'a>>,
// 3.6.8. Optional Fields
pub optional: HashMap<&'a str, String>,
//pub failed: HashMap<&'a str, String>,
//pub garbage: &'a str,
}
/// Permissive Header Section /// Permissive Header Section
/// ///
/// This is a structure intended for parsing/decoding, /// This is a structure intended for parsing/decoding,
@ -91,7 +70,7 @@ pub struct Trace<'a> {
/// as invalid according to RFC5322 but for which we can /// as invalid according to RFC5322 but for which we can
/// still extract some data. /// still extract some data.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct CommonFields<'a> { pub struct HeaderSection<'a> {
// 3.6.1. The Origination Date Field // 3.6.1. The Origination Date Field
pub date: HeaderDate, pub date: HeaderDate,
@ -115,16 +94,13 @@ pub struct CommonFields<'a> {
pub comments: Vec<String>, pub comments: Vec<String>,
pub keywords: Vec<String>, pub keywords: Vec<String>,
// 3.6.6 Not implemented
// 3.6.7 Trace Fields
pub return_path: Vec<MailboxRef>,
pub received: Vec<&'a str>,
// 3.6.8. Optional Fields // 3.6.8. Optional Fields
pub optional: HashMap<&'a str, String>, pub optional: HashMap<&'a str, String>,
//pub failed: HashMap<&'a str, String>, //pub failed: HashMap<&'a str, String>,
//pub garbage: &'a str, //pub garbage: &'a str,
} }
pub struct HeaderSection<'a> {
// 3.6.7 Traces
pub traces: Vec<Trace<'a>>,
// 3.6.x
pub common: CommonFields<'a>,
}

View file

@ -1,7 +1,14 @@
use imf_codec::common_fields; use imf_codec::header;
fn main() { fn main() {
let header = r#"Date: Fri, 21 Nov 1997 10:01:10 -0600 let hdr = r#"Return-Path: <gitlab@framasoft.org>
Delivered-To: quentin@dufour.io
Received: from smtp.deuxfleurs.fr ([10.83.2.2])
by doradille with LMTP
id IKPyOvS8iGTxBAEAvTd7DQ
(envelope-from <gitlab@framasoft.org>)
for <quentin@dufour.io>; Tue, 13 Jun 2023 19:01:08 +0000
Date: Fri, 21 Nov 1997 10:01:10 -0600
From: Mary Smith From: Mary Smith
<mary@example.net> <mary@example.net>
Sender: imf@example.com Sender: imf@example.com
@ -21,5 +28,5 @@ References: <1234@local.machine.example>
This is a reply to your hello. This is a reply to your hello.
"#; "#;
println!("{:?}", common_fields::section(header)); println!("{:?}", header::section(hdr));
} }

View file

@ -4,116 +4,24 @@ use nom::{
branch::alt, branch::alt,
bytes::complete::tag, bytes::complete::tag,
character::complete::space0, character::complete::space0,
combinator::{map, not, opt, recognize}, combinator::{map, opt, recognize},
multi::{fold_many0, many0, many1}, multi::many0,
sequence::{delimited, preceded, terminated, pair, tuple}, sequence::{delimited, pair, tuple},
}; };
use crate::{address, common_fields, identification, mailbox, model, misc_token, whitespace}; use crate::{datetime, mailbox, model, misc_token, whitespace};
enum RestField<'a> { pub fn received_body(input: &str) -> IResult<&str, &str> {
// 3.6.6. Resent Fields map(
ResentDate(model::HeaderDate), tuple((
ResentFrom(Vec<model::MailboxRef>), recognize(many0(received_tokens)),
ResentSender(model::MailboxRef), tag(";"),
ResentTo(Vec<model::AddressRef>), datetime::section,
ResentCc(Vec<model::AddressRef>), )),
ResentBcc(Vec<model::AddressRef>), |(tokens, _, _)| tokens,
ResentMessageID(model::MessageId<'a>),
// 3.6.8. Optional fields
OptionalField(&'a str, String),
}
enum PreludeField {
// 3.6.7. Trace Fields
ReturnPath(String),
Received(Vec<String>),
}
/// Section
///
/// Optional fields are allowed everywhere in this implementation...
///
/// ```abnf
///*(trace
/// *(optional-field /
/// resent-date /
/// resent-from /
/// resent-sender /
/// resent-to /
/// resent-cc /
/// resent-bcc /
/// resent-msg-id))
/// ```
pub fn section(input: &str) -> IResult<&str, model::Trace> {
let (input, (path, recv)) = prelude(input)?;
let (input, mut full_trace) = fold_many0(
alt((resent_field, unknown_field)),
model::Trace::default,
|mut trace, field| {
match field {
RestField::ResentDate(date) => {
trace.resent_date = date;
}
RestField::ResentFrom(from) => {
trace.resent_from = from;
}
RestField::ResentSender(sender) => {
trace.resent_sender = Some(sender);
}
RestField::ResentTo(to) => {
trace.resent_to = to;
}
RestField::ResentCc(cc) => {
trace.resent_cc = cc;
}
RestField::ResentBcc(bcc) => {
trace.resent_bcc = bcc;
}
RestField::ResentMessageID(mid) => {
trace.resent_msg_id = Some(mid);
}
RestField::OptionalField(name, body) => {
trace.optional.insert(name, body);
}
};
trace
}
)(input)?;
full_trace.received = recv;
full_trace.return_path = path;
Ok((input, full_trace))
}
/// Trace prelude
///
/// ```abnf
/// trace = [return]
/// 1*received
/// return = "Return-Path:" path CRLF
/// path = angle-addr / ([CFWS] "<" [CFWS] ">" [CFWS])
/// received = "Received:" *received-token ";" date-time CRLF
/// received-token = word / angle-addr / addr-spec / domain
/// ```
fn prelude(input: &str) -> IResult<&str, (Option<model::MailboxRef>, Vec<&str>)> {
let (input, (return_path, received)) = pair(
opt(return_path_field),
many1(received_field),
)(input)?;
Ok((input, (return_path.flatten(), received)))
}
fn return_path_field(input: &str) -> IResult<&str, Option<model::MailboxRef>> {
delimited(
pair(tag("Return-Path:"), space0),
path,
whitespace::perm_crlf,
)(input) )(input)
} }
fn path(input: &str) -> IResult<&str, Option<model::MailboxRef>> { pub fn return_path_body(input: &str) -> IResult<&str, Option<model::MailboxRef>> {
alt(( alt((
map(mailbox::angle_addr, |a| Some(a)), map(mailbox::angle_addr, |a| Some(a)),
empty_path empty_path
@ -131,18 +39,6 @@ fn empty_path(input: &str) -> IResult<&str, Option<model::MailboxRef>> {
Ok((input, None)) Ok((input, None))
} }
fn received_field(input: &str) -> IResult<&str, &str> {
let (input, (_, tk, _, _, _)) = tuple((
pair(tag("Received:"), space0),
recognize(many0(received_tokens)),
tag(";"),
common_fields::datetime,
whitespace::perm_crlf,
))(input)?;
Ok((input, tk))
}
fn received_tokens(input: &str) -> IResult<&str, &str> { fn received_tokens(input: &str) -> IResult<&str, &str> {
alt(( alt((
recognize(mailbox::angle_addr), recognize(mailbox::angle_addr),
@ -152,73 +48,6 @@ fn received_tokens(input: &str) -> IResult<&str, &str> {
))(input) ))(input)
} }
fn resent_field(input: &str) -> IResult<&str, RestField> {
terminated(
alt((
resent_date,
resent_from,
resent_sender,
resent_to,
resent_cc,
resent_bcc,
resent_msg_id,
)),
whitespace::perm_crlf,
)(input)
}
fn resent_date(input: &str) -> IResult<&str, RestField> {
let (input, body) = preceded(pair(tag("Resent-Date:"), space0), common_fields::datetime)(input)?;
Ok((input, RestField::ResentDate(body)))
}
fn resent_from(input: &str) -> IResult<&str, RestField> {
let (input, body) = preceded(pair(tag("Resent-From:"), space0), address::mailbox_list)(input)?;
Ok((input, RestField::ResentFrom(body)))
}
fn resent_sender(input: &str) -> IResult<&str, RestField> {
let (input, body) = preceded(pair(tag("Resent-Sender:"), space0), mailbox::mailbox)(input)?;
Ok((input, RestField::ResentSender(body)))
}
fn resent_to(input: &str) -> IResult<&str, RestField> {
let (input, body) = preceded(pair(tag("Resent-To:"), space0), address::address_list)(input)?;
Ok((input, RestField::ResentTo(body)))
}
fn resent_cc(input: &str) -> IResult<&str, RestField> {
let (input, body) = preceded(pair(tag("Resent-Cc:"), space0), address::address_list)(input)?;
Ok((input, RestField::ResentCc(body)))
}
fn resent_bcc(input: &str) -> IResult<&str, RestField> {
let (input, body) = preceded(
pair(tag("Resent-Bcc:"), space0),
opt(alt((address::address_list, address::address_list_cfws))),
)(input)?;
Ok((input, RestField::ResentBcc(body.unwrap_or(vec![]))))
}
fn resent_msg_id(input: &str) -> IResult<&str, RestField> {
let (input, body) = preceded(pair(tag("Resent-Message-ID:"), space0), identification::msg_id)(input)?;
Ok((input, RestField::ResentMessageID(body)))
}
fn unknown_field(input: &str) -> IResult<&str, RestField> {
// Check that we:
// 1. do not start a new trace
// 2. do not start the common fields
not(prelude)(input)?;
not(common_fields::header_field)(input)?;
// Extract field name
let (input, field_name) = common_fields::field_name(input)?;
let (input, body) = misc_token::unstructured(input)?;
let (input, _) = whitespace::perm_crlf(input)?;
Ok((input, RestField::OptionalField(field_name, body)))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -226,90 +55,20 @@ mod tests {
use chrono::{FixedOffset, TimeZone}; use chrono::{FixedOffset, TimeZone};
#[test] #[test]
fn test_section() { fn test_received() {
let hdrs = r#"Return-Path: <gitlab@example.com> let hdrs = r#"from smtp.example.com ([10.83.2.2])
Received: from smtp.example.com ([10.83.2.2])
by server with LMTP by server with LMTP
id xxxxxxxxx id xxxxxxxxx
(envelope-from <gitlab@example.com>) (envelope-from <gitlab@example.com>)
for <me@example.com>; Tue, 13 Jun 2023 19:01:08 +0000 for <me@example.com>; Tue, 13 Jun 2023 19:01:08 +0000"#;
Resent-Date: Tue, 13 Jun 2023 21:01:07 +0200
Resent-From: <you@example.com>
Resent-Sender: you@example.com
X-Specific: XOXO
Resent-To: Annah <annah@example.com>
Resent-Cc: Empty:;
Resent-Bcc:
Resent-Message-ID: <note_1985938@example.com>
"#;
assert_eq!( assert_eq!(
section(hdrs), received_body(hdrs),
Ok(("", model::Trace { Ok(("", r#"from smtp.example.com ([10.83.2.2])
return_path: Some(model::MailboxRef {
name: None,
addrspec: model::AddrSpec {
local_part: "gitlab".into(),
domain: "example.com".into(),
}
}),
received: vec![
r#"from smtp.example.com ([10.83.2.2])
by server with LMTP by server with LMTP
id xxxxxxxxx id xxxxxxxxx
(envelope-from <gitlab@example.com>) (envelope-from <gitlab@example.com>)
for <me@example.com>"#, for <me@example.com>"#))
],
resent_date: model::HeaderDate::Parsed(
FixedOffset::east_opt(2 * 3600)
.unwrap()
.with_ymd_and_hms(2023, 06, 13, 21, 1, 7)
.unwrap()),
resent_from: vec![
model::MailboxRef {
name: None,
addrspec: model::AddrSpec {
local_part: "you".into(),
domain: "example.com".into(),
}
}
],
resent_sender: Some(model::MailboxRef {
name: None,
addrspec: model::AddrSpec {
local_part: "you".into(),
domain: "example.com".into(),
}
}),
resent_to: vec![
model::AddressRef::Single(model::MailboxRef {
name: Some("Annah".into()),
addrspec: model::AddrSpec {
local_part: "annah".into(),
domain: "example.com".into(),
}
})
],
resent_cc: vec![
model::AddressRef::Many(model::GroupRef {
name: "Empty".into(),
participants: vec![],
})
],
resent_bcc: vec![],
resent_msg_id: Some(model::MessageId {
left: "note_1985938",
right: "example.com",
}),
optional: HashMap::from([("X-Specific", "XOXO".into())]),
}))
); );
} }
} }