mirror of
https://github.com/chatmail/relay.git
synced 2026-09-08 08:23:14 +00:00
Generated
+3
-2
@@ -522,6 +522,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"lru",
|
"lru",
|
||||||
"mailparse",
|
"mailparse",
|
||||||
|
"memchr",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"retainer",
|
"retainer",
|
||||||
"rstest",
|
"rstest",
|
||||||
@@ -1312,9 +1313,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.6"
|
version = "2.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ serini = "0.2.2"
|
|||||||
tokio = { version = "1.52.1", features = ["full"] }
|
tokio = { version = "1.52.1", features = ["full"] }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
mailparse = "0.16.1"
|
mailparse = "0.16.1"
|
||||||
|
memchr = "2.8.0"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
env_logger = "0.11.10"
|
env_logger = "0.11.10"
|
||||||
governor = "0.10.4"
|
governor = "0.10.4"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use crate::utils::{extract_address, log_eml};
|
use crate::utils::{extract_address, log_eml};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use memchr::{Memchr, memmem};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
@@ -12,9 +13,48 @@ pub struct Envelope {
|
|||||||
pub mail_from: String,
|
pub mail_from: String,
|
||||||
pub origin_ip: String,
|
pub origin_ip: String,
|
||||||
pub rcpt_to: Vec<String>,
|
pub rcpt_to: Vec<String>,
|
||||||
|
|
||||||
|
/// Mail data as transmitted over SMTP/LMTP.
|
||||||
|
///
|
||||||
|
/// Described in <https://www.rfc-editor.org/rfc/rfc5321.html#section-2.3.9>.
|
||||||
|
///
|
||||||
|
/// It MUST end with `<CRLF>`, contain no bare `<CR>` or `<LF>`
|
||||||
|
/// and have all `<CRLF>.` sequences escaped with `.` according to
|
||||||
|
/// <https://www.rfc-editor.org/rfc/rfc5321.html#section-4.5.2>.
|
||||||
pub data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if mail data is valid.
|
||||||
|
fn is_valid_data(data: &[u8]) -> bool {
|
||||||
|
// DATA must end with <CRLF>.
|
||||||
|
//
|
||||||
|
// Otherwise it is not possible to reinject it as is into SMTP/LMTP
|
||||||
|
// without adding <CRLF> at the end and modifying the message.
|
||||||
|
if !data.ends_with(b"\r\n") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for bare `<CR>` and `<LF>`.
|
||||||
|
// <https://www.rfc-editor.org/rfc/rfc5321.html#section-2.3.8>
|
||||||
|
for pos in Memchr::new(b'\r', data) {
|
||||||
|
if data.get(pos + 1) != Some(&b'\n') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for pos in Memchr::new(b'\n', data) {
|
||||||
|
if pos == 0 || data.get(pos - 1) != Some(&b'\r') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not allow unescaped `.`.
|
||||||
|
if data.starts_with(b".\r\n") || memmem::find(data, b"\r\n.\r\n").is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Trait defining the SMTP handler interface.
|
/// Trait defining the SMTP handler interface.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait SmtpHandler: Send + Sync {
|
pub trait SmtpHandler: Send + Sync {
|
||||||
@@ -32,6 +72,17 @@ pub trait SmtpHandler: Send + Sync {
|
|||||||
/// Handles the DATA command.
|
/// Handles the DATA command.
|
||||||
async fn handle_data(&self, envelope: &mut Envelope) -> Result<String, String> {
|
async fn handle_data(&self, envelope: &mut Envelope) -> Result<String, String> {
|
||||||
log::debug!("handle_DATA before-queue");
|
log::debug!("handle_DATA before-queue");
|
||||||
|
|
||||||
|
// Check if the DATA is valid
|
||||||
|
// before doing any custom checks.
|
||||||
|
//
|
||||||
|
// We are not going to normalize newlines
|
||||||
|
// and escape the dots in the mail data.
|
||||||
|
// If mail data turned out to be invalid, reject immediately.
|
||||||
|
if !is_valid_data(&envelope.data) {
|
||||||
|
return Err("500 Invalid DATA".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
self.check_data(envelope).await?;
|
self.check_data(envelope).await?;
|
||||||
if envelope.rcpt_to.is_empty() {
|
if envelope.rcpt_to.is_empty() {
|
||||||
log::warn!("Dropping mail; All recipients disabled.");
|
log::warn!("Dropping mail; All recipients disabled.");
|
||||||
@@ -246,3 +297,40 @@ where
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rstest::*;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(b"", false)]
|
||||||
|
#[case(b".", false)]
|
||||||
|
#[case(b"Hello!\n", false)]
|
||||||
|
#[case(b"Hello!\n\r", false)]
|
||||||
|
#[case(b"Hello\nworld!\r\n", false)]
|
||||||
|
#[case(b"Hello!\r\n\n", false)]
|
||||||
|
#[case(b"Hello\r\n.\r\n", false)]
|
||||||
|
#[case(b"Hello!\r\n .\r\n", true)]
|
||||||
|
#[case(b"Hello!\r\n..\r\n", true)]
|
||||||
|
#[case(b"Hello!\r\n", true)]
|
||||||
|
#[case(b"Hello\r\n.world\r\n", true)]
|
||||||
|
#[case(b"Hello!\r\r\n", false)]
|
||||||
|
#[case(b"Hello!\r\r\n\n", false)]
|
||||||
|
#[case(b"Hello\rworld!\r\n", false)]
|
||||||
|
#[case(b"\n", false)]
|
||||||
|
#[case(b"\nHello\r\n", false)]
|
||||||
|
#[case(b"\r", false)]
|
||||||
|
#[case(b".\r\n", false)]
|
||||||
|
#[case(b".\r\nHello\r\n", false)]
|
||||||
|
#[case(b"..\r\n.\r\n", false)]
|
||||||
|
#[case(b".\r\n..\r\n", false)]
|
||||||
|
#[case(b"\r\n.\r\n", false)]
|
||||||
|
#[case(b"..\r\n..\r\n", true)]
|
||||||
|
#[case(b" .\r\n", true)]
|
||||||
|
#[case(b"..\r\n", true)]
|
||||||
|
#[case(b"\r\n", true)]
|
||||||
|
fn test_is_valid_data(#[case] data: &[u8], #[case] expected: bool) {
|
||||||
|
assert_eq!(is_valid_data(data), expected, "{data:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user