Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ecb87f9fd | ||
|
|
fd700b1717 | ||
|
|
f8f19d6af5 | ||
|
|
cc25223914 | ||
|
|
750573d38b | ||
|
|
0734a96343 | ||
|
|
3c2f996856 | ||
|
|
9cae29dd07 | ||
|
|
e1a146c8f8 | ||
|
|
840a19784a | ||
|
|
5a61ba36b5 | ||
|
|
dbf0e53c31 | ||
|
|
c914a07379 | ||
|
|
2c4fa39523 | ||
|
|
28f0af16be | ||
|
|
f0614be555 | ||
|
|
a3fcdf263d | ||
|
|
d4da2e1f14 | ||
|
|
5655958288 | ||
|
|
11b4acf0cd | ||
|
|
b3b5df285a | ||
|
|
3c051d52e7 | ||
|
|
d6128a146e | ||
|
|
fab6680150 |
36
.github/workflows/test.yml
vendored
36
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rustfmt:
|
rustfmt:
|
||||||
name: rustfmt / nightly-2022-02-11
|
name: rustfmt / nightly-2022-11-12
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: |
|
run: |
|
||||||
rustup default nightly-2022-02-11
|
rustup default nightly-2022-11-12
|
||||||
rustup component add rustfmt
|
rustup component add rustfmt
|
||||||
|
|
||||||
- name: cargo fmt
|
- name: cargo fmt
|
||||||
@@ -52,17 +52,11 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-check
|
|
||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: rustup update --no-self-update stable
|
run: rustup update --no-self-update stable
|
||||||
|
|
||||||
|
- name: Setup cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Install cargo hack
|
- name: Install cargo hack
|
||||||
run: cargo install cargo-hack --debug
|
run: cargo install cargo-hack --debug
|
||||||
@@ -81,27 +75,21 @@ jobs:
|
|||||||
rust: stable
|
rust: stable
|
||||||
- name: beta
|
- name: beta
|
||||||
rust: beta
|
rust: beta
|
||||||
- name: 1.56.0
|
- name: 1.60.0
|
||||||
rust: 1.56.0
|
rust: 1.60.0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}
|
|
||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: |
|
run: |
|
||||||
rustup default ${{ matrix.rust }}
|
rustup default ${{ matrix.rust }}
|
||||||
rustup update --no-self-update ${{ matrix.rust }}
|
rustup update --no-self-update ${{ matrix.rust }}
|
||||||
|
|
||||||
|
- name: Setup cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Install postfix
|
- name: Install postfix
|
||||||
run: |
|
run: |
|
||||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update
|
DEBIAN_FRONTEND=noninteractive sudo apt-get update
|
||||||
@@ -134,10 +122,10 @@ jobs:
|
|||||||
run: cargo test
|
run: cargo test
|
||||||
|
|
||||||
- name: Test with all features (-native-tls)
|
- name: Test with all features (-native-tls)
|
||||||
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,regex,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots
|
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots
|
||||||
|
|
||||||
- name: Test with all features (-boring-tls)
|
- name: Test with all features (-boring-tls)
|
||||||
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,regex,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots
|
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots
|
||||||
|
|
||||||
# coverage:
|
# coverage:
|
||||||
# name: Coverage
|
# name: Coverage
|
||||||
|
|||||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,3 +1,39 @@
|
|||||||
|
<a name="v0.10.2"></a>
|
||||||
|
### v0.10.2 (2023-01-29)
|
||||||
|
|
||||||
|
#### Upgrade notes
|
||||||
|
|
||||||
|
* MSRV is now 1.60 ([#828])
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* Allow providing a custom `tokio` stream for `AsyncSmtpTransport` ([#805])
|
||||||
|
* Return whole SMTP error message ([#821])
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
|
||||||
|
* Mailbox displays wrongly when containing a comma and a non-ascii char in its name ([#827])
|
||||||
|
* Require `quoted_printable` ^0.4.6 in order to fix encoding of tabs and spaces at the end of line ([#837])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Increase tracing ([#848])
|
||||||
|
* Bump `idna` to 0.3 ([#816])
|
||||||
|
* Update `base64` to 0.21 ([#840] and [#851])
|
||||||
|
* Update `rsa` to 0.8 ([#829] and [#852])
|
||||||
|
|
||||||
|
[#805]: https://github.com/lettre/lettre/pull/805
|
||||||
|
[#816]: https://github.com/lettre/lettre/pull/816
|
||||||
|
[#821]: https://github.com/lettre/lettre/pull/821
|
||||||
|
[#827]: https://github.com/lettre/lettre/pull/827
|
||||||
|
[#828]: https://github.com/lettre/lettre/pull/828
|
||||||
|
[#829]: https://github.com/lettre/lettre/pull/829
|
||||||
|
[#837]: https://github.com/lettre/lettre/pull/837
|
||||||
|
[#840]: https://github.com/lettre/lettre/pull/840
|
||||||
|
[#848]: https://github.com/lettre/lettre/pull/848
|
||||||
|
[#851]: https://github.com/lettre/lettre/pull/851
|
||||||
|
[#852]: https://github.com/lettre/lettre/pull/852
|
||||||
|
|
||||||
<a name="v0.10.1"></a>
|
<a name="v0.10.1"></a>
|
||||||
### v0.10.1 (2022-07-20)
|
### v0.10.1 (2022-07-20)
|
||||||
|
|
||||||
|
|||||||
33
Cargo.toml
33
Cargo.toml
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lettre"
|
name = "lettre"
|
||||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||||
version = "0.10.1"
|
version = "0.10.2"
|
||||||
description = "Email client"
|
description = "Email client"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://lettre.rs"
|
homepage = "https://lettre.rs"
|
||||||
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
|
|||||||
categories = ["email", "network-programming"]
|
categories = ["email", "network-programming"]
|
||||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.56"
|
rust-version = "1.60"
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||||
@@ -19,7 +19,7 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
|||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idna = "0.2"
|
idna = "0.3"
|
||||||
once_cell = { version = "1", optional = true }
|
once_cell = { version = "1", optional = true }
|
||||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@ tracing = { version = "0.1.16", default-features = false, features = ["std"], op
|
|||||||
httpdate = { version = "1", optional = true }
|
httpdate = { version = "1", optional = true }
|
||||||
mime = { version = "0.3.4", optional = true }
|
mime = { version = "0.3.4", optional = true }
|
||||||
fastrand = { version = "1.4", optional = true }
|
fastrand = { version = "1.4", optional = true }
|
||||||
quoted_printable = { version = "0.4", optional = true }
|
quoted_printable = { version = "0.4.6", optional = true }
|
||||||
base64 = { version = "0.13", optional = true }
|
base64 = { version = "0.21", optional = true }
|
||||||
email-encoding = { version = "0.1.1", optional = true }
|
email-encoding = { version = "0.2", optional = true }
|
||||||
|
|
||||||
# file transport
|
# file transport
|
||||||
uuid = { version = "1", features = ["v4"], optional = true }
|
uuid = { version = "1", features = ["v4"], optional = true }
|
||||||
@@ -54,28 +54,27 @@ futures-util = { version = "0.3.7", default-features = false, features = ["io"],
|
|||||||
async-trait = { version = "0.1", optional = true }
|
async-trait = { version = "0.1", optional = true }
|
||||||
|
|
||||||
## async-std
|
## async-std
|
||||||
async-std = { version = "1.8", optional = true, features = ["unstable"] }
|
async-std = { version = "1.8", optional = true }
|
||||||
#async-native-tls = { version = "0.3.3", optional = true }
|
#async-native-tls = { version = "0.3.3", optional = true }
|
||||||
futures-rustls = { version = "0.22", optional = true }
|
futures-rustls = { version = "0.22", optional = true }
|
||||||
|
|
||||||
## tokio
|
## tokio
|
||||||
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "rt", "process", "time", "net", "io-util"], optional = true }
|
tokio1_crate = { package = "tokio", version = "1", optional = true }
|
||||||
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
|
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
|
||||||
tokio1_rustls = { package = "tokio-rustls", version = "0.23", optional = true }
|
tokio1_rustls = { package = "tokio-rustls", version = "0.23", optional = true }
|
||||||
tokio1_boring = { package = "tokio-boring", version = "2.1.4", optional = true }
|
tokio1_boring = { package = "tokio-boring", version = "2.1.4", optional = true }
|
||||||
|
|
||||||
## dkim
|
## dkim
|
||||||
sha2 = { version = "0.10", optional = true }
|
sha2 = { version = "0.10", optional = true, features = ["oid"] }
|
||||||
rsa = { version = "0.6.0", optional = true }
|
rsa = { version = "0.8", optional = true }
|
||||||
ed25519-dalek = { version = "1.0.1", optional = true }
|
ed25519-dalek = { version = "1.0.1", optional = true }
|
||||||
regex = { version = "1", default-features = false, features = ["std"], optional = true }
|
|
||||||
|
|
||||||
# email formats
|
# email formats
|
||||||
email_address = { version = "0.2.1", default-features = false }
|
email_address = { version = "0.2.1", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1"
|
pretty_assertions = "1"
|
||||||
criterion = "0.3"
|
criterion = "0.4"
|
||||||
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
|
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
@@ -83,7 +82,7 @@ walkdir = "2"
|
|||||||
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
async-std = { version = "1.8", features = ["attributes"] }
|
async-std = { version = "1.8", features = ["attributes"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
maud = "0.23"
|
maud = "0.24"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
harness = false
|
harness = false
|
||||||
@@ -95,10 +94,10 @@ builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"]
|
|||||||
mime03 = ["mime"]
|
mime03 = ["mime"]
|
||||||
|
|
||||||
# transports
|
# transports
|
||||||
file-transport = ["uuid"]
|
file-transport = ["uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||||
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
||||||
sendmail-transport = []
|
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
||||||
smtp-transport = ["base64", "nom", "socket2", "once_cell"]
|
smtp-transport = ["base64", "nom", "socket2", "once_cell", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||||
|
|
||||||
pool = ["futures-util"]
|
pool = ["futures-util"]
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
|||||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
||||||
tokio1-boring-tls = ["tokio1", "boring-tls", "tokio1_boring"]
|
tokio1-boring-tls = ["tokio1", "boring-tls", "tokio1_boring"]
|
||||||
|
|
||||||
dkim = ["base64", "sha2", "rsa", "ed25519-dalek", "regex", "once_cell"]
|
dkim = ["base64", "sha2", "rsa", "ed25519-dalek"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://deps.rs/crate/lettre/0.10.1">
|
<a href="https://deps.rs/crate/lettre/0.10.2">
|
||||||
<img src="https://deps.rs/crate/lettre/0.10.1/status.svg"
|
<img src="https://deps.rs/crate/lettre/0.10.2/status.svg"
|
||||||
alt="dependency status" />
|
alt="dependency status" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
|
|||||||
## Supported Rust Versions
|
## Supported Rust Versions
|
||||||
|
|
||||||
Lettre supports all Rust versions released in the last 6 months. At the time of writing
|
Lettre supports all Rust versions released in the last 6 months. At the time of writing
|
||||||
the minimum supported Rust version is 1.56, but this could change at any time either from
|
the minimum supported Rust version is 1.60, but this could change at any time either from
|
||||||
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
This library requires Rust 1.56.0 or newer.
|
This library requires Rust 1.60 or newer.
|
||||||
To use this library, add the following to your `Cargo.toml`:
|
To use this library, add the following to your `Cargo.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ async fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(email).await {
|
match mailer.send(email).await {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ async fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(email).await {
|
match mailer.send(email).await {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,6 @@ fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ async fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(email).await {
|
match mailer.send(email).await {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ async fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(email).await {
|
match mailer.send(email).await {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ where
|
|||||||
let domain = domain.as_ref();
|
let domain = domain.as_ref();
|
||||||
Address::check_domain(domain)?;
|
Address::check_domain(domain)?;
|
||||||
|
|
||||||
let serialized = format!("{}@{}", user, domain);
|
let serialized = format!("{user}@{domain}");
|
||||||
Ok(Address {
|
Ok(Address {
|
||||||
serialized,
|
serialized,
|
||||||
at_start: user.len(),
|
at_start: user.len(),
|
||||||
|
|||||||
12
src/base64.rs
Normal file
12
src/base64.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use ::base64::{
|
||||||
|
engine::{general_purpose::STANDARD, Engine},
|
||||||
|
DecodeError,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
|
||||||
|
STANDARD.encode(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
|
||||||
|
STANDARD.decode(input)
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
//! * Secure defaults
|
//! * Secure defaults
|
||||||
//! * Async support
|
//! * Async support
|
||||||
//!
|
//!
|
||||||
//! Lettre requires Rust 1.56.0 or newer.
|
//! Lettre requires Rust 1.60 or newer.
|
||||||
//!
|
//!
|
||||||
//! ## Features
|
//! ## Features
|
||||||
//!
|
//!
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
//! [mime 0.3]: https://docs.rs/mime/0.3
|
//! [mime 0.3]: https://docs.rs/mime/0.3
|
||||||
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
|
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
|
||||||
|
|
||||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.1")]
|
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.2")]
|
||||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -203,6 +203,8 @@ Make sure to apply the same to any of your crate dependencies that use the `lett
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod address;
|
pub mod address;
|
||||||
|
#[cfg(any(feature = "smtp-transport", feature = "dkim"))]
|
||||||
|
mod base64;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
mod executor;
|
mod executor;
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ impl Attachment {
|
|||||||
builder.header(header::ContentDisposition::attachment(&filename))
|
builder.header(header::ContentDisposition::attachment(&filename))
|
||||||
}
|
}
|
||||||
Disposition::Inline(content_id) => builder
|
Disposition::Inline(content_id) => builder
|
||||||
.header(header::ContentId::from(format!("<{}>", content_id)))
|
.header(header::ContentId::from(format!("<{content_id}>")))
|
||||||
.header(header::ContentDisposition::inline()),
|
.header(header::ContentDisposition::inline()),
|
||||||
};
|
};
|
||||||
builder = builder.header(content_type);
|
builder = builder.header(content_type);
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use ed25519_dalek::Signer;
|
use ed25519_dalek::Signer;
|
||||||
use once_cell::sync::Lazy;
|
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs1v15::Pkcs1v15Sign, RsaPrivateKey};
|
||||||
use regex::bytes::Regex;
|
|
||||||
use rsa::{pkcs1::DecodeRsaPrivateKey, Hash, PaddingScheme, RsaPrivateKey};
|
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use crate::message::{
|
use crate::message::{
|
||||||
@@ -123,14 +121,14 @@ impl DkimSigningKey {
|
|||||||
RsaPrivateKey::from_pkcs1_pem(private_key)
|
RsaPrivateKey::from_pkcs1_pem(private_key)
|
||||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
|
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
|
||||||
),
|
),
|
||||||
DkimSigningAlgorithm::Ed25519 => {
|
DkimSigningAlgorithm::Ed25519 => InnerDkimSigningKey::Ed25519(
|
||||||
InnerDkimSigningKey::Ed25519(
|
ed25519_dalek::Keypair::from_bytes(
|
||||||
ed25519_dalek::Keypair::from_bytes(&base64::decode(private_key).map_err(
|
&crate::base64::decode(private_key).map_err(|err| {
|
||||||
|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)),
|
DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err))
|
||||||
)?)
|
})?,
|
||||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
|
|
||||||
)
|
)
|
||||||
}
|
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
|
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
|
||||||
@@ -219,24 +217,34 @@ fn dkim_header_format(
|
|||||||
|
|
||||||
/// Canonicalize the body of an email
|
/// Canonicalize the body of an email
|
||||||
fn dkim_canonicalize_body(
|
fn dkim_canonicalize_body(
|
||||||
body: &[u8],
|
mut body: &[u8],
|
||||||
canonicalization: DkimCanonicalizationType,
|
canonicalization: DkimCanonicalizationType,
|
||||||
) -> Cow<'_, [u8]> {
|
) -> Cow<'_, [u8]> {
|
||||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new("(\r\n)+$").unwrap());
|
|
||||||
static RE_DOUBLE_SPACE: Lazy<Regex> = Lazy::new(|| Regex::new("[\\t ]+").unwrap());
|
|
||||||
static RE_SPACE_EOL: Lazy<Regex> = Lazy::new(|| Regex::new("[\t ]\r\n").unwrap());
|
|
||||||
match canonicalization {
|
match canonicalization {
|
||||||
DkimCanonicalizationType::Simple => RE.replace(body, &b"\r\n"[..]),
|
DkimCanonicalizationType::Simple => {
|
||||||
DkimCanonicalizationType::Relaxed => {
|
// Remove empty lines at end
|
||||||
let body = RE_DOUBLE_SPACE.replace_all(body, &b" "[..]);
|
while body.ends_with(b"\r\n\r\n") {
|
||||||
let body = match RE_SPACE_EOL.replace_all(&body, &b"\r\n"[..]) {
|
body = &body[..body.len() - 2];
|
||||||
Cow::Borrowed(_body) => body,
|
|
||||||
Cow::Owned(body) => Cow::Owned(body),
|
|
||||||
};
|
|
||||||
match RE.replace(&body, &b"\r\n"[..]) {
|
|
||||||
Cow::Borrowed(_body) => body,
|
|
||||||
Cow::Owned(body) => Cow::Owned(body),
|
|
||||||
}
|
}
|
||||||
|
Cow::Borrowed(body)
|
||||||
|
}
|
||||||
|
DkimCanonicalizationType::Relaxed => {
|
||||||
|
let mut out = Vec::with_capacity(body.len());
|
||||||
|
loop {
|
||||||
|
match body {
|
||||||
|
[b' ' | b'\t', b'\r', b'\n', ..] => {}
|
||||||
|
[b' ' | b'\t', b' ' | b'\t', ..] => {}
|
||||||
|
[b' ' | b'\t', ..] => out.push(b' '),
|
||||||
|
[c, ..] => out.push(*c),
|
||||||
|
[] => break,
|
||||||
|
}
|
||||||
|
body = &body[1..];
|
||||||
|
}
|
||||||
|
// Remove empty lines at end
|
||||||
|
while out.ends_with(b"\r\n\r\n") {
|
||||||
|
out.truncate(out.len() - 2);
|
||||||
|
}
|
||||||
|
Cow::Owned(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,11 +354,11 @@ fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timesta
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs();
|
.as_secs();
|
||||||
let headers = message.headers();
|
let headers = message.headers();
|
||||||
let body_hash = Sha256::digest(&dkim_canonicalize_body(
|
let body_hash = Sha256::digest(dkim_canonicalize_body(
|
||||||
&message.body_raw(),
|
&message.body_raw(),
|
||||||
dkim_config.canonicalization.body,
|
dkim_config.canonicalization.body,
|
||||||
));
|
));
|
||||||
let bh = base64::encode(body_hash);
|
let bh = crate::base64::encode(body_hash);
|
||||||
let mut signed_headers_list =
|
let mut signed_headers_list =
|
||||||
dkim_config
|
dkim_config
|
||||||
.headers
|
.headers
|
||||||
@@ -382,16 +390,13 @@ fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timesta
|
|||||||
hashed_headers.update(canonicalized_dkim_header.trim_end().as_bytes());
|
hashed_headers.update(canonicalized_dkim_header.trim_end().as_bytes());
|
||||||
let hashed_headers = hashed_headers.finalize();
|
let hashed_headers = hashed_headers.finalize();
|
||||||
let signature = match &dkim_config.private_key.0 {
|
let signature = match &dkim_config.private_key.0 {
|
||||||
InnerDkimSigningKey::Rsa(private_key) => base64::encode(
|
InnerDkimSigningKey::Rsa(private_key) => crate::base64::encode(
|
||||||
private_key
|
private_key
|
||||||
.sign(
|
.sign(Pkcs1v15Sign::new::<Sha256>(), &hashed_headers)
|
||||||
PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA2_256)),
|
|
||||||
&hashed_headers,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
),
|
),
|
||||||
InnerDkimSigningKey::Ed25519(private_key) => {
|
InnerDkimSigningKey::Ed25519(private_key) => {
|
||||||
base64::encode(private_key.sign(&hashed_headers).to_bytes())
|
crate::base64::encode(private_key.sign(&hashed_headers).to_bytes())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let dkim_header = dkim_header_format(
|
let dkim_header = dkim_header_format(
|
||||||
@@ -416,8 +421,9 @@ mod test {
|
|||||||
header::{HeaderName, HeaderValue},
|
header::{HeaderName, HeaderValue},
|
||||||
Header, Message,
|
Header, Message,
|
||||||
},
|
},
|
||||||
dkim_canonicalize_headers, dkim_sign_fixed_time, DkimCanonicalization,
|
dkim_canonicalize_body, dkim_canonicalize_headers, dkim_sign_fixed_time,
|
||||||
DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm, DkimSigningKey,
|
DkimCanonicalization, DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm,
|
||||||
|
DkimSigningKey,
|
||||||
};
|
};
|
||||||
use crate::StdError;
|
use crate::StdError;
|
||||||
|
|
||||||
@@ -490,6 +496,24 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
|||||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
|
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_simple_canonicalize() {
|
||||||
|
let body = b" C \r\nD \t E\r\n\r\n\r\n";
|
||||||
|
assert_eq!(
|
||||||
|
dkim_canonicalize_body(body, DkimCanonicalizationType::Simple).into_owned(),
|
||||||
|
b" C \r\nD \t E\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_body_relaxed_canonicalize() {
|
||||||
|
let body = b" C \r\nD \t E\r\n\tF\r\n\t\r\n\r\n\r\n";
|
||||||
|
assert_eq!(
|
||||||
|
dkim_canonicalize_body(body, DkimCanonicalizationType::Relaxed).into_owned(),
|
||||||
|
b" C\r\nD E\r\n F\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_signature_rsa_simple() {
|
fn test_signature_rsa_simple() {
|
||||||
let mut message = test_message();
|
let mut message = test_message();
|
||||||
@@ -564,7 +588,7 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
|||||||
);
|
);
|
||||||
let signed = message.formatted();
|
let signed = message.formatted();
|
||||||
let signed = std::str::from_utf8(&signed).unwrap();
|
let signed = std::str::from_utf8(&signed).unwrap();
|
||||||
println!("{}", signed);
|
println!("{signed}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
signed,
|
signed,
|
||||||
std::concat!(
|
std::concat!(
|
||||||
|
|||||||
@@ -33,17 +33,19 @@ impl ContentDisposition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn with_name(kind: &str, file_name: &str) -> Self {
|
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 mut encoded_value = String::new();
|
||||||
let line_len = "Content-Disposition: ".len();
|
let line_len = "Content-Disposition: ".len();
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, false);
|
{
|
||||||
w.write_str(kind).expect("writing `kind` returned an error");
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
||||||
w.write_char(';').expect("writing `;` returned an error");
|
w.write_str(kind).expect("writing `kind` returned an error");
|
||||||
w.space();
|
w.write_char(';').expect("writing `;` returned an error");
|
||||||
|
w.optional_breakpoint();
|
||||||
|
|
||||||
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
|
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
|
||||||
.expect("some Write implementation returned an error");
|
.expect("some Write implementation returned an error");
|
||||||
|
}
|
||||||
|
|
||||||
Self(HeaderValue::dangerous_new_pre_encoded(
|
Self(HeaderValue::dangerous_new_pre_encoded(
|
||||||
Self::name(),
|
Self::name(),
|
||||||
@@ -88,12 +90,12 @@ mod test {
|
|||||||
|
|
||||||
headers.set(ContentDisposition::inline());
|
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"));
|
headers.set(ContentDisposition::attachment("something.txt"));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", headers),
|
format!("{headers}"),
|
||||||
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
|
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,8 +135,7 @@ mod serde {
|
|||||||
match ContentType::parse(mime) {
|
match ContentType::parse(mime) {
|
||||||
Ok(content_type) => Ok(content_type),
|
Ok(content_type) => Ok(content_type),
|
||||||
Err(_) => Err(E::custom(format!(
|
Err(_) => Err(E::custom(format!(
|
||||||
"Couldn't parse the following MIME-Type: {}",
|
"Couldn't parse the following MIME-Type: {mime}"
|
||||||
mime
|
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ macro_rules! mailbox_header {
|
|||||||
fn display(&self) -> HeaderValue {
|
fn display(&self) -> HeaderValue {
|
||||||
let mut encoded_value = String::new();
|
let mut encoded_value = String::new();
|
||||||
let line_len = $header_name.len() + ": ".len();
|
let line_len = $header_name.len() + ": ".len();
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, false);
|
{
|
||||||
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
||||||
|
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
||||||
|
}
|
||||||
|
|
||||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
||||||
}
|
}
|
||||||
@@ -78,8 +80,10 @@ macro_rules! mailboxes_header {
|
|||||||
fn display(&self) -> HeaderValue {
|
fn display(&self) -> HeaderValue {
|
||||||
let mut encoded_value = String::new();
|
let mut encoded_value = String::new();
|
||||||
let line_len = $header_name.len() + ": ".len();
|
let line_len = $header_name.len() + ": ".len();
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, false);
|
{
|
||||||
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
||||||
|
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
||||||
|
}
|
||||||
|
|
||||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,10 +323,12 @@ impl HeaderValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "dkim")]
|
||||||
pub(crate) fn get_raw(&self) -> &str {
|
pub(crate) fn get_raw(&self) -> &str {
|
||||||
&self.raw_value
|
&self.raw_value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "dkim")]
|
||||||
pub(crate) fn get_encoded(&self) -> &str {
|
pub(crate) fn get_encoded(&self) -> &str {
|
||||||
&self.encoded_value
|
&self.encoded_value
|
||||||
}
|
}
|
||||||
@@ -340,28 +342,21 @@ struct HeaderValueEncoder<'a> {
|
|||||||
|
|
||||||
impl<'a> HeaderValueEncoder<'a> {
|
impl<'a> HeaderValueEncoder<'a> {
|
||||||
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
|
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
|
||||||
let (words_iter, encoder) = Self::new(name, value, f);
|
let encoder = Self::new(name, f);
|
||||||
encoder.format(words_iter)
|
encoder.format(value.split_inclusive(' '))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(
|
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
|
||||||
name: &str,
|
|
||||||
value: &'a str,
|
|
||||||
writer: &'a mut dyn Write,
|
|
||||||
) -> (WordsPlusFillIterator<'a>, Self) {
|
|
||||||
let line_len = name.len() + ": ".len();
|
let line_len = name.len() + ": ".len();
|
||||||
let writer = EmailWriter::new(writer, line_len, false);
|
let writer = EmailWriter::new(writer, line_len, 0, false, false);
|
||||||
|
|
||||||
(
|
Self {
|
||||||
WordsPlusFillIterator { s: value },
|
writer,
|
||||||
Self {
|
encode_buf: String::new(),
|
||||||
writer,
|
}
|
||||||
encode_buf: String::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(mut self, words_iter: WordsPlusFillIterator<'_>) -> fmt::Result {
|
fn format(mut self, words_iter: impl Iterator<Item = &'a str>) -> fmt::Result {
|
||||||
for next_word in words_iter {
|
for next_word in words_iter {
|
||||||
let allowed = allowed_str(next_word);
|
let allowed = allowed_str(next_word);
|
||||||
|
|
||||||
@@ -389,71 +384,20 @@ impl<'a> HeaderValueEncoder<'a> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// It is important that we don't encode leading whitespace otherwise it breaks wrapping.
|
let prefix = self.encode_buf.trim_end_matches(' ');
|
||||||
let first_not_allowed = self
|
email_encoding::headers::rfc2047::encode(prefix, &mut self.writer)?;
|
||||||
.encode_buf
|
|
||||||
.bytes()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_i, c)| !allowed_char(*c))
|
|
||||||
.map(|(i, _)| i);
|
|
||||||
// May as well also write the tail in plain text.
|
|
||||||
let last_not_allowed = self
|
|
||||||
.encode_buf
|
|
||||||
.bytes()
|
|
||||||
.enumerate()
|
|
||||||
.rev()
|
|
||||||
.find(|(_i, c)| !allowed_char(*c))
|
|
||||||
.map(|(i, _)| i + 1);
|
|
||||||
|
|
||||||
let (prefix, to_encode, suffix) = match first_not_allowed {
|
// TODO: add a better API for doing this in email-encoding
|
||||||
Some(first_not_allowed) => {
|
let spaces = self.encode_buf.len() - prefix.len();
|
||||||
let last_not_allowed = last_not_allowed.unwrap();
|
for _ in 0..spaces {
|
||||||
|
self.writer.space();
|
||||||
let (remaining, suffix) = self.encode_buf.split_at(last_not_allowed);
|
}
|
||||||
let (prefix, to_encode) = remaining.split_at(first_not_allowed);
|
|
||||||
|
|
||||||
(prefix, to_encode, suffix)
|
|
||||||
}
|
|
||||||
None => ("", self.encode_buf.as_str(), ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.writer.folding().write_str(prefix)?;
|
|
||||||
email_encoding::headers::rfc2047::encode(to_encode, &mut self.writer)?;
|
|
||||||
self.writer.folding().write_str(suffix)?;
|
|
||||||
|
|
||||||
self.encode_buf.clear();
|
self.encode_buf.clear();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterator yielding a string split by space, but spaces are included before the next word.
|
|
||||||
struct WordsPlusFillIterator<'a> {
|
|
||||||
s: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for WordsPlusFillIterator<'a> {
|
|
||||||
type Item = &'a str;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if self.s.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let next_word = self
|
|
||||||
.s
|
|
||||||
.bytes()
|
|
||||||
.enumerate()
|
|
||||||
.skip(1)
|
|
||||||
.find(|&(_i, c)| c == b' ')
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
.unwrap_or(self.s.len());
|
|
||||||
|
|
||||||
let word = &self.s[..next_word];
|
|
||||||
self.s = &self.s[word.len()..];
|
|
||||||
Some(word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allowed_str(s: &str) -> bool {
|
fn allowed_str(s: &str) -> bool {
|
||||||
s.bytes().all(allowed_char)
|
s.bytes().all(allowed_char)
|
||||||
}
|
}
|
||||||
@@ -466,7 +410,8 @@ const fn allowed_char(c: u8) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::{HeaderName, HeaderValue, Headers};
|
use super::{HeaderName, HeaderValue, Headers, To};
|
||||||
|
use crate::message::Mailboxes;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_headername() {
|
fn valid_headername() {
|
||||||
@@ -617,7 +562,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
"To: Se=?utf-8?b?w6E=?=n <sean@example.com>\r\n"
|
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,19 +583,46 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_special_with_folding() {
|
fn format_special_with_folding() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(HeaderValue::new(
|
let to = To::from(Mailboxes::from_iter([
|
||||||
HeaderName::new_from_ascii_str("To"),
|
"🌍 <world@example.com>".parse().unwrap(),
|
||||||
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(),
|
"🦆 Everywhere <ducks@example.com>".parse().unwrap(),
|
||||||
) );
|
"Иванов Иван Иванович <ivanov@example.com>".parse().unwrap(),
|
||||||
|
"Jānis Bērziņš <janis@example.com>".parse().unwrap(),
|
||||||
|
"Seán Ó Rudaí <sean@example.com>".parse().unwrap(),
|
||||||
|
]));
|
||||||
|
headers.set(to);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= Everywhere\r\n",
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhiBFdmVyeXdo?=\r\n",
|
||||||
" <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n",
|
" =?utf-8?b?ZXJl?= <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LI=?=\r\n",
|
||||||
" =?utf-8?b?0L7QstC40Yc=?= <ivanov@example.com>, J=?utf-8?b?xIFuaXMgQsST?=\r\n",
|
" =?utf-8?b?0LDQvSDQmNCy0LDQvdC+0LLQuNGH?= <ivanov@example.com>,\r\n",
|
||||||
" =?utf-8?b?cnppxYbFoQ==?= <janis@example.com>, Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n",
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||||
" =?utf-8?b?ZGHDrQ==?= <sean@example.com>\r\n",
|
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_special_with_folding_raw() {
|
||||||
|
let mut headers = Headers::new();
|
||||||
|
headers.insert_raw(HeaderValue::new(
|
||||||
|
HeaderName::new_from_ascii_str("To"),
|
||||||
|
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// TODO: fix the fact that the encoder doesn't know that
|
||||||
|
// the space between the name and the address should be
|
||||||
|
// removed when wrapping.
|
||||||
|
assert_eq!(
|
||||||
|
headers.to_string(),
|
||||||
|
concat!(
|
||||||
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||||
|
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||||
|
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||||
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
|
||||||
|
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -715,17 +687,20 @@ mod tests {
|
|||||||
"quoted-printable".to_string(),
|
"quoted-printable".to_string(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// TODO: fix the fact that the encoder doesn't know that
|
||||||
|
// the space between the name and the address should be
|
||||||
|
// removed when wrapping.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: Hello! This is lettre, and this\r\n",
|
"Subject: Hello! This is lettre, and this\r\n",
|
||||||
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||||
" guess that's it!\r\n",
|
" guess that's it!\r\n",
|
||||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= Everywhere\r\n",
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||||
" <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n",
|
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||||
" =?utf-8?b?0L7QstC40Yc=?= <ivanov@example.com>, J=?utf-8?b?xIFuaXMgQsST?=\r\n",
|
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||||
" =?utf-8?b?cnppxYbFoQ==?= <janis@example.com>, Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n",
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
|
||||||
" =?utf-8?b?ZGHDrQ==?= <sean@example.com>\r\n",
|
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
||||||
"From: Someone <somewhere@example.com>\r\n",
|
"From: Someone <somewhere@example.com>\r\n",
|
||||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||||
)
|
)
|
||||||
@@ -743,8 +718,8 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go;\r\n",
|
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7?=\r\n",
|
||||||
" ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg=?utf-8?b?772G44Gj?=\r\n"
|
" =?utf-8?b?Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,17 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_utf8_word() {
|
||||||
|
let mut headers = Headers::new();
|
||||||
|
headers.set(Subject("Administratör".into()));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
headers.to_string(),
|
||||||
|
"Subject: =?utf-8?b?QWRtaW5pc3RyYXTDtnI=?=\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_ascii() {
|
fn parse_ascii() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ impl Mailbox {
|
|||||||
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
||||||
if let Some(name) = &self.name {
|
if let Some(name) = &self.name {
|
||||||
email_encoding::headers::quoted_string::encode(name, w)?;
|
email_encoding::headers::quoted_string::encode(name, w)?;
|
||||||
w.space();
|
w.optional_breakpoint();
|
||||||
w.write_char('<')?;
|
w.write_char('<')?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ impl Mailboxes {
|
|||||||
for mailbox in self.iter() {
|
for mailbox in self.iter() {
|
||||||
if !mem::take(&mut first) {
|
if !mem::take(&mut first) {
|
||||||
w.write_char(',')?;
|
w.write_char(',')?;
|
||||||
w.space();
|
w.optional_breakpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
mailbox.encode(w)?;
|
mailbox.encode(w)?;
|
||||||
@@ -397,7 +397,7 @@ fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
|
|||||||
} else {
|
} else {
|
||||||
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
||||||
f.write_char('"')?;
|
f.write_char('"')?;
|
||||||
for &c in s.as_bytes() {
|
for c in s.chars() {
|
||||||
write_quoted_string_char(f, c)?;
|
write_quoted_string_char(f, c)?;
|
||||||
}
|
}
|
||||||
f.write_char('"')?;
|
f.write_char('"')?;
|
||||||
@@ -441,34 +441,37 @@ fn is_valid_atom_char(c: u8) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
||||||
fn write_quoted_string_char(f: &mut Formatter<'_>, c: u8) -> FmtResult {
|
fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
|
||||||
match c {
|
match c {
|
||||||
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
|
// Can not be encoded.
|
||||||
1..=8 | 11 | 12 | 14..=31 | 127 |
|
'\n' | '\r' => Err(std::fmt::Error),
|
||||||
|
|
||||||
// Note, not qcontent but can be put before or after any qcontent.
|
// Note, not qcontent but can be put before or after any qcontent.
|
||||||
b'\t' |
|
'\t' | ' ' => f.write_char(c),
|
||||||
b' ' |
|
|
||||||
|
|
||||||
// The rest of the US-ASCII except \ and "
|
c if match c as u32 {
|
||||||
33 |
|
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
|
||||||
35..=91 |
|
1..=8 | 11 | 12 | 14..=31 | 127 |
|
||||||
93..=126 |
|
|
||||||
|
|
||||||
// Non-ascii characters will be escaped separately later.
|
// The rest of the US-ASCII except \ and "
|
||||||
128..=255
|
33 |
|
||||||
|
35..=91 |
|
||||||
|
93..=126 |
|
||||||
|
|
||||||
=> f.write_char(c.into()),
|
// Non-ascii characters will be escaped separately later.
|
||||||
|
128.. => true,
|
||||||
|
_ => false,
|
||||||
|
} =>
|
||||||
|
{
|
||||||
|
f.write_char(c)
|
||||||
|
}
|
||||||
|
|
||||||
// Can not be encoded.
|
_ => {
|
||||||
b'\n' | b'\r' => Err(std::fmt::Error),
|
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
|
||||||
|
f.write_char('\\')?;
|
||||||
c => {
|
f.write_char(c)
|
||||||
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
|
}
|
||||||
f.write_char('\\')?;
|
}
|
||||||
f.write_char(c.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -515,6 +518,34 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mailbox_format_address_with_comma_and_non_ascii() {
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Mailbox::new(
|
||||||
|
Some("Laşt, First".into()),
|
||||||
|
"kayo@example.com".parse().unwrap()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
r#""Laşt, First" <kayo@example.com>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mailbox_format_address_with_comma_and_quoted_non_ascii() {
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Mailbox::new(
|
||||||
|
Some(r#"Laşt, "First""#.into()),
|
||||||
|
"kayo@example.com".parse().unwrap()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
r#""Laşt, \"First\"" <kayo@example.com>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mailbox_format_address_with_color() {
|
fn mailbox_format_address_with_color() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -190,9 +190,9 @@ impl MultiPartKind {
|
|||||||
},
|
},
|
||||||
boundary,
|
boundary,
|
||||||
match self {
|
match self {
|
||||||
Self::Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
|
Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
|
||||||
Self::Signed { protocol, micalg } =>
|
Self::Signed { protocol, micalg } =>
|
||||||
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
|
format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
//! The easiest way of creating a message, which uses a plain text body.
|
//! The easiest way of creating a message, which uses a plain text body.
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use lettre::message::Message;
|
//! use lettre::message::{header::ContentType, Message};
|
||||||
//!
|
//!
|
||||||
//! # use std::error::Error;
|
//! # use std::error::Error;
|
||||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||||
//! .subject("Happy new year")
|
//! .subject("Happy new year")
|
||||||
|
//! .header(ContentType::TEXT_PLAIN)
|
||||||
//! .body(String::from("Be happy!"))?;
|
//! .body(String::from("Be happy!"))?;
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
//! To: Hei <hei@domain.tld>
|
//! To: Hei <hei@domain.tld>
|
||||||
//! Subject: Happy new year
|
//! Subject: Happy new year
|
||||||
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
||||||
|
//! Content-Type: text/plain; charset=utf-8
|
||||||
//! Content-Transfer-Encoding: 7bit
|
//! Content-Transfer-Encoding: 7bit
|
||||||
//!
|
//!
|
||||||
//! Be happy!
|
//! Be happy!
|
||||||
@@ -356,7 +358,7 @@ impl MessageBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set [User-Agent
|
/// Set [User-Agent
|
||||||
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
|
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00)
|
||||||
pub fn user_agent(self, id: String) -> Self {
|
pub fn user_agent(self, id: String) -> Self {
|
||||||
self.header(header::UserAgent::from(id))
|
self.header(header::UserAgent::from(id))
|
||||||
}
|
}
|
||||||
@@ -673,7 +675,7 @@ mod test {
|
|||||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
||||||
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
|
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
|
||||||
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvQ==?=!\r\n",
|
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"Happy new year!"
|
"Happy new year!"
|
||||||
@@ -711,7 +713,7 @@ mod test {
|
|||||||
"Bcc: hidden@example.com\r\n",
|
"Bcc: hidden@example.com\r\n",
|
||||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
||||||
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
|
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
|
||||||
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvQ==?=!\r\n",
|
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"Happy new year!"
|
"Happy new year!"
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ impl fmt::Display for Error {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref e) = self.inner.source {
|
if let Some(ref e) = self.inner.source {
|
||||||
write!(f, ": {}", e)?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -208,18 +208,18 @@ impl FileTransport {
|
|||||||
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
let eml_file = self.path.join(format!("{}.eml", email_id));
|
let eml_file = self.path.join(format!("{email_id}.eml"));
|
||||||
let eml = fs::read(eml_file).map_err(error::io)?;
|
let eml = fs::read(eml_file).map_err(error::io)?;
|
||||||
|
|
||||||
let json_file = self.path.join(format!("{}.json", email_id));
|
let json_file = self.path.join(format!("{email_id}.json"));
|
||||||
let json = fs::read(&json_file).map_err(error::io)?;
|
let json = fs::read(json_file).map_err(error::io)?;
|
||||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||||
|
|
||||||
Ok((envelope, eml))
|
Ok((envelope, eml))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
|
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
|
||||||
self.path.join(format!("{}.{}", email_id, extension))
|
self.path.join(format!("{email_id}.{extension}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,10 +255,10 @@ where
|
|||||||
/// Reads the envelope and the raw message content.
|
/// Reads the envelope and the raw message content.
|
||||||
#[cfg(feature = "file-transport-envelope")]
|
#[cfg(feature = "file-transport-envelope")]
|
||||||
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||||
let eml_file = self.inner.path.join(format!("{}.eml", email_id));
|
let eml_file = self.inner.path.join(format!("{email_id}.eml"));
|
||||||
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
|
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
|
||||||
|
|
||||||
let json_file = self.inner.path.join(format!("{}.json", email_id));
|
let json_file = self.inner.path.join(format!("{email_id}.json"));
|
||||||
let json = E::fs_read(&json_file).await.map_err(error::io)?;
|
let json = E::fs_read(&json_file).await.map_err(error::io)?;
|
||||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||||
|
|
||||||
@@ -276,6 +276,8 @@ impl Transport for FileTransport {
|
|||||||
let email_id = Uuid::new_v4();
|
let email_id = Uuid::new_v4();
|
||||||
|
|
||||||
let file = self.path(&email_id, "eml");
|
let file = self.path(&email_id, "eml");
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(?file, "writing email to");
|
||||||
fs::write(file, email).map_err(error::io)?;
|
fs::write(file, email).map_err(error::io)?;
|
||||||
|
|
||||||
#[cfg(feature = "file-transport-envelope")]
|
#[cfg(feature = "file-transport-envelope")]
|
||||||
@@ -306,6 +308,8 @@ where
|
|||||||
let email_id = Uuid::new_v4();
|
let email_id = Uuid::new_v4();
|
||||||
|
|
||||||
let file = self.inner.path(&email_id, "eml");
|
let file = self.inner.path(&email_id, "eml");
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(?file, "writing email to");
|
||||||
E::fs_write(&file, email).await.map_err(error::io)?;
|
E::fs_write(&file, email).await.map_err(error::io)?;
|
||||||
|
|
||||||
#[cfg(feature = "file-transport-envelope")]
|
#[cfg(feature = "file-transport-envelope")]
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ pub trait Transport {
|
|||||||
#[cfg(feature = "builder")]
|
#[cfg(feature = "builder")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||||
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::trace!("starting to send an email");
|
||||||
|
|
||||||
let raw = message.formatted();
|
let raw = message.formatted();
|
||||||
self.send_raw(message.envelope(), &raw)
|
self.send_raw(message.envelope(), &raw)
|
||||||
}
|
}
|
||||||
@@ -149,6 +152,9 @@ pub trait AsyncTransport {
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||||
// TODO take &Message
|
// TODO take &Message
|
||||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::trace!("starting to send an email");
|
||||||
|
|
||||||
let raw = message.formatted();
|
let raw = message.formatted();
|
||||||
let envelope = message.envelope();
|
let envelope = message.envelope();
|
||||||
self.send_raw(envelope, &raw).await
|
self.send_raw(envelope, &raw).await
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ impl fmt::Display for Error {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref e) = self.inner.source {
|
if let Some(ref e) = self.inner.source {
|
||||||
write!(f, ": {}", e)?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -232,6 +232,9 @@ impl Transport for SendmailTransport {
|
|||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(command = ?self.command, "sending email with");
|
||||||
|
|
||||||
// Spawn the sendmail command
|
// Spawn the sendmail command
|
||||||
let mut process = self.command(envelope).spawn().map_err(error::client)?;
|
let mut process = self.command(envelope).spawn().map_err(error::client)?;
|
||||||
|
|
||||||
@@ -261,6 +264,9 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
|
|||||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||||
use async_std::io::prelude::WriteExt;
|
use async_std::io::prelude::WriteExt;
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(command = ?self.inner.command, "sending email with");
|
||||||
|
|
||||||
let mut command = self.async_std_command(envelope);
|
let mut command = self.async_std_command(envelope);
|
||||||
|
|
||||||
// Spawn the sendmail command
|
// Spawn the sendmail command
|
||||||
@@ -293,6 +299,9 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
|
|||||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||||
use tokio1_crate::io::AsyncWriteExt;
|
use tokio1_crate::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(command = ?self.inner.command, "sending email with");
|
||||||
|
|
||||||
let mut command = self.tokio1_command(envelope);
|
let mut command = self.tokio1_command(envelope);
|
||||||
|
|
||||||
// Spawn the sendmail command
|
// Spawn the sendmail command
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
#[cfg(feature = "pool")]
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Debug},
|
fmt::{self, Debug},
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use std::{fmt::Display, net::IpAddr, time::Duration};
|
|||||||
|
|
||||||
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
use super::async_net::AsyncTokioStream;
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
use super::escape_crlf;
|
use super::escape_crlf;
|
||||||
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||||
@@ -46,6 +48,18 @@ impl AsyncSmtpConnection {
|
|||||||
&self.server_info
|
&self.server_info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connects with existing async stream
|
||||||
|
///
|
||||||
|
/// Sends EHLO and parses server information
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
pub async fn connect_with_transport(
|
||||||
|
stream: Box<dyn AsyncTokioStream>,
|
||||||
|
hello_name: &ClientId,
|
||||||
|
) -> Result<AsyncSmtpConnection, Error> {
|
||||||
|
let stream = AsyncNetworkStream::use_existing_tokio1(stream);
|
||||||
|
Self::connect_impl(stream, hello_name).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Connects to the configured server
|
/// Connects to the configured server
|
||||||
///
|
///
|
||||||
/// Sends EHLO and parses server information
|
/// Sends EHLO and parses server information
|
||||||
@@ -303,7 +317,7 @@ impl AsyncSmtpConnection {
|
|||||||
} else {
|
} else {
|
||||||
Err(error::code(
|
Err(error::code(
|
||||||
response.code(),
|
response.code(),
|
||||||
response.first_line().map(|s| s.to_owned()),
|
Some(response.message().collect()),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io, mem,
|
fmt, io, mem,
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, SocketAddr},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
@@ -19,7 +19,7 @@ use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
|||||||
#[cfg(feature = "tokio1-boring-tls")]
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
use tokio1_boring::SslStream as Tokio1SslStream;
|
use tokio1_boring::SslStream as Tokio1SslStream;
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
|
use tokio1_crate::io::{AsyncRead, AsyncWrite, ReadBuf as Tokio1ReadBuf};
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
use tokio1_crate::net::{
|
use tokio1_crate::net::{
|
||||||
TcpSocket as Tokio1TcpSocket, TcpStream as Tokio1TcpStream,
|
TcpSocket as Tokio1TcpSocket, TcpStream as Tokio1TcpStream,
|
||||||
@@ -44,28 +44,42 @@ use crate::transport::smtp::client::net::resolved_address_filter;
|
|||||||
use crate::transport::smtp::{error, Error};
|
use crate::transport::smtp::{error, Error};
|
||||||
|
|
||||||
/// A network stream
|
/// A network stream
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct AsyncNetworkStream {
|
pub struct AsyncNetworkStream {
|
||||||
inner: InnerAsyncNetworkStream,
|
inner: InnerAsyncNetworkStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
pub trait AsyncTokioStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + fmt::Debug {
|
||||||
|
fn peer_addr(&self) -> io::Result<SocketAddr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
impl AsyncTokioStream for Tokio1TcpStream {
|
||||||
|
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||||
|
self.peer_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents the different types of underlying network streams
|
/// Represents the different types of underlying network streams
|
||||||
// usually only one TLS backend at a time is going to be enabled,
|
// usually only one TLS backend at a time is going to be enabled,
|
||||||
// so clippy::large_enum_variant doesn't make sense here
|
// so clippy::large_enum_variant doesn't make sense here
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug)]
|
||||||
enum InnerAsyncNetworkStream {
|
enum InnerAsyncNetworkStream {
|
||||||
/// Plain Tokio 1.x TCP stream
|
/// Plain Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
Tokio1Tcp(Tokio1TcpStream),
|
Tokio1Tcp(Box<dyn AsyncTokioStream>),
|
||||||
/// Encrypted Tokio 1.x TCP stream
|
/// Encrypted Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "tokio1-native-tls")]
|
#[cfg(feature = "tokio1-native-tls")]
|
||||||
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>),
|
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
|
||||||
/// Encrypted Tokio 1.x TCP stream
|
/// Encrypted Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>),
|
Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
|
||||||
/// Encrypted Tokio 1.x TCP stream
|
/// Encrypted Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "tokio1-boring-tls")]
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
Tokio1BoringTls(Tokio1SslStream<Tokio1TcpStream>),
|
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
|
||||||
/// Plain Tokio 1.x TCP stream
|
/// Plain Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
AsyncStd1Tcp(AsyncStd1TcpStream),
|
AsyncStd1Tcp(AsyncStd1TcpStream),
|
||||||
@@ -117,6 +131,11 @@ impl AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
|
||||||
|
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
|
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
|
||||||
server: T,
|
server: T,
|
||||||
@@ -175,7 +194,8 @@ impl AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let tcp_stream = try_connect(server, timeout, local_addr).await?;
|
let tcp_stream = try_connect(server, timeout, local_addr).await?;
|
||||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
|
let mut stream =
|
||||||
|
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
|
||||||
if let Some(tls_parameters) = tls_parameters {
|
if let Some(tls_parameters) = tls_parameters {
|
||||||
stream.upgrade_tls(tls_parameters).await?;
|
stream.upgrade_tls(tls_parameters).await?;
|
||||||
}
|
}
|
||||||
@@ -300,7 +320,7 @@ impl AsyncNetworkStream {
|
|||||||
feature = "tokio1-boring-tls"
|
feature = "tokio1-boring-tls"
|
||||||
))]
|
))]
|
||||||
async fn upgrade_tokio1_tls(
|
async fn upgrade_tokio1_tls(
|
||||||
tcp_stream: Tokio1TcpStream,
|
tcp_stream: Box<dyn AsyncTokioStream>,
|
||||||
tls_parameters: TlsParameters,
|
tls_parameters: TlsParameters,
|
||||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||||
let domain = tls_parameters.domain().to_string();
|
let domain = tls_parameters.domain().to_string();
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ impl SmtpConnection {
|
|||||||
} else {
|
} else {
|
||||||
Err(error::code(
|
Err(error::code(
|
||||||
response.code(),
|
response.code(),
|
||||||
response.first_line().map(|s| s.to_owned()),
|
Some(response.message().collect()),
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ use std::fmt::Debug;
|
|||||||
pub use self::async_connection::AsyncSmtpConnection;
|
pub use self::async_connection::AsyncSmtpConnection;
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
pub use self::async_net::AsyncNetworkStream;
|
pub use self::async_net::AsyncNetworkStream;
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
pub use self::async_net::AsyncTokioStream;
|
||||||
use self::net::NetworkStream;
|
use self::net::NetworkStream;
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
pub(super) use self::tls::InnerTlsParameters;
|
pub(super) use self::tls::InnerTlsParameters;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
#[cfg(feature = "rustls-tls")]
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
mem,
|
mem,
|
||||||
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ pub struct TlsParameters {
|
|||||||
pub(crate) connector: InnerTlsParameters,
|
pub(crate) connector: InnerTlsParameters,
|
||||||
/// The domain name which is expected in the TLS certificate from the server
|
/// The domain name which is expected in the TLS certificate from the server
|
||||||
pub(super) domain: String,
|
pub(super) domain: String,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
pub(super) accept_invalid_hostnames: bool,
|
pub(super) accept_invalid_hostnames: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +230,7 @@ impl TlsParametersBuilder {
|
|||||||
Ok(TlsParameters {
|
Ok(TlsParameters {
|
||||||
connector: InnerTlsParameters::NativeTls(connector),
|
connector: InnerTlsParameters::NativeTls(connector),
|
||||||
domain: self.domain,
|
domain: self.domain,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -322,6 +324,7 @@ impl TlsParametersBuilder {
|
|||||||
Ok(TlsParameters {
|
Ok(TlsParameters {
|
||||||
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
||||||
domain: self.domain,
|
domain: self.domain,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ impl Display for Mail {
|
|||||||
self.sender.as_ref().map_or("", |s| s.as_ref())
|
self.sender.as_ref().map_or("", |s| s.as_ref())
|
||||||
)?;
|
)?;
|
||||||
for parameter in &self.parameters {
|
for parameter in &self.parameters {
|
||||||
write!(f, " {}", parameter)?;
|
write!(f, " {parameter}")?;
|
||||||
}
|
}
|
||||||
f.write_str("\r\n")
|
f.write_str("\r\n")
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ impl Display for Rcpt {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||||
for parameter in &self.parameters {
|
for parameter in &self.parameters {
|
||||||
write!(f, " {}", parameter)?;
|
write!(f, " {parameter}")?;
|
||||||
}
|
}
|
||||||
f.write_str("\r\n")
|
f.write_str("\r\n")
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ impl Display for Help {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str("HELP")?;
|
f.write_str("HELP")?;
|
||||||
if let Some(argument) = &self.argument {
|
if let Some(argument) = &self.argument {
|
||||||
write!(f, " {}", argument)?;
|
write!(f, " {argument}")?;
|
||||||
}
|
}
|
||||||
f.write_str("\r\n")
|
f.write_str("\r\n")
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ pub struct Auth {
|
|||||||
|
|
||||||
impl Display for Auth {
|
impl Display for Auth {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
let encoded_response = self.response.as_ref().map(base64::encode);
|
let encoded_response = self.response.as_ref().map(crate::base64::encode);
|
||||||
|
|
||||||
if self.mechanism.supports_initial_response() {
|
if self.mechanism.supports_initial_response() {
|
||||||
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
||||||
@@ -271,7 +271,7 @@ impl Auth {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
||||||
|
|
||||||
let decoded_base64 = base64::decode(&encoded_challenge).map_err(error::response)?;
|
let decoded_base64 = crate::base64::decode(encoded_challenge).map_err(error::response)?;
|
||||||
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
|
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
||||||
@@ -341,9 +341,9 @@ mod test {
|
|||||||
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
|
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
|
||||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(format!("{}", Quit), "QUIT\r\n");
|
assert_eq!(format!("{Quit}"), "QUIT\r\n");
|
||||||
assert_eq!(format!("{}", Data), "DATA\r\n");
|
assert_eq!(format!("{Data}"), "DATA\r\n");
|
||||||
assert_eq!(format!("{}", Noop), "NOOP\r\n");
|
assert_eq!(format!("{Noop}"), "NOOP\r\n");
|
||||||
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", Help::new(Some("test".to_string()))),
|
format!("{}", Help::new(Some("test".to_string()))),
|
||||||
@@ -357,7 +357,7 @@ mod test {
|
|||||||
format!("{}", Expn::new("test".to_string())),
|
format!("{}", Expn::new("test".to_string())),
|
||||||
"EXPN test\r\n"
|
"EXPN test\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(format!("{}", Rset), "RSET\r\n");
|
assert_eq!(format!("{Rset}"), "RSET\r\n");
|
||||||
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -137,15 +137,15 @@ impl fmt::Display for Error {
|
|||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
Kind::Tls => f.write_str("tls error")?,
|
Kind::Tls => f.write_str("tls error")?,
|
||||||
Kind::Transient(ref code) => {
|
Kind::Transient(ref code) => {
|
||||||
write!(f, "transient error ({})", code)?;
|
write!(f, "transient error ({code})")?;
|
||||||
}
|
}
|
||||||
Kind::Permanent(ref code) => {
|
Kind::Permanent(ref code) => {
|
||||||
write!(f, "permanent error ({})", code)?;
|
write!(f, "permanent error ({code})")?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref e) = self.inner.source {
|
if let Some(ref e) = self.inner.source {
|
||||||
write!(f, ": {}", e)?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ impl Display for ClientId {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
Self::Domain(ref value) => f.write_str(value),
|
Self::Domain(ref value) => f.write_str(value),
|
||||||
Self::Ipv4(ref value) => write!(f, "[{}]", value),
|
Self::Ipv4(ref value) => write!(f, "[{value}]"),
|
||||||
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value),
|
Self::Ipv6(ref value) => write!(f, "[IPv6:{value}]"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ impl Display for Extension {
|
|||||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||||
Extension::StartTls => f.write_str("STARTTLS"),
|
Extension::StartTls => f.write_str("STARTTLS"),
|
||||||
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
|
Extension::Authentication(ref mechanism) => write!(f, "AUTH {mechanism}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,8 +228,8 @@ pub enum MailParameter {
|
|||||||
impl Display for MailParameter {
|
impl Display for MailParameter {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
|
MailParameter::Body(ref value) => write!(f, "BODY={value}"),
|
||||||
MailParameter::Size(size) => write!(f, "SIZE={}", size),
|
MailParameter::Size(size) => write!(f, "SIZE={size}"),
|
||||||
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||||
MailParameter::Other {
|
MailParameter::Other {
|
||||||
ref keyword,
|
ref keyword,
|
||||||
@@ -307,7 +307,7 @@ mod test {
|
|||||||
format!("{}", ClientId::Domain("test".to_string())),
|
format!("{}", ClientId::Domain("test".to_string())),
|
||||||
"test".to_string()
|
"test".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string());
|
assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ mod test {
|
|||||||
let res = parse_response(raw_response);
|
let res = parse_response(raw_response);
|
||||||
match res {
|
match res {
|
||||||
Err(nom::Err::Incomplete(_)) => {}
|
Err(nom::Err::Incomplete(_)) => {}
|
||||||
_ => panic!("Expected incomplete response, got {:?}", res),
|
_ => panic!("Expected incomplete response, got {res:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ mod sync {
|
|||||||
let result = sender.send(&email);
|
let result = sender.send(&email);
|
||||||
let id = result.unwrap();
|
let id = result.unwrap();
|
||||||
|
|
||||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||||
let eml = read_to_string(&eml_file).unwrap();
|
let eml = read_to_string(&eml_file).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -68,10 +68,10 @@ mod sync {
|
|||||||
let result = sender.send(&email);
|
let result = sender.send(&email);
|
||||||
let id = result.unwrap();
|
let id = result.unwrap();
|
||||||
|
|
||||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||||
let eml = read_to_string(&eml_file).unwrap();
|
let eml = read_to_string(&eml_file).unwrap();
|
||||||
|
|
||||||
let json_file = temp_dir().join(format!("{}.json", id));
|
let json_file = temp_dir().join(format!("{id}.json"));
|
||||||
let json = read_to_string(&json_file).unwrap();
|
let json = read_to_string(&json_file).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -131,7 +131,7 @@ mod tokio_1 {
|
|||||||
let result = sender.send(email).await;
|
let result = sender.send(email).await;
|
||||||
let id = result.unwrap();
|
let id = result.unwrap();
|
||||||
|
|
||||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||||
let eml = read_to_string(&eml_file).unwrap();
|
let eml = read_to_string(&eml_file).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -182,7 +182,7 @@ mod asyncstd_1 {
|
|||||||
let result = sender.send(email).await;
|
let result = sender.send(email).await;
|
||||||
let id = result.unwrap();
|
let id = result.unwrap();
|
||||||
|
|
||||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||||
let eml = read_to_string(&eml_file).unwrap();
|
let eml = read_to_string(&eml_file).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ mod sync {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = sender.send(&email);
|
let result = sender.send(&email);
|
||||||
println!("{:?}", result);
|
println!("{result:?}");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ mod tokio_1 {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = sender.send(email).await;
|
let result = sender.send(email).await;
|
||||||
println!("{:?}", result);
|
println!("{result:?}");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ mod asyncstd_1 {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = sender.send(email).await;
|
let result = sender.send(email).await;
|
||||||
println!("{:?}", result);
|
println!("{result:?}");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user