Compare commits

...

31 Commits

Author SHA1 Message Date
Paolo Barbolini
ce363273fe Prepare 0.10.4 (#871) 2023-04-02 11:42:57 +02:00
Paolo Barbolini
e59ecc20e7 Bump rustls to 0.21 (#867) 2023-04-02 10:54:46 +02:00
Paolo Barbolini
4fb67a7da1 Prepare 0.10.3 (#860) 2023-02-20 11:56:28 +01:00
Paolo Barbolini
9041f210f4 Add Content-Type to all examples sending a basic text/plain message (#859) 2023-02-14 17:54:05 +00:00
Paolo Barbolini
77b7d40fb8 mailbox: replace serialize_str(&self.to_string()) with collect_str(self) (#858) 2023-02-14 18:35:29 +01:00
Paolo Barbolini
2b6d457f85 clippy: deny str_to_string and empty_structs_with_brackets (#857) 2023-02-14 18:33:10 +01:00
Stéphan Kochen
952c1b39df Add support for rustls-native-certs (#843) 2023-02-14 18:11:42 +01:00
Paolo Barbolini
7ecb87f9fd Prepare 0.10.2 (#853) 2023-01-29 14:58:41 +01:00
Paolo Barbolini
fd700b1717 cargo: switch to crates.io release of email-encoding v0.2 (#854) 2023-01-29 14:47:08 +01:00
Paolo Barbolini
f8f19d6af5 clippy: fix latest warnings (#855) 2023-01-29 13:46:57 +00:00
Paolo Barbolini
cc25223914 Update rsa to v0.8 (#852) 2023-01-24 10:26:25 +01:00
Paolo Barbolini
750573d38b Update base64 to v0.21 (#851) 2023-01-24 10:07:48 +01:00
finga
0734a96343 tracing: Write some logs when sending an email (#848)
Write a trace message when sending an email. Further, write a debug
message when using the sendmail transport method, containing which
program is called. And write a debug message containing the target
file name when the file transport method is used.

This should help improve #556 a tiny bit.

Co-authored-by: finga <finga@onders.org>
2023-01-24 09:14:31 +01:00
Christian Mandery
3c2f996856 Fix broken URL for IETF user-agent draft (#849) 2023-01-16 11:34:30 +00:00
lfuerderer
9cae29dd07 Add Content-Type header in documentation example (#841)
* Add Content-Type header in documentation example

In the example showing how to build a message from a pure string, set
the Content-Type to text/plain explicitly.
This header constant also includes the phrase charset=utf-8 so that
non-ascii characters will be displayed correctly.

* cargo fmt

* Fix generated email example

Co-authored-by: Paolo Barbolini <paolo.barbolini@m4ss.net>
2022-12-16 22:04:29 +00:00
Paolo Barbolini
e1a146c8f8 cargo: bump base64 to 0.20 (#840) 2022-12-10 22:29:01 +00:00
Paolo Barbolini
840a19784a cargo: require quoted_printable ^0.4.6 (#837) 2022-12-07 15:37:38 +00:00
Paolo Barbolini
5a61ba36b5 ci: bump nightly used for rustfmt (#817) 2022-11-12 19:51:24 +01:00
Paolo Barbolini
dbf0e53c31 Fix latest clippy warnings (#830) 2022-11-12 17:54:20 +00:00
Paolo Barbolini
c914a07379 Update dependencies (#829) 2022-11-12 17:45:19 +00:00
Paolo Barbolini
2c4fa39523 Use cargo weak dependency features to reduce tokio features (#785) 2022-11-12 17:34:55 +00:00
Clément DOUIN
28f0af16be Mailbox displays wrongly when containing a comma and a non-ascii char in its name (#827) 2022-11-12 17:23:06 +00:00
Paolo Barbolini
f0614be555 Bump MSRV to 1.60 (#828) 2022-11-12 17:13:14 +00:00
André Cruz
a3fcdf263d fix(transport): return whole smtp error string (#821)
We were only returning the first line of the error message,
but that may not be enough to get the whole context of the error.
Since we have already parsed the response, just return the whole error
to the user.

Related to #694
2022-09-22 16:30:04 +02:00
Paolo Barbolini
d4da2e1f14 ci: switch to Swatinem/rust-cache@v2 (#819) 2022-09-10 13:19:49 +02:00
Paolo Barbolini
5655958288 clippy: fix latest warning (#818) 2022-09-10 10:59:49 +00:00
Paolo Barbolini
11b4acf0cd Improve header encoding and wrapping (#811)
Also cleans up the encoder a lot, removing some
complicated logic introduced by the initial round
of implementation
2022-09-10 12:40:00 +02:00
Paolo Barbolini
b3b5df285a Bump idna to 0.3 (#816) 2022-09-09 07:38:59 +00:00
Tom Dryer
3c051d52e7 Remove dependency on regex crate (#815)
Replace implementation of DKIM body canonicalization to remove
dependency on the `regex` crate.

Fixes #768.
2022-08-22 09:44:10 +02:00
André Cruz
d6128a146e use a generic transport trait for async connections (#805)
Rely on a generic transport trait and allow passing in one. This
will enable use cases where we don't have a real Tokio TCP stream,
or have to bind a specific source address before establishing
the connection.
2022-07-27 09:40:13 +02:00
André Cruz
fab6680150 Fix clippy warnings (#807)
Depending on the features chosen these attributes were left
unused.
2022-07-25 18:00:17 +02:00
52 changed files with 743 additions and 465 deletions

View File

@@ -13,7 +13,7 @@ env:
jobs:
rustfmt:
name: rustfmt / nightly-2022-02-11
name: rustfmt / nightly-2022-11-12
runs-on: ubuntu-latest
steps:
@@ -22,7 +22,7 @@ jobs:
- name: Install rust
run: |
rustup default nightly-2022-02-11
rustup default nightly-2022-11-12
rustup component add rustfmt
- name: cargo fmt
@@ -52,17 +52,11 @@ jobs:
- name: Checkout
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
run: rustup update --no-self-update stable
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install cargo hack
run: cargo install cargo-hack --debug
@@ -81,27 +75,21 @@ jobs:
rust: stable
- name: beta
rust: beta
- name: 1.56.0
rust: 1.56.0
- name: 1.60.0
rust: 1.60.0
steps:
- name: Checkout
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
run: |
rustup default ${{ matrix.rust }}
rustup update --no-self-update ${{ matrix.rust }}
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install postfix
run: |
DEBIAN_FRONTEND=noninteractive sudo apt-get update
@@ -134,10 +122,10 @@ jobs:
run: cargo test
- 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-native-certs,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)
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-native-certs,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:
# name: Coverage

View File

@@ -1,3 +1,81 @@
<a name="v0.10.4"></a>
### v0.10.4 (2023-04-02)
#### Misc
* Bumped rustls to 0.21 and all related dependencies ([#867])
[#867]: https://github.com/lettre/lettre/pull/867
<a name="v0.10.3"></a>
### v0.10.3 (2023-02-20)
#### Announcements
It was found that what had been used until now as a basic lettre 0.10
`MessageBuilder::body` example failed to mention that for maximum
compatibility with various email clients a `Content-Type` header
should always be present in the message.
##### Before
```rust
Message::builder()
// [...] some headers skipped for brevity
.body(String::from("A plaintext or html body"))?
```
##### Patch
```diff
Message::builder()
// [...] some headers skipped for brevity
+ .header(ContentType::TEXT_PLAIN) // or `TEXT_HTML` if the body is html
.body(String::from("A plaintext or html body"))?
```
#### Features
* Add support for rustls-native-certs when using rustls ([#843])
[#843]: https://github.com/lettre/lettre/pull/843
<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>
### v0.10.1 (2022-07-20)

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.10.1"
version = "0.10.4"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021"
rust-version = "1.56"
rust-version = "1.60"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -19,7 +19,7 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
[dependencies]
idna = "0.2"
idna = "0.3"
once_cell = { version = "1", optional = true }
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 }
mime = { version = "0.3.4", optional = true }
fastrand = { version = "1.4", optional = true }
quoted_printable = { version = "0.4", optional = true }
base64 = { version = "0.13", optional = true }
email-encoding = { version = "0.1.1", optional = true }
quoted_printable = { version = "0.4.6", optional = true }
base64 = { version = "0.21", optional = true }
email-encoding = { version = "0.2", optional = true }
# file transport
uuid = { version = "1", features = ["v4"], optional = true }
@@ -43,9 +43,10 @@ socket2 = { version = "0.4.4", optional = true }
## tls
native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true }
rustls-pemfile = { version = "1", optional = true }
webpki-roots = { version = "0.22", optional = true }
rustls-native-certs = { version = "0.6.2", optional = true }
webpki-roots = { version = "0.23", optional = true }
boring = { version = "2.0.0", optional = true }
# async
@@ -54,28 +55,27 @@ futures-util = { version = "0.3.7", default-features = false, features = ["io"],
async-trait = { version = "0.1", optional = true }
## 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 }
futures-rustls = { version = "0.22", optional = true }
futures-rustls = { version = "0.24", optional = true }
## 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_rustls = { package = "tokio-rustls", version = "0.23", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.24", optional = true }
tokio1_boring = { package = "tokio-boring", version = "2.1.4", optional = true }
## dkim
sha2 = { version = "0.10", optional = true }
rsa = { version = "0.6.0", optional = true }
sha2 = { version = "0.10", optional = true, features = ["oid"] }
rsa = { version = "0.8", optional = true }
ed25519-dalek = { version = "1.0.1", optional = true }
regex = { version = "1", default-features = false, features = ["std"], optional = true }
# email formats
email_address = { version = "0.2.1", default-features = false }
[dev-dependencies]
pretty_assertions = "1"
criterion = "0.3"
criterion = "0.4"
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
tracing-subscriber = "0.3"
glob = "0.3"
@@ -83,7 +83,7 @@ walkdir = "2"
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1"
maud = "0.23"
maud = "0.24"
[[bench]]
harness = false
@@ -95,10 +95,10 @@ builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"]
mime03 = ["mime"]
# transports
file-transport = ["uuid"]
file-transport = ["uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "serde_json", "file-transport"]
sendmail-transport = []
smtp-transport = ["base64", "nom", "socket2", "once_cell"]
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
smtp-transport = ["base64", "nom", "socket2", "once_cell", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
pool = ["futures-util"]
@@ -115,7 +115,7 @@ tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
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]
all-features = true

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.10.1">
<img src="https://deps.rs/crate/lettre/0.10.1/status.svg"
<a href="https://deps.rs/crate/lettre/0.10.4">
<img src="https://deps.rs/crate/lettre/0.10.4/status.svg"
alt="dependency status" />
</a>
</div>
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
## Supported Rust Versions
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.
## 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`:
```toml
@@ -67,6 +67,7 @@ lettre = "0.10"
```
```rust,no_run
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
@@ -75,10 +76,11 @@ let email = Message::builder()
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")

View File

@@ -1,6 +1,6 @@
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
AsyncTransport, Message,
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncStd1Executor, AsyncTransport, Message,
};
#[async_std::main]
@@ -12,10 +12,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
@@ -27,6 +28,6 @@ async fn main() {
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,6 +1,6 @@
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
AsyncTransport, Message,
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncStd1Executor, AsyncTransport, Message,
};
#[async_std::main]
@@ -12,10 +12,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
@@ -27,6 +28,6 @@ async fn main() {
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,4 +1,4 @@
use lettre::{Message, SmtpTransport, Transport};
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
fn main() {
tracing_subscriber::fmt::init();
@@ -8,6 +8,7 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
@@ -17,6 +18,6 @@ fn main() {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,6 +1,7 @@
use std::fs;
use lettre::{
message::header::ContentType,
transport::smtp::{
authentication::Credentials,
client::{Certificate, Tls, TlsParameters},
@@ -16,18 +17,19 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
// Use a custom certificate stored on disk to securely verify the server's certificate
let pem_cert = fs::read("certificate.pem").unwrap();
let cert = Certificate::from_pem(&pem_cert).unwrap();
let tls = TlsParameters::builder("smtp.server.com".to_string())
let tls = TlsParameters::builder("smtp.server.com".to_owned())
.add_root_certificate(cert)
.build()
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to the smtp server
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
@@ -39,6 +41,6 @@ fn main() {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,4 +1,7 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() {
tracing_subscriber::fmt::init();
@@ -8,10 +11,11 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
@@ -22,6 +26,6 @@ fn main() {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -1,4 +1,7 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() {
tracing_subscriber::fmt::init();
@@ -8,10 +11,11 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
@@ -22,6 +26,6 @@ fn main() {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -2,8 +2,8 @@
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor,
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncTransport, Message, Tokio1Executor,
};
use tokio1_crate as tokio;
@@ -16,10 +16,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio1Executor> =
@@ -31,6 +32,6 @@ async fn main() {
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -2,8 +2,8 @@
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor,
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncTransport, Message, Tokio1Executor,
};
use tokio1_crate as tokio;
@@ -16,10 +16,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio1Executor> =
@@ -31,6 +32,6 @@ async fn main() {
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
Err(e) => panic!("Could not send email: {e:?}"),
}
}

View File

@@ -184,7 +184,7 @@ where
let domain = domain.as_ref();
Address::check_domain(domain)?;
let serialized = format!("{}@{}", user, domain);
let serialized = format!("{user}@{domain}");
Ok(Address {
serialized,
at_start: user.len(),

12
src/base64.rs Normal file
View 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)
}

View File

@@ -6,7 +6,7 @@
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.56.0 or newer.
//! Lettre requires Rust 1.60 or newer.
//!
//! ## Features
//!
@@ -109,7 +109,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.10.1")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.4")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
@@ -135,6 +135,8 @@
clippy::manual_assert,
clippy::unnecessary_join,
clippy::wildcard_imports,
clippy::str_to_string,
clippy::empty_structs_with_brackets,
clippy::zero_sized_map_values
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
@@ -203,6 +205,8 @@ Make sure to apply the same to any of your crate dependencies that use the `lett
}
pub mod address;
#[cfg(any(feature = "smtp-transport", feature = "dkim"))]
mod base64;
pub mod error;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod executor;

View File

@@ -96,7 +96,7 @@ impl Attachment {
builder.header(header::ContentDisposition::attachment(&filename))
}
Disposition::Inline(content_id) => builder
.header(header::ContentId::from(format!("<{}>", content_id)))
.header(header::ContentId::from(format!("<{content_id}>")))
.header(header::ContentDisposition::inline()),
};
builder = builder.header(content_type);

View File

@@ -7,9 +7,7 @@ use std::{
};
use ed25519_dalek::Signer;
use once_cell::sync::Lazy;
use regex::bytes::Regex;
use rsa::{pkcs1::DecodeRsaPrivateKey, Hash, PaddingScheme, RsaPrivateKey};
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs1v15::Pkcs1v15Sign, RsaPrivateKey};
use sha2::{Digest, Sha256};
use crate::message::{
@@ -123,14 +121,14 @@ impl DkimSigningKey {
RsaPrivateKey::from_pkcs1_pem(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
),
DkimSigningAlgorithm::Ed25519 => {
InnerDkimSigningKey::Ed25519(
ed25519_dalek::Keypair::from_bytes(&base64::decode(private_key).map_err(
|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)),
)?)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
DkimSigningAlgorithm::Ed25519 => InnerDkimSigningKey::Ed25519(
ed25519_dalek::Keypair::from_bytes(
&crate::base64::decode(private_key).map_err(|err| {
DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err))
})?,
)
}
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
),
}))
}
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
@@ -219,24 +217,34 @@ fn dkim_header_format(
/// Canonicalize the body of an email
fn dkim_canonicalize_body(
body: &[u8],
mut body: &[u8],
canonicalization: DkimCanonicalizationType,
) -> 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 {
DkimCanonicalizationType::Simple => RE.replace(body, &b"\r\n"[..]),
DkimCanonicalizationType::Relaxed => {
let body = RE_DOUBLE_SPACE.replace_all(body, &b" "[..]);
let body = match RE_SPACE_EOL.replace_all(&body, &b"\r\n"[..]) {
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),
DkimCanonicalizationType::Simple => {
// Remove empty lines at end
while body.ends_with(b"\r\n\r\n") {
body = &body[..body.len() - 2];
}
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()
.as_secs();
let headers = message.headers();
let body_hash = Sha256::digest(&dkim_canonicalize_body(
let body_hash = Sha256::digest(dkim_canonicalize_body(
&message.body_raw(),
dkim_config.canonicalization.body,
));
let bh = base64::encode(body_hash);
let bh = crate::base64::encode(body_hash);
let mut signed_headers_list =
dkim_config
.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());
let hashed_headers = hashed_headers.finalize();
let signature = match &dkim_config.private_key.0 {
InnerDkimSigningKey::Rsa(private_key) => base64::encode(
InnerDkimSigningKey::Rsa(private_key) => crate::base64::encode(
private_key
.sign(
PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA2_256)),
&hashed_headers,
)
.sign(Pkcs1v15Sign::new::<Sha256>(), &hashed_headers)
.unwrap(),
),
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(
@@ -416,8 +421,9 @@ mod test {
header::{HeaderName, HeaderValue},
Header, Message,
},
dkim_canonicalize_headers, dkim_sign_fixed_time, DkimCanonicalization,
DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm, DkimSigningKey,
dkim_canonicalize_body, dkim_canonicalize_headers, dkim_sign_fixed_time,
DkimCanonicalization, DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm,
DkimSigningKey,
};
use crate::StdError;
@@ -471,9 +477,9 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
.from("Test O'Leary <test+ezrz@example.net>".parse().unwrap())
.to("Test2 <test2@example.org>".parse().unwrap())
.date(std::time::UNIX_EPOCH)
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_string()))
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_owned()))
.subject("Test with utf-8 ë")
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_string()).unwrap()
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_owned()).unwrap()
}
#[test]
@@ -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")
}
#[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]
fn test_signature_rsa_simple() {
let mut message = test_message();
@@ -497,8 +521,8 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
dkim_sign_fixed_time(
&mut message,
&DkimConfig::new(
"dkimtest".to_string(),
"example.org".to_string(),
"dkimtest".to_owned(),
"example.org".to_owned(),
signing_key,
vec![
HeaderName::new_from_ascii_str("Date"),
@@ -546,8 +570,8 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
dkim_sign_fixed_time(
&mut message,
&DkimConfig::new(
"dkimtest".to_string(),
"example.org".to_string(),
"dkimtest".to_owned(),
"example.org".to_owned(),
signing_key,
vec![
HeaderName::new_from_ascii_str("Date"),
@@ -564,7 +588,7 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
);
let signed = message.formatted();
let signed = std::str::from_utf8(&signed).unwrap();
println!("{}", signed);
println!("{signed}");
assert_eq!(
signed,
std::concat!(

View File

@@ -99,7 +99,7 @@ mod test {
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"7bit".to_string(),
"7bit".to_owned(),
));
assert_eq!(
@@ -109,7 +109,7 @@ mod test {
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_string(),
"base64".to_owned(),
));
assert_eq!(

View File

@@ -16,8 +16,8 @@ impl ContentDisposition {
pub fn inline() -> Self {
Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(),
"inline".to_string(),
"inline".to_string(),
"inline".to_owned(),
"inline".to_owned(),
))
}
@@ -33,17 +33,19 @@ impl ContentDisposition {
}
fn with_name(kind: &str, file_name: &str) -> Self {
let raw_value = format!("{}; filename=\"{}\"", kind, file_name);
let raw_value = format!("{kind}; filename=\"{file_name}\"");
let mut encoded_value = String::new();
let line_len = "Content-Disposition: ".len();
let mut w = EmailWriter::new(&mut encoded_value, line_len, false);
w.write_str(kind).expect("writing `kind` returned an error");
w.write_char(';').expect("writing `;` returned an error");
w.space();
{
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
w.write_str(kind).expect("writing `kind` returned an error");
w.write_char(';').expect("writing `;` returned an error");
w.optional_breakpoint();
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
.expect("some Write implementation returned an error");
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
.expect("some Write implementation returned an error");
}
Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(),
@@ -88,12 +90,12 @@ mod test {
headers.set(ContentDisposition::inline());
assert_eq!(format!("{}", headers), "Content-Disposition: inline\r\n");
assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");
headers.set(ContentDisposition::attachment("something.txt"));
assert_eq!(
format!("{}", headers),
format!("{headers}"),
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
);
}
@@ -104,7 +106,7 @@ mod test {
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"),
"inline".to_string(),
"inline".to_owned(),
));
assert_eq!(
@@ -114,7 +116,7 @@ mod test {
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"),
"attachment; filename=\"something.txt\"".to_string(),
"attachment; filename=\"something.txt\"".to_owned(),
));
assert_eq!(

View File

@@ -135,8 +135,7 @@ mod serde {
match ContentType::parse(mime) {
Ok(content_type) => Ok(content_type),
Err(_) => Err(E::custom(format!(
"Couldn't parse the following MIME-Type: {}",
mime
"Couldn't parse the following MIME-Type: {mime}"
))),
}
}
@@ -179,14 +178,14 @@ mod test {
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"),
"text/plain; charset=utf-8".to_string(),
"text/plain; charset=utf-8".to_owned(),
));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"),
"text/html; charset=utf-8".to_string(),
"text/html; charset=utf-8".to_owned(),
));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));

View File

@@ -90,7 +90,7 @@ mod test {
assert_eq!(
headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_string()
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_owned()
);
// Tue, 15 Nov 1994 08:12:32 GMT
@@ -110,7 +110,7 @@ mod test {
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:31 +0000".to_string(),
"Tue, 15 Nov 1994 08:12:31 +0000".to_owned(),
));
assert_eq!(
@@ -122,7 +122,7 @@ mod test {
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:32 +0000".to_string(),
"Tue, 15 Nov 1994 08:12:32 +0000".to_owned(),
));
assert_eq!(

View File

@@ -30,8 +30,10 @@ macro_rules! mailbox_header {
fn display(&self) -> HeaderValue {
let mut encoded_value = String::new();
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)
}
@@ -78,8 +80,10 @@ macro_rules! mailboxes_header {
fn display(&self) -> HeaderValue {
let mut encoded_value = String::new();
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)
}
@@ -248,7 +252,7 @@ mod test {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com".to_string(),
"kayo@example.com".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from)));
@@ -261,7 +265,7 @@ mod test {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_string(),
"K. <kayo@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from)));
@@ -277,7 +281,7 @@ mod test {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com, pony@domain.tld".to_string(),
"kayo@example.com, pony@domain.tld".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -293,7 +297,7 @@ mod test {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -309,7 +313,7 @@ mod test {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_string(),
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -320,7 +324,7 @@ mod test {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2".to_string(),
"Test, test <1@example.com>, Test2, test2".to_owned(),
));
assert_eq!(headers.get::<From>(), None);

View File

@@ -323,10 +323,12 @@ impl HeaderValue {
}
}
#[cfg(feature = "dkim")]
pub(crate) fn get_raw(&self) -> &str {
&self.raw_value
}
#[cfg(feature = "dkim")]
pub(crate) fn get_encoded(&self) -> &str {
&self.encoded_value
}
@@ -340,28 +342,21 @@ struct HeaderValueEncoder<'a> {
impl<'a> HeaderValueEncoder<'a> {
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
let (words_iter, encoder) = Self::new(name, value, f);
encoder.format(words_iter)
let encoder = Self::new(name, f);
encoder.format(value.split_inclusive(' '))
}
fn new(
name: &str,
value: &'a str,
writer: &'a mut dyn Write,
) -> (WordsPlusFillIterator<'a>, Self) {
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
let line_len = name.len() + ": ".len();
let writer = EmailWriter::new(writer, line_len, false);
let writer = EmailWriter::new(writer, line_len, 0, false, false);
(
WordsPlusFillIterator { s: value },
Self {
writer,
encode_buf: String::new(),
},
)
Self {
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 {
let allowed = allowed_str(next_word);
@@ -389,71 +384,20 @@ impl<'a> HeaderValueEncoder<'a> {
return Ok(());
}
// It is important that we don't encode leading whitespace otherwise it breaks wrapping.
let first_not_allowed = self
.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 = self.encode_buf.trim_end_matches(' ');
email_encoding::headers::rfc2047::encode(prefix, &mut self.writer)?;
let (prefix, to_encode, suffix) = match first_not_allowed {
Some(first_not_allowed) => {
let last_not_allowed = last_not_allowed.unwrap();
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)?;
// TODO: add a better API for doing this in email-encoding
let spaces = self.encode_buf.len() - prefix.len();
for _ in 0..spaces {
self.writer.space();
}
self.encode_buf.clear();
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 {
s.bytes().all(allowed_char)
}
@@ -466,7 +410,8 @@ const fn allowed_char(c: u8) -> bool {
mod tests {
use pretty_assertions::assert_eq;
use super::{HeaderName, HeaderValue, Headers};
use super::{HeaderName, HeaderValue, Headers, To};
use crate::message::Mailboxes;
#[test]
fn valid_headername() {
@@ -529,7 +474,7 @@ mod tests {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_string(),
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_owned(),
));
assert_eq!(
@@ -543,7 +488,7 @@ mod tests {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_string(),
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_owned(),
));
assert_eq!(
@@ -561,7 +506,7 @@ mod tests {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
));
assert_eq!(
@@ -580,7 +525,7 @@ mod tests {
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string()
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
));
assert_eq!(
@@ -598,7 +543,7 @@ mod tests {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string()
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
));
assert_eq!(
@@ -612,12 +557,12 @@ mod tests {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"Seán <sean@example.com>".to_string(),
"Seán <sean@example.com>".to_owned(),
));
assert_eq!(
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"
);
}
@@ -626,7 +571,7 @@ mod tests {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"🌎 <world@example.com>".to_string(),
"🌎 <world@example.com>".to_owned(),
));
assert_eq!(
@@ -638,19 +583,46 @@ mod tests {
#[test]
fn format_special_with_folding() {
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(),
) );
let to = To::from(Mailboxes::from_iter([
"🌍 <world@example.com>".parse().unwrap(),
"🦆 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!(
headers.to_string(),
concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= Everywhere\r\n",
" <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n",
" =?utf-8?b?0L7QstC40Yc=?= <ivanov@example.com>, J=?utf-8?b?xIFuaXMgQsST?=\r\n",
" =?utf-8?b?cnppxYbFoQ==?= <janis@example.com>, Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n",
" =?utf-8?b?ZGHDrQ==?= <sean@example.com>\r\n",
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhiBFdmVyeXdo?=\r\n",
" =?utf-8?b?ZXJl?= <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LI=?=\r\n",
" =?utf-8?b?0LDQvSDQmNCy0LDQvdC+0LLQuNGH?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\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_owned(),
));
// 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",
)
);
}
@@ -661,7 +633,7 @@ mod tests {
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(),)
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
);
assert_eq!(
@@ -682,7 +654,7 @@ mod tests {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! \r\n This is \" bad \0. 👋".to_string(),
"Hello! \r\n This is \" bad \0. 👋".to_owned(),
));
assert_eq!(
@@ -697,35 +669,38 @@ mod tests {
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
)
);
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(),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
)
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".to_string(),
"Someone <somewhere@example.com>".to_owned(),
));
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"quoted-printable".to_string(),
"quoted-printable".to_owned(),
));
// 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!(
"Subject: Hello! This is lettre, and this\r\n",
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\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",
" <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n",
" =?utf-8?b?0L7QstC40Yc=?= <ivanov@example.com>, J=?utf-8?b?xIFuaXMgQsST?=\r\n",
" =?utf-8?b?cnppxYbFoQ==?= <janis@example.com>, Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n",
" =?utf-8?b?ZGHDrQ==?= <sean@example.com>\r\n",
"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",
"From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
)
@@ -737,14 +712,14 @@ mod tests {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_string(),
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_owned(),
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go;\r\n",
" ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg=?utf-8?b?772G44Gj?=\r\n"
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7?=\r\n",
" =?utf-8?b?Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n",
)
);
}

View File

@@ -91,14 +91,14 @@ mod test {
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"),
"1.0".to_string(),
"1.0".to_owned(),
));
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"),
"0.1".to_string(),
"0.1".to_owned(),
));
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));

View File

@@ -109,12 +109,23 @@ 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]
fn parse_ascii() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Sample subject".to_string(),
"Sample subject".to_owned(),
));
assert_eq!(

View File

@@ -13,7 +13,7 @@ impl Serialize for Mailbox {
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
serializer.collect_str(self)
}
}
@@ -111,7 +111,7 @@ impl Serialize for Mailboxes {
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
serializer.collect_str(self)
}
}

View File

@@ -70,7 +70,7 @@ impl Mailbox {
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
if let Some(name) = &self.name {
email_encoding::headers::quoted_string::encode(name, w)?;
w.space();
w.optional_breakpoint();
w.write_char('<')?;
}
@@ -275,7 +275,7 @@ impl Mailboxes {
for mailbox in self.iter() {
if !mem::take(&mut first) {
w.write_char(',')?;
w.space();
w.optional_breakpoint();
}
mailbox.encode(w)?;
@@ -397,7 +397,7 @@ fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
} else {
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
f.write_char('"')?;
for &c in s.as_bytes() {
for c in s.chars() {
write_quoted_string_char(f, c)?;
}
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
fn write_quoted_string_char(f: &mut Formatter<'_>, c: u8) -> FmtResult {
fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
match c {
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
1..=8 | 11 | 12 | 14..=31 | 127 |
// Can not be encoded.
'\n' | '\r' => Err(std::fmt::Error),
// Note, not qcontent but can be put before or after any qcontent.
b'\t' |
b' ' |
// Note, not qcontent but can be put before or after any qcontent.
'\t' | ' ' => f.write_char(c),
// The rest of the US-ASCII except \ and "
33 |
35..=91 |
93..=126 |
c if match c as u32 {
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
1..=8 | 11 | 12 | 14..=31 | 127 |
// Non-ascii characters will be escaped separately later.
128..=255
// The rest of the US-ASCII except \ and "
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),
c => {
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
f.write_char('\\')?;
f.write_char(c.into())
}
}
_ => {
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
f.write_char('\\')?;
f.write_char(c)
}
}
}
#[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]
fn mailbox_format_address_with_color() {
assert_eq!(
@@ -589,7 +620,7 @@ mod test {
#[test]
fn parse_address_from_tuple() {
assert_eq!(
("K.".to_string(), "kayo@example.com".to_string()).try_into(),
("K.".to_owned(), "kayo@example.com".to_owned()).try_into(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()

View File

@@ -190,9 +190,9 @@ impl MultiPartKind {
},
boundary,
match self {
Self::Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
Self::Signed { protocol, micalg } =>
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
_ => String::new(),
}
)

View File

@@ -14,7 +14,7 @@
//! The easiest way of creating a message, which uses a plain text body.
//!
//! ```rust
//! use lettre::message::Message;
//! use lettre::message::{header::ContentType, Message};
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
@@ -23,6 +23,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//! # Ok(())
//! # }
@@ -38,6 +39,7 @@
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
//! Content-Type: text/plain; charset=utf-8
//! Content-Transfer-Encoding: 7bit
//!
//! Be happy!
@@ -343,9 +345,9 @@ impl MessageBuilder {
let hostname = hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string());
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
self.header(header::MessageId::from(
// https://tools.ietf.org/html/rfc5322#section-3.6.4
@@ -356,7 +358,7 @@ impl MessageBuilder {
}
/// 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 {
self.header(header::UserAgent::from(id))
}
@@ -539,7 +541,7 @@ impl Message {
/// .reply_to("Bob <bob@example.org>".parse().unwrap())
/// .to("Carla <carla@example.net>".parse().unwrap())
/// .subject("Hello")
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_string())
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
/// .unwrap();
/// let key = "-----BEGIN RSA PRIVATE KEY-----
/// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
@@ -570,8 +572,8 @@ impl Message {
/// -----END RSA PRIVATE KEY-----";
/// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
/// message.sign(&DkimConfig::default_config(
/// "dkimtest".to_string(),
/// "example.org".to_string(),
/// "dkimtest".to_owned(),
/// "example.org".to_owned(),
/// signing_key,
/// ));
/// println!(
@@ -673,7 +675,7 @@ mod test {
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\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",
"\r\n",
"Happy new year!"
@@ -711,7 +713,7 @@ mod test {
"Bcc: hidden@example.com\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\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",
"\r\n",
"Happy new year!"

View File

@@ -71,7 +71,7 @@ impl fmt::Display for Error {
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
write!(f, ": {e}")?;
}
Ok(())

View File

@@ -208,18 +208,18 @@ impl FileTransport {
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
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 json_file = self.path.join(format!("{}.json", email_id));
let json = fs::read(&json_file).map_err(error::io)?;
let json_file = self.path.join(format!("{email_id}.json"));
let json = fs::read(json_file).map_err(error::io)?;
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
Ok((envelope, eml))
}
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.
#[cfg(feature = "file-transport-envelope")]
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 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 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 file = self.path(&email_id, "eml");
#[cfg(feature = "tracing")]
tracing::debug!(?file, "writing email to");
fs::write(file, email).map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")]
@@ -306,6 +308,8 @@ where
let email_id = Uuid::new_v4();
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)?;
#[cfg(feature = "file-transport-envelope")]

View File

@@ -65,7 +65,7 @@
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
//! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
//!
//! // Open a remote connection to the SMTP relay server
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
@@ -127,6 +127,9 @@ pub trait Transport {
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "tracing")]
tracing::trace!("starting to send an email");
let raw = message.formatted();
self.send_raw(message.envelope(), &raw)
}
@@ -149,6 +152,9 @@ pub trait AsyncTransport {
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
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 envelope = message.envelope();
self.send_raw(envelope, &raw).await

View File

@@ -68,7 +68,7 @@ impl fmt::Display for Error {
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
write!(f, ": {e}")?;
}
Ok(())

View File

@@ -232,6 +232,9 @@ impl Transport for SendmailTransport {
type Error = 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
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> {
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);
// 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> {
use tokio1_crate::io::AsyncWriteExt;
#[cfg(feature = "tracing")]
tracing::debug!(command = ?self.inner.command, "sending email with");
let mut command = self.tokio1_command(envelope);
// Spawn the sendmail command

View File

@@ -1,7 +1,8 @@
#[cfg(feature = "pool")]
use std::sync::Arc;
use std::{
fmt::{self, Debug},
marker::PhantomData,
sync::Arc,
time::Duration,
};

View File

@@ -127,7 +127,7 @@ mod test {
fn test_plain() {
let mechanism = Mechanism::Plain;
let credentials = Credentials::new("username".to_string(), "password".to_string());
let credentials = Credentials::new("username".to_owned(), "password".to_owned());
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
@@ -140,7 +140,7 @@ mod test {
fn test_login() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
assert_eq!(
mechanism.response(&credentials, Some("Username")).unwrap(),
@@ -158,8 +158,8 @@ mod test {
let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new(
"username".to_string(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
"username".to_owned(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
);
assert_eq!(
@@ -172,7 +172,7 @@ mod test {
#[test]
fn test_from_user_pass_for_credentials() {
assert_eq!(
Credentials::new("alice".to_string(), "wonderland".to_string()),
Credentials::new("alice".to_owned(), "wonderland".to_owned()),
Credentials::from(("alice", "wonderland"))
);
}

View File

@@ -2,6 +2,8 @@ use std::{fmt::Display, net::IpAddr, time::Duration};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[cfg(feature = "tokio1")]
use super::async_net::AsyncTokioStream;
#[cfg(feature = "tracing")]
use super::escape_crlf;
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
@@ -46,6 +48,18 @@ impl AsyncSmtpConnection {
&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
///
/// Sends EHLO and parses server information
@@ -303,7 +317,7 @@ impl AsyncSmtpConnection {
} else {
Err(error::code(
response.code(),
response.first_line().map(|s| s.to_owned()),
Some(response.message().collect()),
))
}
}

View File

@@ -1,5 +1,5 @@
use std::{
io, mem,
fmt, io, mem,
net::{IpAddr, SocketAddr},
pin::Pin,
task::{Context, Poll},
@@ -19,7 +19,7 @@ use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio1-boring-tls")]
use tokio1_boring::SslStream as Tokio1SslStream;
#[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")]
use tokio1_crate::net::{
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};
/// A network stream
#[derive(Debug)]
pub struct AsyncNetworkStream {
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
// usually only one TLS backend at a time is going to be enabled,
// so clippy::large_enum_variant doesn't make sense here
#[allow(clippy::large_enum_variant)]
#[allow(dead_code)]
#[derive(Debug)]
enum InnerAsyncNetworkStream {
/// Plain Tokio 1.x TCP stream
#[cfg(feature = "tokio1")]
Tokio1Tcp(Tokio1TcpStream),
Tokio1Tcp(Box<dyn AsyncTokioStream>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-native-tls")]
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>),
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-rustls-tls")]
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>),
Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-boring-tls")]
Tokio1BoringTls(Tokio1SslStream<Tokio1TcpStream>),
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
/// Plain Tokio 1.x TCP stream
#[cfg(feature = "async-std1")]
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")]
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
server: T,
@@ -175,7 +194,8 @@ impl AsyncNetworkStream {
}
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 {
stream.upgrade_tls(tls_parameters).await?;
}
@@ -300,10 +320,10 @@ impl AsyncNetworkStream {
feature = "tokio1-boring-tls"
))]
async fn upgrade_tokio1_tls(
tcp_stream: Tokio1TcpStream,
tcp_stream: Box<dyn AsyncTokioStream>,
tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = tls_parameters.domain().to_string();
let domain = tls_parameters.domain().to_owned();
match tls_parameters.connector {
#[cfg(feature = "native-tls")]

View File

@@ -285,7 +285,7 @@ impl SmtpConnection {
} else {
Err(error::code(
response.code(),
response.first_line().map(|s| s.to_owned()),
Some(response.message().collect()),
))
};
}

View File

@@ -11,7 +11,7 @@
//! client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT,
//! };
//!
//! let hello = ClientId::Domain("my_hostname".to_string());
//! let hello = ClientId::Domain("my_hostname".to_owned());
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?;
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
@@ -29,6 +29,8 @@ use std::fmt::Debug;
pub use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_net::AsyncNetworkStream;
#[cfg(feature = "tokio1")]
pub use self::async_net::AsyncTokioStream;
use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub(super) use self::tls::InnerTlsParameters;
@@ -36,7 +38,7 @@ pub(super) use self::tls::InnerTlsParameters;
pub use self::tls::TlsVersion;
pub use self::{
connection::SmtpConnection,
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]

View File

@@ -1,8 +1,9 @@
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
use std::{
io::{self, Read, Write},
mem,
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
sync::Arc,
time::Duration,
};

View File

@@ -3,13 +3,16 @@ use std::fmt::{self, Debug};
use std::{sync::Arc, time::SystemTime};
#[cfg(feature = "boring-tls")]
use boring::ssl::{SslConnector, SslVersion};
use boring::{
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::{
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName,
ClientConfig, Error as TlsError, RootCertStore, ServerName,
};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -84,22 +87,52 @@ impl Debug for Tls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::None => f.pad("None"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"),
}
}
}
/// Source for the base set of root certificates to trust.
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug)]
pub enum CertificateStore {
/// Use the default for the TLS backend.
///
/// For native-tls, this will use the system certificate store on Windows, the keychain on
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
///
/// For rustls, this will also use the the system store if the `rustls-native-certs` feature is
/// enabled, or will fall back to `webpki-roots`.
///
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
Default,
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
///
/// This option is only available in the rustls backend.
#[cfg(feature = "webpki-roots")]
WebpkiRoots,
/// Don't use any system certificates.
None,
}
impl Default for CertificateStore {
fn default() -> Self {
CertificateStore::Default
}
}
/// Parameters to use for secure clients
#[derive(Clone)]
pub struct TlsParameters {
pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server
pub(super) domain: String,
#[cfg(feature = "boring-tls")]
pub(super) accept_invalid_hostnames: bool,
}
@@ -107,6 +140,7 @@ pub struct TlsParameters {
#[derive(Debug, Clone)]
pub struct TlsParametersBuilder {
domain: String,
cert_store: CertificateStore,
root_certs: Vec<Certificate>,
accept_invalid_hostnames: bool,
accept_invalid_certs: bool,
@@ -119,6 +153,7 @@ impl TlsParametersBuilder {
pub fn new(domain: String) -> Self {
Self {
domain,
cert_store: CertificateStore::Default,
root_certs: Vec::new(),
accept_invalid_hostnames: false,
accept_invalid_certs: false,
@@ -127,6 +162,12 @@ impl TlsParametersBuilder {
}
}
/// Set the source for the base set of root certificates to trust.
pub fn certificate_store(mut self, cert_store: CertificateStore) -> Self {
self.cert_store = cert_store;
self
}
/// Add a custom root certificate
///
/// Can be used to safely connect to a server using a self signed certificate, for example.
@@ -207,6 +248,18 @@ impl TlsParametersBuilder {
pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder();
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
tls_builder.disable_built_in_roots(true);
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in native tls"
)))
}
}
for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls);
}
@@ -229,6 +282,7 @@ impl TlsParametersBuilder {
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
}
@@ -244,6 +298,21 @@ impl TlsParametersBuilder {
if self.accept_invalid_certs {
tls_builder.set_verify(SslVerifyMode::NONE);
} else {
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
// Replace the default store with an empty store.
tls_builder
.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in boring tls"
)))
}
}
let cert_store = tls_builder.cert_store_mut();
for cert in self.root_certs {
@@ -297,20 +366,58 @@ impl TlsParametersBuilder {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
} else {
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
let mut valid_count = 0;
let mut invalid_count = 0;
for cert in native_certs {
match store.add(&rustls::Certificate(cert.0)) {
Ok(_) => valid_count += 1,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::debug!("certificate parsing failed: {:?}", err);
invalid_count += 1;
}
}
}
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {valid_count} valid and {invalid_count} invalid certs"
);
Ok(())
}
#[cfg(feature = "webpki-roots")]
fn load_webpki_roots(store: &mut RootCertStore) {
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
}
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store)?;
#[cfg(all(not(feature = "rustls-native-certs"), feature = "webpki-roots"))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(feature = "webpki-roots")]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
}
}
root_cert_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
},
));
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
root_cert_store,
@@ -322,6 +429,7 @@ impl TlsParametersBuilder {
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
}

View File

@@ -59,7 +59,7 @@ impl Display for Mail {
self.sender.as_ref().map_or("", |s| s.as_ref())
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
write!(f, " {parameter}")?;
}
f.write_str("\r\n")
}
@@ -84,7 +84,7 @@ impl Display for Rcpt {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
write!(f, " {parameter}")?;
}
f.write_str("\r\n")
}
@@ -144,7 +144,7 @@ impl Display for Help {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("HELP")?;
if let Some(argument) = &self.argument {
write!(f, " {}", argument)?;
write!(f, " {argument}")?;
}
f.write_str("\r\n")
}
@@ -220,7 +220,7 @@ pub struct Auth {
impl Display for Auth {
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() {
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
@@ -271,7 +271,7 @@ impl Auth {
#[cfg(feature = "tracing")]
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)?;
#[cfg(feature = "tracing")]
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
@@ -296,15 +296,15 @@ mod test {
#[test]
fn test_display() {
let id = ClientId::Domain("localhost".to_string());
let id = ClientId::Domain("localhost".to_owned());
let email = Address::from_str("test@example.com").unwrap();
let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
keyword: "TEST".to_owned(),
value: Some("value".to_owned()),
};
let rcpt_parameter = RcptParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
keyword: "TEST".to_owned(),
value: Some("value".to_owned()),
};
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
assert_eq!(
@@ -341,24 +341,18 @@ mod test {
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n"
);
assert_eq!(format!("{}", Quit), "QUIT\r\n");
assert_eq!(format!("{}", Data), "DATA\r\n");
assert_eq!(format!("{}", Noop), "NOOP\r\n");
assert_eq!(format!("{Quit}"), "QUIT\r\n");
assert_eq!(format!("{Data}"), "DATA\r\n");
assert_eq!(format!("{Noop}"), "NOOP\r\n");
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
assert_eq!(
format!("{}", Help::new(Some("test".to_string()))),
format!("{}", Help::new(Some("test".to_owned()))),
"HELP test\r\n"
);
assert_eq!(
format!("{}", Vrfy::new("test".to_string())),
"VRFY test\r\n"
);
assert_eq!(
format!("{}", Expn::new("test".to_string())),
"EXPN test\r\n"
);
assert_eq!(format!("{}", Rset), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string());
assert_eq!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\r\n");
assert_eq!(format!("{Rset}"), "RSET\r\n");
let credentials = Credentials::new("user".to_owned(), "password".to_owned());
assert_eq!(
format!(
"{}",

View File

@@ -137,15 +137,15 @@ impl fmt::Display for Error {
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Kind::Tls => f.write_str("tls error")?,
Kind::Transient(ref code) => {
write!(f, "transient error ({})", code)?;
write!(f, "transient error ({code})")?;
}
Kind::Permanent(ref code) => {
write!(f, "permanent error ({})", code)?;
write!(f, "permanent error ({code})")?;
}
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
write!(f, ": {e}")?;
}
Ok(())

View File

@@ -55,8 +55,8 @@ impl Display for ClientId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::Domain(ref value) => f.write_str(value),
Self::Ipv4(ref value) => write!(f, "[{}]", value),
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value),
Self::Ipv4(ref value) => write!(f, "[{value}]"),
Self::Ipv6(ref value) => write!(f, "[IPv6:{value}]"),
}
}
}
@@ -97,7 +97,7 @@ impl Display for Extension {
Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
Extension::StartTls => f.write_str("STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {mechanism}"),
}
}
}
@@ -119,7 +119,7 @@ pub struct ServerInfo {
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let features = if self.features.is_empty() {
"no supported features".to_string()
"no supported features".to_owned()
} else {
format!("{:?}", self.features)
};
@@ -174,7 +174,7 @@ impl ServerInfo {
}
Ok(ServerInfo {
name: name.to_string(),
name: name.to_owned(),
features,
})
}
@@ -228,8 +228,8 @@ pub enum MailParameter {
impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
MailParameter::Size(size) => write!(f, "SIZE={}", size),
MailParameter::Body(ref value) => write!(f, "BODY={value}"),
MailParameter::Size(size) => write!(f, "SIZE={size}"),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other {
ref keyword,
@@ -304,21 +304,21 @@ mod test {
#[test]
fn test_clientid_fmt() {
assert_eq!(
format!("{}", ClientId::Domain("test".to_string())),
"test".to_string()
format!("{}", ClientId::Domain("test".to_owned())),
"test".to_owned()
);
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string());
assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_owned());
}
#[test]
fn test_extension_fmt() {
assert_eq!(
format!("{}", Extension::EightBitMime),
"8BITMIME".to_string()
"8BITMIME".to_owned()
);
assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string()
"AUTH PLAIN".to_owned()
);
}
@@ -331,11 +331,11 @@ mod test {
format!(
"{}",
ServerInfo {
name: "name".to_string(),
name: "name".to_owned(),
features: eightbitmime,
}
),
"name with {EightBitMime}".to_string()
"name with {EightBitMime}".to_owned()
);
let empty = HashSet::new();
@@ -344,11 +344,11 @@ mod test {
format!(
"{}",
ServerInfo {
name: "name".to_string(),
name: "name".to_owned(),
features: empty,
}
),
"name with no supported features".to_string()
"name with no supported features".to_owned()
);
let mut plain = HashSet::new();
@@ -358,11 +358,11 @@ mod test {
format!(
"{}",
ServerInfo {
name: "name".to_string(),
name: "name".to_owned(),
features: plain,
}
),
"name with {Authentication(Plain)}".to_string()
"name with {Authentication(Plain)}".to_owned()
);
}
@@ -374,18 +374,14 @@ mod test {
Category::Unspecified4,
Detail::One,
),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
);
let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo {
name: "me".to_string(),
name: "me".to_owned(),
features,
};
@@ -401,10 +397,10 @@ mod test {
Detail::One,
),
vec![
"me".to_string(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"me".to_owned(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
"8BITMIME".to_owned(),
"SIZE 42".to_owned(),
],
);
@@ -414,7 +410,7 @@ mod test {
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo {
name: "me".to_string(),
name: "me".to_owned(),
features: features2,
};

View File

@@ -77,8 +77,8 @@
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication
//! .credentials(Credentials::new(
//! "username".to_string(),
//! "password".to_string(),
//! "username".to_owned(),
//! "password".to_owned(),
//! ))
//! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain])
@@ -111,7 +111,7 @@
//! .body(String::from("Be happy!"))?;
//!
//! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_string())
//! let tls = TlsParameters::builder("smtp.example.com".to_owned())
//! .dangerous_accept_invalid_certs(true)
//! .build()?;
//!
@@ -200,7 +200,7 @@ struct SmtpInfo {
impl Default for SmtpInfo {
fn default() -> Self {
Self {
server: "localhost".to_string(),
server: "localhost".to_owned(),
port: SMTP_PORT,
hello_name: ClientId::default(),
credentials: None,

View File

@@ -203,7 +203,7 @@ impl<E: Executor> Debug for Pool<E> {
&match self.connections.try_lock() {
Some(connections) => format!("{} connections", connections.len()),
None => "LOCKED".to_string(),
None => "LOCKED".to_owned(),
},
)
.field("client", &self.client)

View File

@@ -186,8 +186,8 @@ impl Debug for Pool {
&match self.connections.try_lock() {
Ok(connections) => format!("{} connections", connections.len()),
Err(TryLockError::WouldBlock) => "LOCKED".to_string(),
Err(TryLockError::Poisoned(_)) => "POISONED".to_string(),
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
},
)
.field("client", &self.client)

View File

@@ -151,7 +151,7 @@ impl FromStr for Response {
fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s)
.map(|(_, r)| r)
.map_err(|e| error::response(e.to_string()))
.map_err(|e| error::response(e.to_owned()))
}
}
@@ -329,10 +329,10 @@ mod test {
detail: Detail::Zero,
},
message: vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"AUTH PLAIN CRAM-MD5".to_string(),
"me".to_owned(),
"8BITMIME".to_owned(),
"SIZE 42".to_owned(),
"AUTH PLAIN CRAM-MD5".to_owned(),
],
}
);
@@ -352,11 +352,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
)
.is_positive());
assert!(!Response::new(
@@ -365,11 +361,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
)
.is_positive());
}
@@ -382,11 +374,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
)
.has_code(451));
assert!(!Response::new(
@@ -395,11 +383,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
)
.has_code(251));
}
@@ -413,11 +397,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
)
.first_word(),
Some("me")
@@ -430,9 +410,9 @@ mod test {
detail: Detail::One,
},
vec![
"me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"me mo".to_owned(),
"8BITMIME".to_owned(),
"SIZE 42".to_owned(),
],
)
.first_word(),
@@ -457,7 +437,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
vec![" ".to_owned()],
)
.first_word(),
None
@@ -469,7 +449,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
vec![" ".to_owned()],
)
.first_word(),
None
@@ -481,7 +461,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec!["".to_string()],
vec!["".to_owned()],
)
.first_word(),
None
@@ -494,7 +474,7 @@ mod test {
let res = parse_response(raw_response);
match res {
Err(nom::Err::Incomplete(_)) => {}
_ => panic!("Expected incomplete response, got {:?}", res),
_ => panic!("Expected incomplete response, got {res:?}"),
}
}
@@ -507,11 +487,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
)
.first_line(),
Some("me")
@@ -524,9 +500,9 @@ mod test {
detail: Detail::One,
},
vec![
"me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"me mo".to_owned(),
"8BITMIME".to_owned(),
"SIZE 42".to_owned(),
],
)
.first_line(),
@@ -551,7 +527,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
vec![" ".to_owned()],
)
.first_line(),
Some(" ")
@@ -563,7 +539,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
vec![" ".to_owned()],
)
.first_line(),
Some(" ")
@@ -575,7 +551,7 @@ mod test {
category: Category::MailSystem,
detail: Detail::One,
},
vec!["".to_string()],
vec!["".to_owned()],
)
.first_line(),
Some("")

View File

@@ -41,7 +41,7 @@ mod tests {
]
.iter()
{
assert_eq!(format!("{}", XText(input)), (*expect).to_string());
assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
}
}
}

View File

@@ -33,7 +33,7 @@ mod sync {
let result = sender.send(&email);
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();
assert_eq!(
@@ -68,10 +68,10 @@ mod sync {
let result = sender.send(&email);
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 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();
assert_eq!(
@@ -131,7 +131,7 @@ mod tokio_1 {
let result = sender.send(email).await;
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();
assert_eq!(
@@ -182,7 +182,7 @@ mod asyncstd_1 {
let result = sender.send(email).await;
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();
assert_eq!(

View File

@@ -15,7 +15,7 @@ mod sync {
.unwrap();
let result = sender.send(&email);
println!("{:?}", result);
println!("{result:?}");
assert!(result.is_ok());
}
}
@@ -42,7 +42,7 @@ mod tokio_1 {
.unwrap();
let result = sender.send(email).await;
println!("{:?}", result);
println!("{result:?}");
assert!(result.is_ok());
}
}
@@ -68,7 +68,7 @@ mod asyncstd_1 {
.unwrap();
let result = sender.send(email).await;
println!("{:?}", result);
println!("{result:?}");
assert!(result.is_ok());
}
}