Skip to content

Commit

Permalink
v0.4.5
Browse files Browse the repository at this point in the history
  • Loading branch information
mdecimus committed Dec 29, 2023
1 parent 2ada376 commit 68114ec
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 29 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
mail-send 0.4.5
================================
- Improved transparency procedure to also escape <CR>.
- Removed `skip-ehlo` feature.

mail-send 0.4.4
================================
- Updated transparency procedure to escape <LF>. as well as <CR><LF>. to prevent SMTP smuggling on vulnerable servers.
Expand Down
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "mail-send"
description = "E-mail delivery library with SMTP and DKIM support"
version = "0.4.4"
version = "0.4.5"
edition = "2021"
authors = [ "Stalwart Labs <[email protected]>"]
license = "Apache-2.0 OR MIT"
Expand Down Expand Up @@ -34,7 +34,6 @@ env_logger = "0.10.0"

[features]
default = ["digest-md5", "cram-md5", "builder", "dkim"]
skip-ehlo = []
builder = ["mail-builder"]
dkim = ["mail-auth"]
digest-md5 = ["md5", "rand"]
Expand Down
16 changes: 8 additions & 8 deletions src/smtp/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,14 @@ impl<U: AsRef<str> + PartialEq + Eq + Hash> AsRef<Credentials<U>> for Credential
}
}

impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidChallenge => write!(f, "Invalid challenge received."),
}
}
}

#[cfg(test)]
mod test {

Expand Down Expand Up @@ -415,11 +423,3 @@ mod test {
);
}
}

impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidChallenge => write!(f, "Invalid challenge received."),
}
}
}
43 changes: 28 additions & 15 deletions src/smtp/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,16 @@ impl<T: AsRef<str> + PartialEq + Eq + Hash> SmtpClientBuilder<T> {
}

/// Connect over TLS
#[allow(unused_mut)]
pub async fn connect(&self) -> crate::Result<SmtpClient<TlsStream<TcpStream>>> {
self.connect_opts(true).await
}

/// Connect over TLS specifying whether to say hello
#[allow(unused_mut)]
pub async fn connect_opts(
&self,
say_hello: bool,
) -> crate::Result<SmtpClient<TlsStream<TcpStream>>> {
tokio::time::timeout(self.timeout, async {
let mut client = SmtpClient {
stream: TcpStream::connect(&self.addr).await?,
Expand Down Expand Up @@ -109,15 +117,13 @@ impl<T: AsRef<str> + PartialEq + Eq + Hash> SmtpClientBuilder<T> {
}
};

#[cfg(not(feature = "skip-ehlo"))]
{
// Authenticate
if let Some(credentials) = &self.credentials {
// Obtain capabilities
let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;

// Authenticate
if let Some(credentials) = &self.credentials {
client.authenticate(&credentials, &capabilities).await?;
}
client.authenticate(&credentials, &capabilities).await?;
} else if say_hello {
client.capabilities(&self.local_host, self.is_lmtp).await?;
}

Ok(client)
Expand All @@ -127,8 +133,16 @@ impl<T: AsRef<str> + PartialEq + Eq + Hash> SmtpClientBuilder<T> {
}

/// Connect over clear text (should not be used)
#[allow(unused_mut)]
pub async fn connect_plain(&self) -> crate::Result<SmtpClient<TcpStream>> {
self.connect_plain_opts(true).await
}

/// Connect over clear text specifying whether to say hello
#[allow(unused_mut)]
pub async fn connect_plain_opts(
&self,
say_hello: bool,
) -> crate::Result<SmtpClient<TcpStream>> {
let mut client = SmtpClient {
stream: tokio::time::timeout(self.timeout, async {
TcpStream::connect(&self.addr).await
Expand All @@ -141,15 +155,14 @@ impl<T: AsRef<str> + PartialEq + Eq + Hash> SmtpClientBuilder<T> {
// Read greeting
client.read().await?.assert_positive_completion()?;

#[cfg(not(feature = "skip-ehlo"))]
{
// Authenticate
if let Some(credentials) = &self.credentials {
// Obtain capabilities
let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;

// Authenticate
if let Some(credentials) = &self.credentials {
client.authenticate(&credentials, &capabilities).await?;
}
client.authenticate(&credentials, &capabilities).await?;
} else if say_hello {
client.capabilities(&self.local_host, self.is_lmtp).await?;
}

Ok(client)
Expand Down
51 changes: 51 additions & 0 deletions src/smtp/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,27 @@ mod test {

#[tokio::test]
async fn transparency_procedure() {
const SMUGGLER: &str = r#"From: Joe SixPack <[email protected]>
To: Suzie Q <[email protected]>
Subject: Is dinner ready?
Hi.
We lost the game. Are you hungry yet?
Joe.
<SEP>.
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
From: Joe SixPack <[email protected]>
To: Suzie Q <[email protected]>
Subject: smuggled message
This is a smuggled message
"#;

for (test, result) in [
(
"A: b\r\n.\r\n".to_string(),
Expand All @@ -244,6 +265,36 @@ mod test {
"A: b\r\n...\r\n\r\n.\r\n".to_string(),
),
("A: ...b".to_string(), "A: ...b\r\n.\r\n".to_string()),
(
"A: \n.\r\nMAIL FROM:<>".to_string(),
"A: \n..\r\nMAIL FROM:<>\r\n.\r\n".to_string(),
),
(
"A: \r.\r\nMAIL FROM:<>".to_string(),
"A: \r..\r\nMAIL FROM:<>\r\n.\r\n".to_string(),
),
(
SMUGGLER
.replace('\r', "")
.replace('\n', "\r\n")
.replace("<SEP>", "\r"),
SMUGGLER
.replace('\r', "")
.replace('\n', "\r\n")
.replace("<SEP>", "\r.")
+ "\r\n.\r\n",
),
(
SMUGGLER
.replace('\r', "")
.replace('\n', "\r\n")
.replace("<SEP>", "\n"),
SMUGGLER
.replace('\r', "")
.replace('\n', "\r\n")
.replace("<SEP>", "\n.")
+ "\r\n.\r\n",
),
] {
let mut client = SmtpClient {
stream: AsyncBufWriter::default(),
Expand Down
10 changes: 6 additions & 4 deletions src/smtp/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,25 @@ impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> {

pub async fn write_message(&mut self, message: &[u8]) -> tokio::io::Result<()> {
// Transparency procedure
let mut is_lf = false;
let mut is_cr_or_lf = false;

// As per RFC 5322bis, section 2.3:
// CR and LF MUST only occur together as CRLF; they MUST NOT appear
// independently in the body.
// For this reason, we apply the transparency procedure when there is
// a CR or LF followed by a dot.

let mut last_pos = 0;
for (pos, byte) in message.iter().enumerate() {
if *byte == b'.' && is_lf {
if *byte == b'.' && is_cr_or_lf {
if let Some(bytes) = message.get(last_pos..pos) {
self.stream.write_all(bytes).await?;
self.stream.write_all(b".").await?;
last_pos = pos;
}
is_lf = false;
is_cr_or_lf = false;
} else {
is_lf = *byte == b'\n';
is_cr_or_lf = *byte == b'\n' || *byte == b'\r';
}
}
if let Some(bytes) = message.get(last_pos..) {
Expand Down

0 comments on commit 68114ec

Please sign in to comment.