Compare commits

..

8 Commits

Author SHA1 Message Date
Paolo Barbolini
12dccd2bbe Prepare v0.11.19 (#1115) 2025-10-08 19:23:57 +02:00
Leo-Tinkeam
7899da9672 docs: fix readme example (#1114)
Signed-off-by: Léo-Tinkeam <sarcyleo@gmail.com>
2025-10-01 22:07:44 +02:00
Paolo Barbolini
a85fdefe6f build(deps): upgrade semver compatible dependencies (#1113) 2025-10-01 15:24:41 +02:00
Paolo Barbolini
b73611c67f refactor: replace static_assert! with std assert! (#1112) 2025-10-01 14:45:08 +02:00
Norbiros
55cea7dbe6 feat: add method to set raw custom headers in MessageBuilder (#1108)
Closes #661
2025-10-01 14:32:57 +02:00
Paolo Barbolini
d2b9d50000 build(deps): upgrade semver compatible dependencies (#1103) 2025-08-10 17:16:37 +00:00
Paolo Barbolini
3d16344d53 Prepare v0.11.18 (#1102) 2025-07-28 09:44:38 +02:00
Francesco Luzzi
8873153178 feat: add ability to give a name to an inline attachment (#1101) 2025-07-21 08:19:31 +02:00
12 changed files with 650 additions and 606 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"]

View File

@@ -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!"))

View File

@@ -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)]

View File

@@ -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"
)
);
}
}

View File

@@ -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;
}

View File

@@ -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')))
}

View File

@@ -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())
}

View File

@@ -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)
}

View File

@@ -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
})?;

View File

@@ -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();