Compare commits

..

1 Commits

Author SHA1 Message Date
Paolo Barbolini
c3b5d760f8 wip 2024-10-22 22:49:25 +02:00
23 changed files with 353 additions and 365 deletions

View File

@@ -75,8 +75,8 @@ jobs:
rust: stable
- name: beta
rust: beta
- name: '1.71'
rust: '1.71'
- name: '1.70'
rust: '1.70'
steps:
- name: Checkout

View File

@@ -1,35 +1,3 @@
<a name="v0.11.11"></a>
### v0.11.11 (2024-12-05)
#### Upgrade notes
* MSRV is now 1.71 ([#1008])
#### Bug fixes
* Fix off-by-one error reaching the minimum number of configured pooled connections ([#1012])
#### Misc
* Fix clippy warnings ([#1009])
* Fix `-Zminimal-versions` build ([#1007])
[#1007]: https://github.com/lettre/lettre/pull/1007
[#1008]: https://github.com/lettre/lettre/pull/1008
[#1009]: https://github.com/lettre/lettre/pull/1009
[#1012]: https://github.com/lettre/lettre/pull/1012
<a name="v0.11.10"></a>
### v0.11.10 (2024-10-23)
#### Bug fixes
* Ignore disconnect errors when `pool` feature of SMTP transport is disabled ([#999])
* Use case insensitive comparisons for matching login challenge requests ([#1000])
[#999]: https://github.com/lettre/lettre/pull/999
[#1000]: https://github.com/lettre/lettre/pull/1000
<a name="v0.11.9"></a>
### v0.11.9 (2024-09-13)

462
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.11"
version = "0.11.9"
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.71"
rust-version = "1.70"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -44,7 +44,7 @@ url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }
## tls
native-tls = { version = "0.2.9", optional = true } # feature
native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
rustls-pemfile = { version = "2", optional = true }
rustls-native-certs = { version = "0.8", optional = true }

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.11.11">
<img src="https://deps.rs/crate/lettre/0.11.11/status.svg"
<a href="https://deps.rs/crate/lettre/0.11.9">
<img src="https://deps.rs/crate/lettre/0.11.9/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.71, but this could change at any time either from
the minimum supported Rust version is 1.70, 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.71 or newer.
This library requires Rust 1.70 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml

View File

@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Address {
{
struct FieldVisitor;
impl Visitor<'_> for FieldVisitor {
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {

View File

@@ -6,7 +6,7 @@
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.71 or newer.
//! Lettre requires Rust 1.70 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.11.11")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.9")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]

View File

@@ -1,11 +1,11 @@
use std::{mem, ops::Deref};
use std::{mem, ops::Deref, sync::Arc};
use crate::message::header::ContentTransferEncoding;
/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body that has already been encoded.
#[derive(Debug, Clone)]
pub struct Body {
buf: Vec<u8>,
buf: Arc<[u8]>,
encoding: ContentTransferEncoding,
}
@@ -39,7 +39,7 @@ impl Body {
let encoding = buf.encoding(false);
buf.encode_crlf();
Self::new_impl(buf.into(), encoding)
Self::new_impl(Vec::from(buf).into(), encoding)
}
/// Encode the supplied `buf`, using the provided `encoding`.
@@ -77,7 +77,7 @@ impl Body {
}
buf.encode_crlf();
Ok(Self::new_impl(buf.into(), encoding))
Ok(Self::new_impl(Vec::from(buf).into(), encoding))
}
/// Builds a new `Body` using a pre-encoded buffer.
@@ -87,11 +87,14 @@ impl Body {
/// `buf` shouldn't contain non-ascii characters, lines longer than 1000 characters or nul bytes.
#[inline]
pub fn dangerous_pre_encoded(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
Self { buf, encoding }
Self {
buf: buf.into(),
encoding,
}
}
/// Encodes the supplied `buf` using the provided `encoding`
fn new_impl(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
fn new_impl(buf: Arc<[u8]>, encoding: ContentTransferEncoding) -> Self {
match encoding {
ContentTransferEncoding::SevenBit
| ContentTransferEncoding::EightBit
@@ -133,7 +136,16 @@ impl Body {
/// Consumes `Body` and returns the inner `Vec<u8>`
#[inline]
#[deprecated(
note = "The inner memory is not stored into `Vec<u8>` anymore. Consider using `into_inner`"
)]
pub fn into_vec(self) -> Vec<u8> {
self.buf.to_vec()
}
/// Consumes `Body` and returns the inner `Arc<[u8]>`
#[inline]
pub fn into_inner(self) -> Arc<[u8]> {
self.buf
}
}

View File

@@ -345,6 +345,7 @@ fn dkim_canonicalize_headers<'a>(
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
/// dkim_config
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
}

View File

@@ -119,7 +119,7 @@ mod serde {
{
struct ContentTypeVisitor;
impl Visitor<'_> for ContentTypeVisitor {
impl<'de> Visitor<'de> for ContentTypeVisitor {
type Value = ContentType;
// The error message which states what the Visitor expects to

View File

@@ -18,6 +18,7 @@ pub use self::{
special::*,
textual::*,
};
use super::EmailFormat;
use crate::BoxError;
mod content;
@@ -154,6 +155,19 @@ impl Display for Headers {
}
}
impl EmailFormat for Headers {
fn format<'a>(&'a self, out: &mut impl Extend<Cow<'a, [u8]>>) {
for value in &self.headers {
out.extend([
Cow::Borrowed(value.name.as_bytes()),
Cow::Borrowed(b": "),
Cow::Borrowed(value.encoded_value.as_bytes()),
Cow::Borrowed(b"\r\n"),
]);
}
}
}
/// A possible error when converting a `HeaderName` from another type.
// comes from `http` crate
#[allow(missing_copy_implementations)]

View File

@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Mailbox {
{
struct FieldVisitor;
impl Visitor<'_> for FieldVisitor {
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {

View File

@@ -174,7 +174,7 @@ impl Mailboxes {
self
}
/// Adds a new [`Mailbox`] to the list, in a `Vec::push` style pattern.
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
///
/// # Examples
///

View File

@@ -1,4 +1,4 @@
use std::{io::Write, iter::repeat_with};
use std::{borrow::Cow, iter::repeat_with, sync::Arc};
use mime::Mime;
@@ -28,7 +28,7 @@ impl Part {
}
impl EmailFormat for Part {
fn format(&self, out: &mut Vec<u8>) {
fn format<'a>(&'a self, out: &mut impl Extend<Cow<'a, [u8]>>) {
match self {
Part::Single(part) => part.format(out),
Part::Multi(part) => part.format(out),
@@ -71,7 +71,7 @@ impl SinglePartBuilder {
SinglePart {
headers: self.headers,
body: body.into_vec(),
body: body.into_inner(),
}
}
}
@@ -100,7 +100,7 @@ impl Default for SinglePartBuilder {
#[derive(Debug, Clone)]
pub struct SinglePart {
headers: Headers,
body: Vec<u8>,
body: Arc<[u8]>,
}
impl SinglePart {
@@ -138,24 +138,18 @@ impl SinglePart {
/// Get message content formatted for sending
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
/// Format only the signlepart body
fn format_body(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.body);
out.extend_from_slice(b"\r\n");
self.format_to_vec()
}
}
impl EmailFormat for SinglePart {
fn format(&self, out: &mut Vec<u8>) {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
self.format_body(out);
fn format<'a>(&'a self, out: &mut impl Extend<Cow<'a, [u8]>>) {
self.headers.format(out);
out.extend([
Cow::Borrowed("\r\n".as_bytes()),
Cow::Borrowed(&self.body),
Cow::Borrowed(b"\r\n"),
]);
}
}
@@ -384,33 +378,36 @@ impl MultiPart {
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
self.format_to_vec()
}
/// Format only the multipart body
fn format_body(&self, out: &mut Vec<u8>) {
fn format_body<'a>(&'a self, out: &mut impl Extend<Cow<'a, [u8]>>) {
let boundary = self.boundary();
for part in &self.parts {
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"\r\n");
out.extend([
Cow::Borrowed("--".as_bytes()),
// FIXME: this clone shouldn't exist
Cow::Owned(boundary.clone().into()),
Cow::Borrowed("\r\n".as_bytes()),
]);
part.format(out);
}
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"--\r\n");
out.extend([
Cow::Borrowed("--".as_bytes()),
Cow::Owned(boundary.into()),
Cow::Borrowed("--\r\n".as_bytes()),
]);
}
}
impl EmailFormat for MultiPart {
fn format(&self, out: &mut Vec<u8>) {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
fn format<'a>(&'a self, out: &mut impl Extend<Cow<'a, [u8]>>) {
self.headers.format(out);
out.extend([Cow::Borrowed("\r\n".as_bytes())]);
self.format_body(out);
}
}

View File

@@ -198,7 +198,7 @@
//! ```
//! </details>
use std::{io::Write, iter, time::SystemTime};
use std::{borrow::Cow, iter, sync::Arc, time::SystemTime};
pub use attachment::Attachment;
pub use body::{Body, IntoBody, MaybeString};
@@ -226,7 +226,23 @@ const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
/// Something that can be formatted as an email message
trait EmailFormat {
// Use a writer?
fn format(&self, out: &mut Vec<u8>);
fn format<'a>(&'a self, out: &mut impl Extend<Cow<'a, [u8]>>);
fn format_to_vec(&self) -> Vec<u8> {
struct Formatter(Vec<u8>);
impl<'a> Extend<Cow<'a, [u8]>> for Formatter {
fn extend<T: IntoIterator<Item = Cow<'a, [u8]>>>(&mut self, iter: T) {
for chunk in iter {
self.0.extend_from_slice(&chunk);
}
}
}
let mut formatted = Formatter(Vec::new());
self.format(&mut formatted);
formatted.0
}
}
/// A builder for messages
@@ -345,7 +361,7 @@ impl MessageBuilder {
let hostname = hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|()| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
@@ -454,7 +470,7 @@ impl MessageBuilder {
let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding());
self.build(MessageBody::Raw(body.into_vec()))
self.build(MessageBody::Raw(body.into_inner()))
}
/// Create message using mime body ([`MultiPart`][self::MultiPart])
@@ -489,7 +505,7 @@ pub struct Message {
#[derive(Clone, Debug)]
enum MessageBody {
Mime(Part),
Raw(Vec<u8>),
Raw(Arc<[u8]>),
}
impl Message {
@@ -515,9 +531,7 @@ impl Message {
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
self.format_to_vec()
}
#[cfg(feature = "dkim")]
@@ -593,15 +607,13 @@ impl Message {
}
impl EmailFormat for Message {
fn format(&self, out: &mut Vec<u8>) {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
fn format<'a>(&'a self, out: &mut impl Extend<Cow<'a, [u8]>>) {
self.headers.format(out);
match &self.body {
MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n");
out.extend_from_slice(r)
out.extend([Cow::Borrowed("\r\n".as_bytes()), Cow::Borrowed(r)]);
}
}
}

View File

@@ -45,7 +45,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.abort().await;
conn.quit().await?;
Ok(result)
}

View File

@@ -98,17 +98,13 @@ impl Mechanism {
let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if contains_ignore_ascii_case(
decoded_challenge,
["User Name", "Username:", "Username", "User Name\0"],
) {
if ["User Name", "Username:", "Username", "User Name\0"]
.contains(&decoded_challenge)
{
return Ok(credentials.authentication_identity.clone());
}
if contains_ignore_ascii_case(
decoded_challenge,
["Password", "Password:", "Password\0"],
) {
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
return Ok(credentials.secret.clone());
}
@@ -125,15 +121,6 @@ impl Mechanism {
}
}
fn contains_ignore_ascii_case<'a>(
haystack: &str,
needles: impl IntoIterator<Item = &'a str>,
) -> bool {
needles
.into_iter()
.any(|item| item.eq_ignore_ascii_case(haystack))
}
#[cfg(test)]
mod test {
use super::{Credentials, Mechanism};
@@ -168,23 +155,6 @@ mod test {
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_login_case_insensitive() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
assert_eq!(
mechanism.response(&credentials, Some("username")).unwrap(),
"alice"
);
assert_eq!(
mechanism.response(&credentials, Some("password")).unwrap(),
"wonderland"
);
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;

View File

@@ -119,12 +119,12 @@ impl NetworkStream {
if let Some(timeout) = timeout {
match socket.connect_timeout(&addr.into(), timeout) {
Ok(()) => return Ok(socket.into()),
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
} else {
match socket.connect(&addr.into()) {
Ok(()) => return Ok(socket.into()),
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
}
@@ -369,7 +369,7 @@ impl Write for NetworkStream {
/// If the local address is set, binds the socket to this address.
/// If local address is not set, then destination address is required to determine the default
/// local address on some platforms.
/// See: <https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560>
/// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560
fn bind_local_address(
socket: &socket2::Socket,
dst_addr: &SocketAddr,

View File

@@ -62,7 +62,7 @@ impl TransportBuilder for AsyncSmtpTransportBuilder {
}
}
/// Create a new `SmtpTransportBuilder` or `AsyncSmtpTransportBuilder` from a connection URL
/// Create a new SmtpTransportBuilder or AsyncSmtpTransportBuilder from a connection URL
pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) -> Result<B, Error> {
let connection_url = Url::parse(connection_url).map_err(error::connection)?;
let tls: Option<String> = connection_url

View File

@@ -78,7 +78,7 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")]
let mut created = 0;
for _ in count..(min_idle as usize) {
for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection().await {
Ok(conn) => conn,
Err(err) => {

View File

@@ -72,7 +72,7 @@ impl Pool {
#[cfg(feature = "tracing")]
let mut created = 0;
for _ in count..(min_idle as usize) {
for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection() {
Ok(conn) => conn,
Err(err) => {

View File

@@ -32,7 +32,7 @@ impl Transport for SmtpTransport {
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "pool"))]
conn.abort();
conn.quit()?;
Ok(result)
}

View File

@@ -6,7 +6,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
#[derive(Debug)]
pub struct XText<'a>(pub &'a str);
impl Display for XText<'_> {
impl<'a> Display for XText<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut rest = self.0;
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {