Compare commits
8 Commits
chumsky-to
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12dccd2bbe | ||
|
|
7899da9672 | ||
|
|
a85fdefe6f | ||
|
|
b73611c67f | ||
|
|
55cea7dbe6 | ||
|
|
d2b9d50000 | ||
|
|
3d16344d53 | ||
|
|
8873153178 |
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,3 +1,33 @@
|
||||
<a name="v0.11.19"></a>
|
||||
### v0.11.19 (2025-10-08)
|
||||
|
||||
#### Features
|
||||
|
||||
* Add raw header setter to `MessageBuilder` ([#1108])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Fix README example ([#1114])
|
||||
* Replace custom `static_assert!` macro with `std::assert!` ([#1112])
|
||||
|
||||
[#1108]: https://github.com/lettre/lettre/pull/1108
|
||||
[#1112]: https://github.com/lettre/lettre/pull/1112
|
||||
[#1114]: https://github.com/lettre/lettre/pull/1114
|
||||
|
||||
<a name="v0.11.18"></a>
|
||||
### v0.11.18 (2025-07-28)
|
||||
|
||||
#### Features
|
||||
|
||||
* Allow inline attachments to be named ([#1101])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Upgrade `socket2` to v0.6 ([#1098])
|
||||
|
||||
[#1098]: https://github.com/lettre/lettre/pull/1098
|
||||
[#1101]: https://github.com/lettre/lettre/pull/1101
|
||||
|
||||
<a name="v0.11.17"></a>
|
||||
### v0.11.17 (2025-06-06)
|
||||
|
||||
|
||||
808
Cargo.lock
generated
808
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lettre"
|
||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||
version = "0.11.17"
|
||||
version = "0.11.19"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "https://lettre.rs"
|
||||
@@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
email_address = { version = "0.2.1", default-features = false }
|
||||
nom = "8"
|
||||
chumsky = "0.9"
|
||||
idna = "1"
|
||||
|
||||
## tracing support
|
||||
@@ -40,6 +40,7 @@ serde = { version = "1.0.110", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
# smtp-transport
|
||||
nom = { version = "8", optional = true }
|
||||
hostname = { version = "0.4", optional = true } # feature
|
||||
socket2 = { version = "0.6", optional = true }
|
||||
url = { version = "2.4", optional = true }
|
||||
@@ -106,7 +107,7 @@ mime03 = ["dep:mime"]
|
||||
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
|
||||
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
||||
smtp-transport = ["dep:base64", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||
|
||||
pool = ["dep:futures-util"]
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -28,8 +28,8 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.11.17">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.17/status.svg"
|
||||
<a href="https://deps.rs/crate/lettre/0.11.19">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.19/status.svg"
|
||||
alt="dependency status" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -67,15 +67,15 @@ lettre = "0.11"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::message::{Mailbox, header::ContentType};
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = Message::builder()
|
||||
.from(Mailbox::new("NoBody".to_owned(), "nobody@domain.tld".parse().unwrap()))
|
||||
.reply_to(Mailbox::new("Yuin".to_owned(), "yuin@domain.tld".parse().unwrap()))
|
||||
.to(Mailbox::new("Hei".to_owned(), "hei@domain.tld".parse().unwrap()))
|
||||
.from(Mailbox::new(Some("NoBody".to_owned()), "nobody@domain.tld".parse().unwrap()))
|
||||
.reply_to(Mailbox::new(Some("Yuin".to_owned()), "yuin@domain.tld".parse().unwrap()))
|
||||
.to(Mailbox::new(Some("Hei".to_owned()), "hei@domain.tld".parse().unwrap()))
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
//! [mime 0.3]: https://docs.rs/mime/0.3
|
||||
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.17")]
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.19")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
@@ -16,7 +16,10 @@ enum Disposition {
|
||||
/// File name
|
||||
Attached(String),
|
||||
/// Content id
|
||||
Inline(String),
|
||||
Inline {
|
||||
content_id: String,
|
||||
name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Attachment {
|
||||
@@ -81,7 +84,50 @@ impl Attachment {
|
||||
/// ```
|
||||
pub fn new_inline(content_id: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Inline(content_id),
|
||||
disposition: Disposition::Inline {
|
||||
content_id,
|
||||
name: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new inline attachment giving it a name
|
||||
///
|
||||
/// This attachment should be displayed inline into the message
|
||||
/// body:
|
||||
///
|
||||
/// ```html
|
||||
/// <img src="cid:123">
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::error::Error;
|
||||
/// use std::fs;
|
||||
///
|
||||
/// use lettre::message::{header::ContentType, Attachment};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let content_id = String::from("123");
|
||||
/// let file_name = String::from("image.jpg");
|
||||
/// # if false {
|
||||
/// let filebody = fs::read(&file_name)?;
|
||||
/// # }
|
||||
/// # let filebody = fs::read("docs/lettre.png")?;
|
||||
/// let content_type = ContentType::parse("image/jpeg").unwrap();
|
||||
/// let attachment =
|
||||
/// Attachment::new_inline_with_name(content_id, file_name).body(filebody, content_type);
|
||||
///
|
||||
/// // The image `attachment` will display inline into the email.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new_inline_with_name(content_id: String, name: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Inline {
|
||||
content_id,
|
||||
name: Some(name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +141,18 @@ impl Attachment {
|
||||
Disposition::Attached(filename) => {
|
||||
builder.header(header::ContentDisposition::attachment(&filename))
|
||||
}
|
||||
Disposition::Inline(content_id) => builder
|
||||
Disposition::Inline {
|
||||
content_id,
|
||||
name: None,
|
||||
} => builder
|
||||
.header(header::ContentId::from(format!("<{content_id}>")))
|
||||
.header(header::ContentDisposition::inline()),
|
||||
Disposition::Inline {
|
||||
content_id,
|
||||
name: Some(name),
|
||||
} => builder
|
||||
.header(header::ContentId::from(format!("<{content_id}>")))
|
||||
.header(header::ContentDisposition::inline_with_name(&name)),
|
||||
};
|
||||
builder = builder.header(content_type);
|
||||
builder.body(content)
|
||||
@@ -142,4 +197,24 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_inline_with_name() {
|
||||
let id = String::from("id");
|
||||
let name = String::from("test");
|
||||
let part = super::Attachment::new_inline_with_name(id, name).body(
|
||||
String::from("Hello world!"),
|
||||
ContentType::parse("text/plain").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8_lossy(&part.formatted()),
|
||||
concat!(
|
||||
"Content-ID: <id>\r\n",
|
||||
"Content-Disposition: inline; filename=\"test\"\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n\r\n",
|
||||
"Hello world!\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,21 +186,15 @@ impl HeaderName {
|
||||
|
||||
/// Creates a new header name, panics on invalid name
|
||||
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
|
||||
macro_rules! static_assert {
|
||||
($condition:expr) => {
|
||||
let _ = [()][(!($condition)) as usize];
|
||||
};
|
||||
}
|
||||
|
||||
static_assert!(!ascii.is_empty());
|
||||
static_assert!(ascii.len() <= 76);
|
||||
assert!(!ascii.is_empty());
|
||||
assert!(ascii.len() <= 76);
|
||||
assert!(ascii.is_ascii());
|
||||
|
||||
let bytes = ascii.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
static_assert!(bytes[i].is_ascii());
|
||||
static_assert!(bytes[i] != b' ');
|
||||
static_assert!(bytes[i] != b':');
|
||||
assert!(bytes[i] != b' ');
|
||||
assert!(bytes[i] != b':');
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
@@ -3,34 +3,30 @@
|
||||
//!
|
||||
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
|
||||
|
||||
use nom::{
|
||||
branch::alt,
|
||||
character::complete::{char, satisfy},
|
||||
IResult, Parser,
|
||||
};
|
||||
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(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| c.is_ascii_alphabetic()).parse(input)
|
||||
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.is_ascii_alphabetic())
|
||||
}
|
||||
|
||||
// DIGIT = %x30-39
|
||||
// ; 0-9
|
||||
pub(super) fn digit(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| c.is_ascii_digit()).parse(input)
|
||||
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(input: &str) -> IResult<&str, char> {
|
||||
char('"').parse(input)
|
||||
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
just('"')
|
||||
}
|
||||
|
||||
// WSP = SP / HTAB
|
||||
// ; white space
|
||||
pub(super) fn wsp(input: &str) -> IResult<&str, char> {
|
||||
alt((char(' '), char('\t'))).parse(input)
|
||||
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((just(' '), just('\t')))
|
||||
}
|
||||
|
||||
@@ -3,14 +3,7 @@
|
||||
//!
|
||||
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
|
||||
|
||||
use nom::{
|
||||
branch::alt,
|
||||
character::complete::{char, satisfy},
|
||||
combinator::{eof, map, opt},
|
||||
multi::{fold_many0, fold_many1, many0, many1, separated_list0},
|
||||
sequence::{delimited, pair, preceded, terminated},
|
||||
IResult, Parser,
|
||||
};
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
|
||||
use super::{rfc2234, rfc5336};
|
||||
|
||||
@@ -22,8 +15,8 @@ use super::{rfc2234, rfc5336};
|
||||
// %d12 / ; carriage return, line feed,
|
||||
// %d14-31 / ; and white space characters
|
||||
// %d127
|
||||
fn no_ws_ctl(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| matches!(u32::from(c), 1..=8 | 11 | 12 | 14..=31 | 127)).parse(input)
|
||||
fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c| matches!(u32::from(*c), 1..=8 | 11 | 12 | 14..=31 | 127))
|
||||
}
|
||||
|
||||
// text = %d1-9 / ; Characters excluding CR and LF
|
||||
@@ -31,16 +24,16 @@ fn no_ws_ctl(input: &str) -> IResult<&str, char> {
|
||||
// %d12 /
|
||||
// %d14-127 /
|
||||
// obs-text
|
||||
fn text(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| matches!(u32::from(c), 1..=9 | 11 | 12 | 14..=127)).parse(input)
|
||||
fn text() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c| matches!(u32::from(*c), 1..=9 | 11 | 12 | 14..=127))
|
||||
}
|
||||
|
||||
// 3.2.2. Quoted characters
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
|
||||
|
||||
// quoted-pair = ("\" text) / obs-qp
|
||||
fn quoted_pair(input: &str) -> IResult<&str, char> {
|
||||
preceded(char('\\'), text).parse(input)
|
||||
fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
just('\\').ignore_then(text())
|
||||
}
|
||||
|
||||
// 3.2.3. Folding white space and comments
|
||||
@@ -48,19 +41,17 @@ fn quoted_pair(input: &str) -> IResult<&str, char> {
|
||||
|
||||
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
|
||||
// obs-FWS
|
||||
pub(super) fn fws(input: &str) -> IResult<&str, Option<char>> {
|
||||
map(
|
||||
pair(opt(rfc2234::wsp), many0(rfc2234::wsp)),
|
||||
|(first, _rest)| first,
|
||||
)
|
||||
.parse(input)
|
||||
pub(super) fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
rfc2234::wsp()
|
||||
.or_not()
|
||||
.then_ignore(rfc2234::wsp().ignored().repeated())
|
||||
}
|
||||
|
||||
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
|
||||
pub(super) fn cfws(input: &str) -> IResult<&str, Option<char>> {
|
||||
pub(super) fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
// TODO: comment are not currently supported, so for now a cfws is
|
||||
// the same as a fws.
|
||||
fws(input)
|
||||
fws()
|
||||
}
|
||||
|
||||
// 3.2.4. Atom
|
||||
@@ -77,13 +68,13 @@ pub(super) fn cfws(input: &str) -> IResult<&str, Option<char>> {
|
||||
// "`" / "{" /
|
||||
// "|" / "}" /
|
||||
// "~"
|
||||
pub(super) fn atext(input: &str) -> IResult<&str, char> {
|
||||
alt((
|
||||
rfc2234::alpha,
|
||||
rfc2234::digit,
|
||||
satisfy(|c| {
|
||||
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((
|
||||
rfc2234::alpha(),
|
||||
rfc2234::digit(),
|
||||
filter(|c| {
|
||||
matches!(
|
||||
c,
|
||||
*c,
|
||||
'!' | '#'
|
||||
| '$'
|
||||
| '%'
|
||||
@@ -105,64 +96,29 @@ pub(super) fn atext(input: &str) -> IResult<&str, char> {
|
||||
)
|
||||
}),
|
||||
// also allow non ASCII UTF8 chars
|
||||
rfc5336::utf8_non_ascii,
|
||||
rfc5336::utf8_non_ascii(),
|
||||
))
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// atom = [CFWS] 1*atext [CFWS]
|
||||
pub(super) fn atom(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
pair(
|
||||
cfws,
|
||||
fold_many1(atext, String::new, |mut acc, c| {
|
||||
acc.push(c);
|
||||
acc
|
||||
}),
|
||||
),
|
||||
|(cfws, mut chars)| {
|
||||
if let Some(cfws) = cfws {
|
||||
chars.insert(0, cfws);
|
||||
}
|
||||
chars
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
cfws().chain(atext().repeated().at_least(1))
|
||||
}
|
||||
|
||||
// dot-atom = [CFWS] dot-atom-text [CFWS]
|
||||
pub(super) fn dot_atom(input: &str) -> IResult<&str, String> {
|
||||
map(pair(cfws, dot_atom_text), |(_cfws, text)| text).parse(input)
|
||||
pub(super) fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
cfws().chain(dot_atom_text())
|
||||
}
|
||||
|
||||
// dot-atom-text = 1*atext *("." 1*atext)
|
||||
pub(super) fn dot_atom_text(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
pair(
|
||||
fold_many1(atext, String::new, |mut acc, c| {
|
||||
acc.push(c);
|
||||
acc
|
||||
}),
|
||||
many0(map(
|
||||
pair(
|
||||
char('.'),
|
||||
fold_many1(atext, String::new, |mut acc, c| {
|
||||
acc.push(c);
|
||||
acc
|
||||
}),
|
||||
),
|
||||
|(dot, chars)| format!("{dot}{chars}"),
|
||||
)),
|
||||
),
|
||||
|(first, rest)| {
|
||||
let mut result = first;
|
||||
for part in rest {
|
||||
result.push_str(&part);
|
||||
}
|
||||
result
|
||||
},
|
||||
pub(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
atext().repeated().at_least(1).chain(
|
||||
just('.')
|
||||
.chain(atext().repeated().at_least(1))
|
||||
.repeated()
|
||||
.at_least(1)
|
||||
.flatten(),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// 3.2.5. Quoted strings
|
||||
@@ -173,168 +129,122 @@ pub(super) fn dot_atom_text(input: &str) -> IResult<&str, String> {
|
||||
// %d33 / ; The rest of the US-ASCII
|
||||
// %d35-91 / ; characters not including "\"
|
||||
// %d93-126 ; or the quote character
|
||||
fn qtext(input: &str) -> IResult<&str, char> {
|
||||
alt((
|
||||
satisfy(|c| matches!(u32::from(c), 33 | 35..=91 | 93..=126)),
|
||||
no_ws_ctl,
|
||||
fn qtext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((
|
||||
filter(|c| matches!(u32::from(*c), 33 | 35..=91 | 93..=126)),
|
||||
no_ws_ctl(),
|
||||
))
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// qcontent = qtext / quoted-pair
|
||||
pub(super) fn qcontent(input: &str) -> IResult<&str, char> {
|
||||
alt((qtext, quoted_pair, rfc5336::utf8_non_ascii)).parse(input)
|
||||
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii()))
|
||||
}
|
||||
|
||||
// quoted-string = [CFWS]
|
||||
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
|
||||
// [CFWS]
|
||||
fn quoted_string(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
delimited(
|
||||
rfc2234::dquote,
|
||||
fold_many0(
|
||||
map(pair(fws, qcontent), |(_fws, c)| c),
|
||||
String::new,
|
||||
|mut acc, c| {
|
||||
acc.push(c);
|
||||
acc
|
||||
},
|
||||
),
|
||||
preceded(many0(satisfy(char::is_whitespace)), rfc2234::dquote),
|
||||
),
|
||||
|s| s,
|
||||
)
|
||||
.parse(input)
|
||||
fn quoted_string() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
rfc2234::dquote()
|
||||
.ignore_then(fws().chain(qcontent()).repeated().flatten())
|
||||
.then_ignore(text::whitespace())
|
||||
.then_ignore(rfc2234::dquote())
|
||||
}
|
||||
|
||||
// 3.2.6. Miscellaneous tokens
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
|
||||
|
||||
// word = atom / quoted-string
|
||||
fn word(input: &str) -> IResult<&str, String> {
|
||||
alt((quoted_string, atom)).parse(input)
|
||||
fn word() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((quoted_string(), atom()))
|
||||
}
|
||||
|
||||
// phrase = 1*word / obs-phrase
|
||||
fn phrase(input: &str) -> IResult<&str, String> {
|
||||
alt((obs_phrase, map(many1(word), |words| words.join(" ")))).parse(input)
|
||||
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((obs_phrase(), word().repeated().at_least(1).flatten()))
|
||||
}
|
||||
|
||||
// 3.4. Address Specification
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||
|
||||
// mailbox = name-addr / addr-spec
|
||||
pub(crate) fn mailbox(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
|
||||
terminated(alt((name_addr, map(addr_spec, |addr| (None, addr)))), eof).parse(input)
|
||||
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>>
|
||||
{
|
||||
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
|
||||
.padded()
|
||||
.then_ignore(end())
|
||||
}
|
||||
|
||||
// name-addr = [display-name] angle-addr
|
||||
fn name_addr(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
|
||||
pair(opt(display_name), angle_addr).parse(input)
|
||||
fn name_addr() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> {
|
||||
display_name().collect().or_not().then(angle_addr())
|
||||
}
|
||||
|
||||
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
|
||||
fn angle_addr(input: &str) -> IResult<&str, (String, String)> {
|
||||
delimited((cfws, char('<')), addr_spec, (char('>'), cfws)).parse(input)
|
||||
fn angle_addr() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||
addr_spec()
|
||||
.delimited_by(just('<').ignored(), just('>').ignored())
|
||||
.padded()
|
||||
}
|
||||
|
||||
// display-name = phrase
|
||||
fn display_name(input: &str) -> IResult<&str, String> {
|
||||
phrase(input)
|
||||
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
phrase()
|
||||
}
|
||||
|
||||
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn mailbox_list(input: &str) -> IResult<&str, Vec<(Option<String>, (String, String))>> {
|
||||
terminated(
|
||||
separated_list0(
|
||||
delimited(
|
||||
many0(satisfy(char::is_whitespace)),
|
||||
char(','),
|
||||
many0(satisfy(char::is_whitespace)),
|
||||
),
|
||||
alt((name_addr, map(addr_spec, |addr| (None, addr)))),
|
||||
),
|
||||
eof,
|
||||
)
|
||||
.parse(input)
|
||||
pub(crate) fn mailbox_list(
|
||||
) -> impl Parser<char, Vec<(Option<String>, (String, String))>, Error = Cheap<char>> {
|
||||
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
|
||||
.separated_by(just(',').padded())
|
||||
.then_ignore(end())
|
||||
}
|
||||
|
||||
// 3.4.1. Addr-spec specification
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
|
||||
|
||||
// addr-spec = local-part "@" domain
|
||||
pub(super) fn addr_spec(input: &str) -> IResult<&str, (String, String)> {
|
||||
pair(terminated(local_part, char('@')), domain).parse(input)
|
||||
pub(super) fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||
local_part()
|
||||
.collect()
|
||||
.then_ignore(just('@'))
|
||||
.then(domain().collect())
|
||||
}
|
||||
|
||||
// local-part = dot-atom / quoted-string / obs-local-part
|
||||
pub(super) fn local_part(input: &str) -> IResult<&str, String> {
|
||||
alt((dot_atom, quoted_string, obs_local_part)).parse(input)
|
||||
pub(super) fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((dot_atom(), quoted_string(), obs_local_part()))
|
||||
}
|
||||
|
||||
// domain = dot-atom / domain-literal / obs-domain
|
||||
pub(super) fn domain(input: &str) -> IResult<&str, String> {
|
||||
pub(super) fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
// NOTE: omitting domain-literal since it may never be used
|
||||
alt((dot_atom, obs_domain)).parse(input)
|
||||
choice((dot_atom(), obs_domain()))
|
||||
}
|
||||
|
||||
// 4.1. Miscellaneous obsolete tokens
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.1
|
||||
|
||||
// obs-phrase = word *(word / "." / CFWS)
|
||||
fn obs_phrase(input: &str) -> IResult<&str, String> {
|
||||
fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
// NOTE: the CFWS is already captured by the word, no need to add
|
||||
// it there.
|
||||
map(
|
||||
pair(word, many0(alt((word, map(char('.'), |c| c.to_string()))))),
|
||||
|(first, rest)| {
|
||||
let mut result = first;
|
||||
for part in rest {
|
||||
result.push_str(&part);
|
||||
}
|
||||
result
|
||||
},
|
||||
word().chain(
|
||||
choice((word(), just('.').repeated().exactly(1)))
|
||||
.repeated()
|
||||
.flatten(),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// 4.4. Obsolete Addressing
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
|
||||
|
||||
// obs-local-part = word *("." word)
|
||||
pub(super) fn obs_local_part(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
pair(
|
||||
word,
|
||||
many0(map(pair(char('.'), word), |(dot, w)| format!("{dot}{w}"))),
|
||||
),
|
||||
|(first, rest)| {
|
||||
let mut result = first;
|
||||
for part in rest {
|
||||
result.push_str(&part);
|
||||
}
|
||||
result
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
pub(super) fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
word().chain(just('.').chain(word()).repeated().flatten())
|
||||
}
|
||||
|
||||
// obs-domain = atom *("." atom)
|
||||
pub(super) fn obs_domain(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
pair(
|
||||
atom,
|
||||
many0(map(pair(char('.'), atom), |(dot, a)| format!("{dot}{a}"))),
|
||||
),
|
||||
|(first, rest)| {
|
||||
let mut result = first;
|
||||
for part in rest {
|
||||
result.push_str(&part);
|
||||
}
|
||||
result
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
pub(super) fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
atom().chain(just('.').chain(atom()).repeated().flatten())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//!
|
||||
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
|
||||
|
||||
use nom::{character::complete::satisfy, IResult, Parser};
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
|
||||
// 3.3. Extended Mailbox Address Syntax
|
||||
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
|
||||
@@ -12,6 +12,6 @@ use nom::{character::complete::satisfy, IResult, Parser};
|
||||
// UTF8-2 = <See Section 4 of RFC 3629>
|
||||
// UTF8-3 = <See Section 4 of RFC 3629>
|
||||
// UTF8-4 = <See Section 4 of RFC 3629>
|
||||
pub(super) fn utf8_non_ascii(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| c.len_utf8() > 1).parse(input)
|
||||
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.len_utf8() > 1)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use chumsky::prelude::*;
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
use super::parsers;
|
||||
@@ -113,7 +114,7 @@ impl FromStr for Mailbox {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||
let (_rest, (name, (user, domain))) = parsers::mailbox(src).map_err(|_errs| {
|
||||
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
@@ -344,7 +345,7 @@ impl FromStr for Mailboxes {
|
||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||
let mut mailboxes = Vec::new();
|
||||
|
||||
let (_rest, parsed_mailboxes) = parsers::mailbox_list(src).map_err(|_errs| {
|
||||
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
|
||||
@@ -217,7 +217,7 @@ mod mimebody;
|
||||
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
|
||||
message::header::{ContentTransferEncoding, Header, HeaderValue, Headers, MailboxesHeader},
|
||||
Error as EmailError,
|
||||
};
|
||||
|
||||
@@ -369,6 +369,12 @@ impl MessageBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set raw custom header to message
|
||||
pub fn raw_header(mut self, raw_header: HeaderValue) -> Self {
|
||||
self.headers.insert_raw(raw_header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add mailbox to header
|
||||
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
|
||||
match self.headers.get::<H>() {
|
||||
@@ -711,7 +717,10 @@ mod test {
|
||||
.header(header::To(
|
||||
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
|
||||
))
|
||||
.header(header::Subject::from(String::from("яңа ел белән!")))
|
||||
.raw_header(header::HeaderValue::new(
|
||||
header::HeaderName::new_from_ascii_str("Subject"),
|
||||
"яңа ел белән!".to_owned(),
|
||||
))
|
||||
.body(String::from("Happy new year!"))
|
||||
.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user