#[inline(always)]
pub(crate) fn detect(data: &[u8]) -> bool {
if data.len() < 5 {
return false;
}
if data.starts_with(b"220 ") || data.starts_with(b"220-") {
return validate_line(data);
}
if is_command(data, b"EHLO")
|| is_command(data, b"HELO")
|| data.starts_with(b"MAIL FROM:")
|| data.starts_with(b"RCPT TO:")
|| is_command(data, b"DATA")
|| is_command(data, b"QUIT")
|| is_command(data, b"STARTTLS")
|| is_command(data, b"VRFY")
|| is_command(data, b"EXPN")
{
return validate_line(data);
}
false
}
#[inline(always)]
fn is_command(data: &[u8], cmd: &[u8]) -> bool {
if !data.starts_with(cmd) {
return false;
}
let len = cmd.len();
if data.len() == len {
return true;
}
let next = data[len];
next == b' ' || next == b'\r' || next == b'\n'
}
#[inline(always)]
fn validate_line(data: &[u8]) -> bool {
let limit = data.len().min(64);
let mut found_newline = false;
for &b in &data[..limit] {
if b == b'\n' {
found_newline = true;
break;
}
if b != b'\r' && b != b'\t' && !(32..=126).contains(&b) {
return false;
}
}
found_newline || data.len() >= 16
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_smtp_greeting() {
assert!(detect(b"220 smtp.example.com ESMTP Postfix\r\n"));
assert!(detect(b"220-smtp.example.com ESMTP Postfix\r\n"));
}
#[test]
fn test_detect_smtp_core_commands() {
assert!(detect(b"EHLO client.example.com\r\n"));
assert!(detect(b"HELO localhost\n"));
assert!(detect(b"MAIL FROM:<user@example.com>\r\n"));
}
#[test]
fn test_detect_smtp_extended_commands() {
assert!(detect(b"RCPT TO:<admin@example.com>\r\n"));
assert!(detect(b"DATA\r\n"));
assert!(detect(b"QUIT\n"));
assert!(detect(b"STARTTLS\r\n"));
assert!(detect(b"VRFY user\r\n"));
}
#[test]
fn test_detect_smtp_partial() {
assert!(detect(b"220 smtp.gmail.com ESMTP"));
assert!(detect(b"EHLO some-long-domain-name"));
}
#[test]
fn test_reject_wrong_prefix() {
assert!(!detect(b"550 Access denied\r\n"));
assert!(!detect(b"DATABASE connection\r\n"));
}
#[test]
fn test_reject_non_ascii() {
let mut data = [0u8; 10];
data[..4].copy_from_slice(b"220 ");
data[4..10].copy_from_slice(&[0xFF, 0x00, 0x12, 0x34, 0x56, 0x78]);
assert!(!detect(&data));
}
#[test]
fn test_reject_binary_protocols() {
assert!(!detect(&[0x16, 0x03, 0x01, 0x00, 0x05]));
assert!(!detect(b"+OK\r\n"));
}
#[test]
fn test_short_data() {
assert!(!detect(b"220"));
assert!(!detect(b"EHLO"));
}
}