Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve mailbox parsing using chumsky #839

Merged
merged 31 commits into from Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2daec8c
init address parsers
soywod Dec 10, 2022
6f32256
clean implem
soywod Dec 17, 2022
c709622
fix tests
soywod Dec 17, 2022
fe232df
store all chumsky errors instead of the first one
soywod Dec 17, 2022
08f0a4f
fix clippy error
soywod Dec 17, 2022
38838b6
fix comments part 1
soywod Jan 7, 2023
33e4af9
split parsers into rfc files
soywod Jan 7, 2023
ed35bc3
simplify implem
soywod Jan 7, 2023
adff054
fix comment part 2
soywod Jan 7, 2023
db743f7
fix ci
soywod Jan 8, 2023
b81cd11
fix typos
soywod Jan 8, 2023
e167cb0
add back obs-phrase to accept dots in display names
soywod Jan 9, 2023
e91b5e2
improve parsers
soywod Jan 9, 2023
475c420
put back once_cell feature
soywod Jan 9, 2023
e9d8214
add parsing benchmarks
soywod Jan 9, 2023
ce0a0aa
fix ci
soywod Jan 9, 2023
4b0ce9a
fix bench, make mailbox parsers consume all
soywod Jan 9, 2023
f1b64e7
replace simple errors my cheap ones
soywod Jan 10, 2023
91736a7
rearrange choices order
soywod Jan 10, 2023
8019bfa
simplify parsers, improve perfs
soywod Jan 28, 2023
4d819a8
fix clippy errors
soywod Jan 28, 2023
914bf8a
fix missing clippy errors
soywod Jan 28, 2023
c6b44b5
fix typo
soywod Jan 28, 2023
fac4e24
fix last clippy error
soywod Jan 28, 2023
e04a5b6
fix last clippy error bis
soywod Jan 28, 2023
25d2394
fix clippy tests
soywod Jan 28, 2023
736ddb2
optimize cfws parser
soywod Jan 28, 2023
c4b1e81
update comment
soywod Jan 28, 2023
248f704
Merge branch 'master' into master
soywod Jan 29, 2023
fefb489
Merge branch 'master' into master
paolobarbolini Feb 20, 2023
3ae79a7
I messed up code formatting while resolving the merge conflict from t…
paolobarbolini Feb 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Expand Up @@ -19,6 +19,7 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }

[dependencies]
chumsky = "0.8.0"
idna = "0.3"
once_cell = { version = "1", optional = true }
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
Expand Down Expand Up @@ -88,6 +89,10 @@ maud = "0.24"
harness = false
name = "transport_smtp"

[[bench]]
harness = false
name = "mailbox_parsing"

[features]
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -89,7 +89,7 @@ let mailer = SmtpTransport::relay("smtp.gmail.com")
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
```

Expand Down
27 changes: 27 additions & 0 deletions benches/mailbox_parsing.rs
@@ -0,0 +1,27 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::message::{Mailbox, Mailboxes};

fn bench_parse_single(mailbox: &str) {
assert!(mailbox.parse::<Mailbox>().is_ok());
}

fn bench_parse_multiple(mailboxes: &str) {
assert!(mailboxes.parse::<Mailboxes>().is_ok());
}

fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("parse single mailbox", |b| {
b.iter(|| bench_parse_single(black_box("\"Benchmark test\" <test@mail.local>")))
});

c.bench_function("parse multiple mailboxes", |b| {
b.iter(|| {
bench_parse_multiple(black_box(
"\"Benchmark test\" <test@mail.local>, Test <test@mail.local>, <test@mail.local>, test@mail.local",
))
})
});
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
2 changes: 1 addition & 1 deletion examples/asyncstd1_smtp_starttls.rs
Expand Up @@ -27,6 +27,6 @@ async fn main() {
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
2 changes: 1 addition & 1 deletion examples/asyncstd1_smtp_tls.rs
Expand Up @@ -27,6 +27,6 @@ async fn main() {
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
2 changes: 1 addition & 1 deletion examples/smtp.rs
Expand Up @@ -17,6 +17,6 @@ fn main() {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
2 changes: 1 addition & 1 deletion examples/smtp_selfsigned.rs
Expand Up @@ -39,6 +39,6 @@ fn main() {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
soywod marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 1 addition & 1 deletion examples/smtp_starttls.rs
Expand Up @@ -22,6 +22,6 @@ fn main() {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
2 changes: 1 addition & 1 deletion examples/smtp_tls.rs
Expand Up @@ -22,6 +22,6 @@ fn main() {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
2 changes: 1 addition & 1 deletion examples/tokio1_smtp_starttls.rs
Expand Up @@ -31,6 +31,6 @@ async fn main() {
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
2 changes: 1 addition & 1 deletion examples/tokio1_smtp_tls.rs
Expand Up @@ -31,6 +31,6 @@ async fn main() {
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
6 changes: 5 additions & 1 deletion src/address/types.rs
Expand Up @@ -184,7 +184,7 @@ where
let domain = domain.as_ref();
Address::check_domain(domain)?;

let serialized = format!("{}@{}", user, domain);
let serialized = format!("{user}@{domain}");
Ok(Address {
serialized,
at_start: user.len(),
Expand Down Expand Up @@ -227,6 +227,7 @@ fn check_address(val: &str) -> Result<usize, AddressError> {
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
/// Errors in email addresses parsing
pub enum AddressError {
/// Missing domain or user
Expand All @@ -237,6 +238,8 @@ pub enum AddressError {
InvalidUser,
/// Invalid email domain
InvalidDomain,
/// Invalid input found
InvalidInput,
}

impl Error for AddressError {}
Expand All @@ -248,6 +251,7 @@ impl Display for AddressError {
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidInput => f.write_str("Invalid input"),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/message/attachment.rs
Expand Up @@ -96,7 +96,7 @@ impl Attachment {
builder.header(header::ContentDisposition::attachment(&filename))
}
Disposition::Inline(content_id) => builder
.header(header::ContentId::from(format!("<{}>", content_id)))
.header(header::ContentId::from(format!("<{content_id}>")))
.header(header::ContentDisposition::inline()),
};
builder = builder.header(content_type);
Expand Down
2 changes: 1 addition & 1 deletion src/message/dkim.rs
Expand Up @@ -588,7 +588,7 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
);
let signed = message.formatted();
let signed = std::str::from_utf8(&signed).unwrap();
println!("{}", signed);
println!("{signed}");
assert_eq!(
signed,
std::concat!(
Expand Down
6 changes: 3 additions & 3 deletions src/message/header/content_disposition.rs
Expand Up @@ -33,7 +33,7 @@ impl ContentDisposition {
}

fn with_name(kind: &str, file_name: &str) -> Self {
let raw_value = format!("{}; filename=\"{}\"", kind, file_name);
let raw_value = format!("{kind}; filename=\"{file_name}\"");

let mut encoded_value = String::new();
let line_len = "Content-Disposition: ".len();
Expand Down Expand Up @@ -90,12 +90,12 @@ mod test {

headers.set(ContentDisposition::inline());

assert_eq!(format!("{}", headers), "Content-Disposition: inline\r\n");
assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");

headers.set(ContentDisposition::attachment("something.txt"));

assert_eq!(
format!("{}", headers),
format!("{headers}"),
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
);
}
Expand Down
3 changes: 1 addition & 2 deletions src/message/header/content_type.rs
Expand Up @@ -135,8 +135,7 @@ mod serde {
match ContentType::parse(mime) {
Ok(content_type) => Ok(content_type),
Err(_) => Err(E::custom(format!(
"Couldn't parse the following MIME-Type: {}",
mime
"Couldn't parse the following MIME-Type: {mime}"
))),
}
}
Expand Down
35 changes: 31 additions & 4 deletions src/message/header/mailbox.rs
Expand Up @@ -306,14 +306,30 @@ mod test {
#[test]
fn parse_multi_with_name_containing_comma() {
let from: Vec<Mailbox> = vec![
"Test, test <1@example.com>".parse().unwrap(),
"Test2, test2 <2@example.com>".parse().unwrap(),
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, test2\" <2@example.com>".parse().unwrap(),
];

let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_string(),
"\"Test, test\" <1@example.com>, \"Test2, test2\" <2@example.com>".to_string(),
));

assert_eq!(headers.get::<From>(), Some(From(from.into())));
}

#[test]
fn parse_multi_with_name_containing_double_quotes() {
let from: Vec<Mailbox> = vec![
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, \"test2\"\" <2@example.com>".parse().unwrap(),
];

let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"Test, test\" <1@example.com>, \"Test2, \"test2\"\" <2@example.com>".to_string(),
));

assert_eq!(headers.get::<From>(), Some(From(from.into())));
Expand All @@ -324,9 +340,20 @@ mod test {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2".to_string(),
"\"Test, test\" <1@example.com>, \"Test2, test2\"".to_string(),
));

assert_eq!(headers.get::<From>(), None);
}

#[test]
fn mailbox_format_address_with_angle_bracket() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("<3".into()), "i@love.example".parse().unwrap())
),
r#""<3" <i@love.example>"#
);
}
soywod marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions src/message/mailbox/mod.rs
@@ -1,3 +1,4 @@
mod parsers;
#[cfg(feature = "serde")]
mod serde;
mod types;
Expand Down
5 changes: 5 additions & 0 deletions src/message/mailbox/parsers/mod.rs
@@ -0,0 +1,5 @@
mod rfc2234;
mod rfc2822;
mod rfc5336;

pub(crate) use rfc2822::{mailbox, mailbox_list};
32 changes: 32 additions & 0 deletions src/message/mailbox/parsers/rfc2234.rs
@@ -0,0 +1,32 @@
//! Partial parsers implementation of [RFC2234]: Augmented BNF for
//! Syntax Specifications: ABNF.
//!
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234

use chumsky::{error::Cheap, prelude::*};

// 6.1 Core Rules
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1

// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_alphabetic())
soywod marked this conversation as resolved.
Show resolved Hide resolved
}

// DIGIT = %x30-39
// ; 0-9
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_digit())
}

// DQUOTE = %x22
// ; " (Double Quote)
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
just('"')
}

// WSP = SP / HTAB
// ; white space
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
choice((just(' '), just('\t')))
}
soywod marked this conversation as resolved.
Show resolved Hide resolved