Improve README
This commit is contained in:
parent
6af2e38ae3
commit
bcb5a81d32
3 changed files with 17 additions and 497 deletions
45
README.md
45
README.md
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
**⚠️ This is currently only a decoder (ie. a parser), encoding is not yet implemented.**
|
**⚠️ This is currently only a decoder (ie. a parser), encoding is not yet implemented.**
|
||||||
|
|
||||||
|
`eml-codec` is a child project of [Aerogramme](https://aerogramme.deuxfleurs.fr), a distributed and encrypted IMAP server developped by the non-profit organization [Deuxfleurs](https://deuxfleurs.fr).
|
||||||
|
Its aim is to be a swiss army knife to handle emails, whether it is to build an IMAP/JMAP server, a mail filter (like an antispam), or a mail client.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
@ -56,34 +59,17 @@ Speak about parser combinators.
|
||||||
|
|
||||||
## Testing strategy
|
## Testing strategy
|
||||||
|
|
||||||
eml-codec aims to be as much tested as possible against real word data.
|
Currently this crate has some unit tests on most of its parsing functions.
|
||||||
|
It is also tested as part of Aerogramme, its parent project where it handles email parsing.
|
||||||
|
In this project, `eml-codec` parsing capabilities are compared to Dovecot, Cyrus, Maddy and other IMAP servers.
|
||||||
|
|
||||||
### Unit testing: parser combinator independently (done)
|
It is planned to test it on large email datasets (like Enron, jpbush, mailing lists, etc.) but it's not done yet.
|
||||||
|
Fuzzing the library would also be interesting, probably to detect crashing due to stack overflow for example
|
||||||
|
due to the infinite recursivity of MIME.
|
||||||
|
|
||||||
### Selected full emails (expected)
|
## RFC and IANA references
|
||||||
|
|
||||||
### Existing datasets
|
RFC
|
||||||
|
|
||||||
**Enron 500k** - Took 20 minutes to parse ~517k emails and check that
|
|
||||||
RFC5322 headers (From, To, Cc, etc.) are correctly parsed.
|
|
||||||
From this list, we had to exclude ~50 emails on which
|
|
||||||
the From/To/Cc fields were simply completely wrong, but while
|
|
||||||
some fields failed to parse, the parser did not crash and
|
|
||||||
parsed the other fields of the email correctly.
|
|
||||||
|
|
||||||
Run it on your machine:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo test -- --ignored --nocapture enron500k
|
|
||||||
```
|
|
||||||
|
|
||||||
Planned: jpbush, my inbox, etc.
|
|
||||||
|
|
||||||
### Fuzzing (expected)
|
|
||||||
|
|
||||||
### Across reference IMAP servers (dovevot, cyrus) (expected)
|
|
||||||
|
|
||||||
## Targeted RFC and IANA references
|
|
||||||
|
|
||||||
| 🚩 | # | Name |
|
| 🚩 | # | Name |
|
||||||
|----|---|------|
|
|----|---|------|
|
||||||
|
@ -106,9 +92,12 @@ Planned: jpbush, my inbox, etc.
|
||||||
| 🔴 |3798 | ↳ Message Disposition Notification |
|
| 🔴 |3798 | ↳ Message Disposition Notification |
|
||||||
| 🔴 |6838 | ↳ Media Type Specifications and Registration Procedures |
|
| 🔴 |6838 | ↳ Media Type Specifications and Registration Procedures |
|
||||||
|
|
||||||
IANA references :
|
IANA
|
||||||
- (tbd) MIME subtypes
|
|
||||||
- [IANA character sets](https://www.iana.org/assignments/character-sets/character-sets.xhtml)
|
| Name | Description | Note |
|
||||||
|
|------|-------------|------|
|
||||||
|
| [Media Types](https://www.iana.org/assignments/media-types/media-types.xhtml) | Registered media types for the Content-Type field | Currently only the media types in the MIME RFC have dedicated support in `eml-codec`. |
|
||||||
|
| [Character sets](https://www.iana.org/assignments/character-sets/character-sets.xhtml) | Supported character sets for the `charset` parameter | They should all be supported through the `encoding_rs` crate |
|
||||||
|
|
||||||
## State of the art / alternatives
|
## State of the art / alternatives
|
||||||
|
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
use imf_codec::fragments::section;
|
|
||||||
use imf_codec::multipass;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
fn parser<'a, F>(input: &'a [u8], func: F) -> ()
|
|
||||||
where
|
|
||||||
F: FnOnce(§ion::Section) -> (),
|
|
||||||
{
|
|
||||||
let seg = multipass::segment::new(input).unwrap();
|
|
||||||
let charset = seg.charset();
|
|
||||||
let fields = charset.fields().unwrap();
|
|
||||||
let field_names = fields.names();
|
|
||||||
let field_body = field_names.body();
|
|
||||||
let section = field_body.section();
|
|
||||||
|
|
||||||
func(§ion.fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn test_enron500k() {
|
|
||||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
d.push("resources/enron/maildir/");
|
|
||||||
let prefix_sz = d.as_path().to_str().unwrap().len();
|
|
||||||
//d.push("williams-w3/");
|
|
||||||
|
|
||||||
let known_bad_fields = HashSet::from([
|
|
||||||
"white-s/calendar/113.", // To: east <7..>
|
|
||||||
"skilling-j/inbox/223.", // From: pep <performance.>
|
|
||||||
"jones-t/all_documents/9806.", // To: <"tibor.vizkelety":@enron.com>
|
|
||||||
"jones-t/notes_inbox/3303.", // To: <"tibor.vizkelety":@enron.com>
|
|
||||||
"lokey-t/calendar/33.", // A second Date entry for the calendar containing
|
|
||||||
// Date: Monday, March 12
|
|
||||||
"zipper-a/inbox/199.", // To: e-mail <mari.>
|
|
||||||
"dasovich-j/deleted_items/128.", // To: f62489 <g>
|
|
||||||
"dasovich-j/all_documents/677.", // To: w/assts <govt.>
|
|
||||||
"dasovich-j/all_documents/8984.", // To: <"ft.com.users":@enron.com>
|
|
||||||
"dasovich-j/all_documents/3514.", // To: <"ft.com.users":@enron.com>
|
|
||||||
"dasovich-j/all_documents/4467.", // To: <"ft.com.users":@enron.com>
|
|
||||||
"dasovich-j/all_documents/578.", // To: w/assts <govt.>
|
|
||||||
"dasovich-j/all_documents/3148.", // To: <"economist.com.readers":@enron.com>
|
|
||||||
"dasovich-j/all_documents/9953.", // To: <"economist.com.reader":@enron.com>
|
|
||||||
"dasovich-j/risk_analytics/3.", // To: w/assts <govt.>
|
|
||||||
"dasovich-j/notes_inbox/5391.", // To: <"ft.com.users":@enron.com>
|
|
||||||
"dasovich-j/notes_inbox/4952.", // To: <"economist.com.reader":@enron.com>
|
|
||||||
"dasovich-j/notes_inbox/2386.", // To: <"ft.com.users":@enron.com>
|
|
||||||
"dasovich-j/notes_inbox/1706.", // To: <"ft.com.users":@enron.com>
|
|
||||||
"dasovich-j/notes_inbox/1489.", // To: <"economist.com.readers":@enron.com>
|
|
||||||
"dasovich-j/notes_inbox/5.", // To: w/assts <govt.>
|
|
||||||
"kaminski-v/sites/19.", // To: <"the.desk":@enron.com>
|
|
||||||
"kaminski-v/sites/1.", // To: <"the.desk":@enron.com>
|
|
||||||
"kaminski-v/discussion_threads/5082.", // To: <"ft.com.users":@enron.com>
|
|
||||||
"kaminski-v/discussion_threads/4046.", // To: <"the.desk":@enron.com>
|
|
||||||
"kaminski-v/discussion_threads/4187.", // To: <"the.desk":@enron.com>
|
|
||||||
"kaminski-v/discussion_threads/8068.", // To: cats <breaktkhrough.>, risk <breakthrough.>, leaders <breaktkhrough.>
|
|
||||||
"kaminski-v/discussion_threads/7980.", // To: dogs <breakthrough.>, cats <breaktkhrough.>, risk <breakthrough.>,\r\n\tleaders <breaktkhrough.>
|
|
||||||
"kaminski-v/all_documents/5970.", //To: dogs <breakthrough.>, cats <breaktkhrough.>, risk <breakthrough.>,\r\n\tleaders <breaktkhrough.>
|
|
||||||
"kaminski-v/all_documents/5838.", // To + Cc: dogs <breakthrough.>, breakthrough.adm@enron.com, breakthrough.adm@enron.com,\r\n\tbreakthrough.adm@enron.com
|
|
||||||
"kaminski-v/all_documents/10070.", // To: <"ft.com.users":@enron.com>
|
|
||||||
"kaminski-v/all_documents/92.", // To: <"the.desk":@enron.com>
|
|
||||||
"kaminski-v/all_documents/276.", // To: <"the.desk":@enron.com>
|
|
||||||
"kaminski-v/technical/1.", // To: <"the.desk":@enron.com>
|
|
||||||
"kaminski-v/technical/7.", // To: <"the.desk":@enron.com>
|
|
||||||
"kaminski-v/notes_inbox/140.", // To: dogs <breakthrough.>, cats <breaktkhrough.>, risk <breakthrough.>,\r\n\tleaders <breaktkhrough.>
|
|
||||||
"kaminski-v/notes_inbox/95.", // To + CC failed: cats <breaktkhrough.>, risk <breakthrough.>, leaders <breaktkhrough.>
|
|
||||||
"kean-s/archiving/untitled/1232.", // To: w/assts <govt.>, mark.palmer@enron.com, karen.denne@enron.com
|
|
||||||
"kean-s/archiving/untitled/1688.", // To: w/assts <govt.>
|
|
||||||
"kean-s/sent/198.", // To: w/assts <govt.>, mark.palmer@enron.com, karen.denne@enron.com
|
|
||||||
"kean-s/reg_risk/9.", // To: w/assts <govt.>
|
|
||||||
"kean-s/discussion_threads/950.", // To: w/assts <govt.>, mark.palmer@enron.com, karen.denne@enron.com
|
|
||||||
"kean-s/discussion_threads/577.", // To: w/assts <govt.>
|
|
||||||
"kean-s/calendar/untitled/1096.", // To: w/assts <govt.>, mark.palmer@enron.com, karen.denne@enron.com
|
|
||||||
"kean-s/calendar/untitled/640.", // To: w/assts <govt.>
|
|
||||||
"kean-s/all_documents/640.", // To: w/assts <govt.>
|
|
||||||
"kean-s/all_documents/1095.", // To: w/assts <govt.>
|
|
||||||
"kean-s/attachments/2030.", // To: w/assts <govt.>
|
|
||||||
"williams-w3/operations_committee_isas/10.", // To: z34655 <m>
|
|
||||||
]);
|
|
||||||
|
|
||||||
let known_bad_from = HashSet::from([
|
|
||||||
"skilling-j/inbox/223.", // From: pep <performance.>
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
for entry in WalkDir::new(d.as_path())
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|file| file.ok())
|
|
||||||
{
|
|
||||||
if entry.metadata().unwrap().is_file() {
|
|
||||||
let mail_path = entry.path();
|
|
||||||
let suffix = &mail_path.to_str().unwrap()[prefix_sz..];
|
|
||||||
|
|
||||||
// read file
|
|
||||||
let mut raw = Vec::new();
|
|
||||||
let mut f = File::open(mail_path).unwrap();
|
|
||||||
f.read_to_end(&mut raw).unwrap();
|
|
||||||
|
|
||||||
// parse
|
|
||||||
parser(&raw, |hdrs| {
|
|
||||||
let ok_date = hdrs.date.is_some();
|
|
||||||
let ok_from = hdrs.from.len() > 0;
|
|
||||||
let ok_fields = hdrs.bad_fields.len() == 0;
|
|
||||||
|
|
||||||
if !ok_date || !ok_from || !ok_fields {
|
|
||||||
println!("Issue with: {}", suffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(ok_date);
|
|
||||||
|
|
||||||
if !known_bad_from.contains(suffix) {
|
|
||||||
assert!(ok_from);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !known_bad_fields.contains(suffix) {
|
|
||||||
assert!(ok_fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 1;
|
|
||||||
if i % 1000 == 0 {
|
|
||||||
println!("Analyzed emails: {}", i);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,340 +0,0 @@
|
||||||
use chrono::{FixedOffset, TimeZone};
|
|
||||||
use imf_codec::fragments::{misc_token, model, section, part, trace};
|
|
||||||
use imf_codec::multipass;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
fn parser<'a, F>(input: &'a [u8], func: F) -> ()
|
|
||||||
where
|
|
||||||
F: FnOnce(§ion::Section) -> (),
|
|
||||||
{
|
|
||||||
let seg = multipass::segment::new(input).unwrap();
|
|
||||||
let charset = seg.charset();
|
|
||||||
let fields = charset.fields().unwrap();
|
|
||||||
let field_names = fields.names();
|
|
||||||
let field_body = field_names.body();
|
|
||||||
let section = field_body.section();
|
|
||||||
|
|
||||||
func(§ion.fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_headers() {
|
|
||||||
let fullmail: &[u8] = r#"Return-Path: <gitlab@example.com>
|
|
||||||
Delivered-To: quentin@example.com
|
|
||||||
Received: from smtp.example.com ([10.83.2.2])
|
|
||||||
by doradille with LMTP
|
|
||||||
id xyzabcd
|
|
||||||
(envelope-from <gitlab@example.com>)
|
|
||||||
for <quentin@example.com>; Tue, 13 Jun 2023 19:01:08 +0000
|
|
||||||
Date: Tue, 13 Jun 2023 10:01:10 +0200
|
|
||||||
From: Mary Smith
|
|
||||||
<mary@example.net>, "A\lan" <alan@example>
|
|
||||||
Sender: imf@example.com
|
|
||||||
Reply-To: "Mary Smith: Personal Account" <smith@home.example>
|
|
||||||
To: John Doe <jdoe@machine.example>
|
|
||||||
Cc: imf2@example.com
|
|
||||||
Bcc: (hidden)
|
|
||||||
Subject: Re: Saying Hello
|
|
||||||
Comments: A simple message
|
|
||||||
Comments: Not that complicated
|
|
||||||
comments : not valid header name but should be accepted
|
|
||||||
by the parser.
|
|
||||||
Keywords: hello, world
|
|
||||||
Héron: Raté
|
|
||||||
Raté raté
|
|
||||||
Keywords: salut, le, monde
|
|
||||||
Not a real header but should still recover
|
|
||||||
Message-ID: <3456@example.net>
|
|
||||||
In-Reply-To: <1234@local.machine.example>
|
|
||||||
References: <1234@local.machine.example>
|
|
||||||
Unknown: unknown
|
|
||||||
|
|
||||||
This is a reply to your hello.
|
|
||||||
"#
|
|
||||||
.as_bytes();
|
|
||||||
parser(fullmail, |parsed_section| {
|
|
||||||
assert_eq!(
|
|
||||||
parsed_section,
|
|
||||||
§ion::Section {
|
|
||||||
date: Some(
|
|
||||||
&FixedOffset::east_opt(2 * 3600)
|
|
||||||
.unwrap()
|
|
||||||
.with_ymd_and_hms(2023, 06, 13, 10, 01, 10)
|
|
||||||
.unwrap()
|
|
||||||
),
|
|
||||||
|
|
||||||
from: vec![
|
|
||||||
&model::MailboxRef {
|
|
||||||
name: Some("Mary Smith".into()),
|
|
||||||
addrspec: model::AddrSpec {
|
|
||||||
local_part: "mary".into(),
|
|
||||||
domain: "example.net".into(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
&model::MailboxRef {
|
|
||||||
name: Some("Alan".into()),
|
|
||||||
addrspec: model::AddrSpec {
|
|
||||||
local_part: "alan".into(),
|
|
||||||
domain: "example".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
sender: Some(&model::MailboxRef {
|
|
||||||
name: None,
|
|
||||||
addrspec: model::AddrSpec {
|
|
||||||
local_part: "imf".into(),
|
|
||||||
domain: "example.com".into(),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
reply_to: vec![&model::AddressRef::Single(model::MailboxRef {
|
|
||||||
name: Some("Mary Smith: Personal Account".into()),
|
|
||||||
addrspec: model::AddrSpec {
|
|
||||||
local_part: "smith".into(),
|
|
||||||
domain: "home.example".into(),
|
|
||||||
}
|
|
||||||
})],
|
|
||||||
|
|
||||||
to: vec![&model::AddressRef::Single(model::MailboxRef {
|
|
||||||
name: Some("John Doe".into()),
|
|
||||||
addrspec: model::AddrSpec {
|
|
||||||
local_part: "jdoe".into(),
|
|
||||||
domain: "machine.example".into(),
|
|
||||||
}
|
|
||||||
})],
|
|
||||||
|
|
||||||
cc: vec![&model::AddressRef::Single(model::MailboxRef {
|
|
||||||
name: None,
|
|
||||||
addrspec: model::AddrSpec {
|
|
||||||
local_part: "imf2".into(),
|
|
||||||
domain: "example.com".into(),
|
|
||||||
}
|
|
||||||
})],
|
|
||||||
|
|
||||||
bcc: vec![],
|
|
||||||
|
|
||||||
msg_id: Some(&model::MessageId {
|
|
||||||
left: "3456",
|
|
||||||
right: "example.net"
|
|
||||||
}),
|
|
||||||
in_reply_to: vec![&model::MessageId {
|
|
||||||
left: "1234",
|
|
||||||
right: "local.machine.example"
|
|
||||||
}],
|
|
||||||
references: vec![&model::MessageId {
|
|
||||||
left: "1234",
|
|
||||||
right: "local.machine.example"
|
|
||||||
}],
|
|
||||||
|
|
||||||
subject: Some(&misc_token::Unstructured("Re: Saying Hello".into())),
|
|
||||||
|
|
||||||
comments: vec![
|
|
||||||
&misc_token::Unstructured("A simple message".into()),
|
|
||||||
&misc_token::Unstructured("Not that complicated".into()),
|
|
||||||
&misc_token::Unstructured(
|
|
||||||
"not valid header name but should be accepted by the parser.".into()
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
keywords: vec![
|
|
||||||
&misc_token::PhraseList(vec!["hello".into(), "world".into(),]),
|
|
||||||
&misc_token::PhraseList(vec!["salut".into(), "le".into(), "monde".into(),]),
|
|
||||||
],
|
|
||||||
|
|
||||||
received: vec![&trace::ReceivedLog(
|
|
||||||
r#"from smtp.example.com ([10.83.2.2])
|
|
||||||
by doradille with LMTP
|
|
||||||
id xyzabcd
|
|
||||||
(envelope-from <gitlab@example.com>)
|
|
||||||
for <quentin@example.com>"#
|
|
||||||
)],
|
|
||||||
|
|
||||||
return_path: vec![&model::MailboxRef {
|
|
||||||
name: None,
|
|
||||||
addrspec: model::AddrSpec {
|
|
||||||
local_part: "gitlab".into(),
|
|
||||||
domain: "example.com".into(),
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
optional: HashMap::from([
|
|
||||||
(
|
|
||||||
"Delivered-To",
|
|
||||||
&misc_token::Unstructured("quentin@example.com".into())
|
|
||||||
),
|
|
||||||
("Unknown", &misc_token::Unstructured("unknown".into())),
|
|
||||||
]),
|
|
||||||
|
|
||||||
bad_fields: vec![],
|
|
||||||
|
|
||||||
unparsed: vec![
|
|
||||||
"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,
|
|
||||||
§ion::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 }),
|
|
||||||
mime: section::MIMESection {
|
|
||||||
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::MIMESection::default()
|
|
||||||
},
|
|
||||||
..section::Section::default()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parser_bodystruct<'a, F>(input: &'a [u8], func: F) -> ()
|
|
||||||
where
|
|
||||||
F: FnOnce(&part::PartNode) -> (),
|
|
||||||
{
|
|
||||||
let seg = multipass::segment::new(input).unwrap();
|
|
||||||
let charset = seg.charset();
|
|
||||||
let fields = charset.fields().unwrap();
|
|
||||||
let field_names = fields.names();
|
|
||||||
let field_body = field_names.body();
|
|
||||||
let section = field_body.section();
|
|
||||||
let bodystruct = section.body_structure();
|
|
||||||
|
|
||||||
func(&bodystruct.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multipart() {
|
|
||||||
let fullmail: &[u8] = r#"Date: Sat, 8 Jul 2023 07:14:29 +0200
|
|
||||||
From: Grrrnd Zero <grrrndzero@example.org>
|
|
||||||
To: John Doe <jdoe@machine.example>
|
|
||||||
Subject: Re: Saying Hello
|
|
||||||
Message-ID: <NTAxNzA2AC47634Y366BAMTY4ODc5MzQyODY0ODY5@www.grrrndzero.org>
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/alternative;
|
|
||||||
boundary="b1_e376dc71bafc953c0b0fdeb9983a9956"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
|
||||||
|
|
||||||
--b1_e376dc71bafc953c0b0fdeb9983a9956
|
|
||||||
Content-Type: text/plain; charset=utf-8
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
GZ
|
|
||||||
OoOoO
|
|
||||||
oOoOoOoOo
|
|
||||||
oOoOoOoOoOoOoOoOo
|
|
||||||
oOoOoOoOoOoOoOoOoOoOoOo
|
|
||||||
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
|
|
||||||
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
|
|
||||||
|
|
||||||
--b1_e376dc71bafc953c0b0fdeb9983a9956
|
|
||||||
Content-Type: text/html; charset=us-ascii
|
|
||||||
|
|
||||||
<div style="text-align: center;"><strong>GZ</strong><br />
|
|
||||||
OoOoO<br />
|
|
||||||
oOoOoOoOo<br />
|
|
||||||
oOoOoOoOoOoOoOoOo<br />
|
|
||||||
oOoOoOoOoOoOoOoOoOoOoOo<br />
|
|
||||||
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo<br />
|
|
||||||
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />
|
|
||||||
|
|
||||||
--b1_e376dc71bafc953c0b0fdeb9983a9956--
|
|
||||||
"#.as_bytes();
|
|
||||||
|
|
||||||
parser_bodystruct(fullmail, |part| {
|
|
||||||
assert_eq!(part, &part::PartNode::Composite(
|
|
||||||
part::PartHeader {
|
|
||||||
..part::PartHeader::default()
|
|
||||||
},
|
|
||||||
vec![
|
|
||||||
part::PartNode::Discrete(
|
|
||||||
part::PartHeader {
|
|
||||||
..part::PartHeader::default()
|
|
||||||
},
|
|
||||||
r#"GZ
|
|
||||||
OoOoO
|
|
||||||
oOoOoOoOo
|
|
||||||
oOoOoOoOoOoOoOoOo
|
|
||||||
oOoOoOoOoOoOoOoOoOoOoOo
|
|
||||||
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
|
|
||||||
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO"#.as_bytes()
|
|
||||||
),
|
|
||||||
part::PartNode::Discrete(
|
|
||||||
part::PartHeader {
|
|
||||||
..part::PartHeader::default()
|
|
||||||
},
|
|
||||||
r#"<div style="text-align: center;"><strong>GZ</strong><br />
|
|
||||||
OoOoO<br />
|
|
||||||
oOoOoOoOo<br />
|
|
||||||
oOoOoOoOoOoOoOoOo<br />
|
|
||||||
oOoOoOoOoOoOoOoOoOoOoOo<br />
|
|
||||||
oOoOoOoOoOoOoOoOoOoOoOoOoOoOo<br />
|
|
||||||
OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />"#.as_bytes()
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in a new issue