drop my early implementation of trace
This commit is contained in:
parent
b6c25a4676
commit
486ccf2de0
8 changed files with 494 additions and 735 deletions
|
@ -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 |
|
||||||
|
|
|
@ -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
13
src/datetime.rs
Normal 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))),
|
||||||
|
}
|
||||||
|
}
|
449
src/header.rs
449
src/header.rs
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
36
src/model.rs
36
src/model.rs
|
@ -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>,
|
|
||||||
}
|
|
||||||
|
|
13
src/parse.rs
13
src/parse.rs
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
281
src/trace.rs
281
src/trace.rs
|
@ -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())]),
|
|
||||||
}))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue