#![forbid(unsafe_code)]
#![forbid(missing_docs)]
use std::io;
use std::net::IpAddr;
mod fsm;
mod parser;
pub mod response;
mod smtp;
pub use crate::{
response::{Action, Response},
smtp::{Session, SessionBuilder},
};
pub trait Handler {
fn helo(&mut self, _ip: IpAddr, _domain: &str) -> Response {
response::OK
}
fn mail(&mut self, _ip: IpAddr, _domain: &str, _from: &str) -> Response {
response::OK
}
fn rcpt(&mut self, _to: &str) -> Response {
response::OK
}
fn data_start(
&mut self,
_domain: &str,
_from: &str,
_is8bit: bool,
_to: &[String],
) -> Response {
response::OK
}
fn data(&mut self, _buf: &[u8]) -> io::Result<()> {
Ok(())
}
fn data_end(&mut self) -> Response {
response::OK
}
fn auth_plain(
&mut self,
_authorization_id: &str,
_authentication_id: &str,
_password: &str,
) -> Response {
response::INVALID_CREDENTIALS
}
fn auth_login(&mut self, _username: &str, _password: &str) -> Response {
response::INVALID_CREDENTIALS
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthMechanism {
Plain,
Login,
}
impl AuthMechanism {
fn extension(&self) -> &'static str {
match self {
AuthMechanism::Plain => "PLAIN",
AuthMechanism::Login => "LOGIN",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::response::*;
use std::io::{Cursor, Write};
use std::net::Ipv4Addr;
struct TestHandler {
ip: IpAddr,
domain: String,
from: String,
to: Vec<String>,
is8bit: bool,
expected_data: Vec<u8>,
cursor: Cursor<Vec<u8>>,
helo_called: bool,
mail_called: bool,
rcpt_called: bool,
data_start_called: bool,
data_called: bool,
data_end_called: bool,
}
impl<'a> Handler for &'a mut TestHandler {
fn helo(&mut self, ip: IpAddr, domain: &str) -> Response {
assert_eq!(self.ip, ip);
assert_eq!(self.domain, domain);
self.helo_called = true;
OK
}
fn mail(&mut self, ip: IpAddr, domain: &str, from: &str) -> Response {
assert_eq!(self.ip, ip);
assert_eq!(self.domain, domain);
assert_eq!(self.from, from);
self.mail_called = true;
OK
}
fn rcpt(&mut self, to: &str) -> Response {
let valid_to = self.to.iter().any(|elem| elem == to);
assert!(valid_to, "Invalid to address");
self.rcpt_called = true;
OK
}
fn data_start(
&mut self,
domain: &str,
from: &str,
is8bit: bool,
to: &[String],
) -> Response {
assert_eq!(self.domain, domain);
assert_eq!(self.from, from);
assert_eq!(self.to, to);
assert_eq!(self.is8bit, is8bit);
self.data_start_called = true;
OK
}
fn data(&mut self, buf: &[u8]) -> io::Result<()> {
self.data_called = true;
self.cursor.write(buf).map(|_| ())
}
fn data_end(&mut self) -> Response {
self.data_end_called = true;
let actual_data = self.cursor.get_ref();
assert_eq!(actual_data, &self.expected_data);
OK
}
}
#[test]
fn callbacks() {
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let domain = "some.domain";
let from = "ship@sea.com";
let to = vec!["fish@sea.com".to_owned(), "seaweed@sea.com".to_owned()];
let data = vec![
b"Hello 8bit world \x40\x7f\r\n" as &[u8],
b"Hello again\r\n" as &[u8],
];
let mut expected_data = Vec::with_capacity(2);
for line in data.clone() {
expected_data.extend(line);
}
let mut handler = TestHandler {
ip,
domain: domain.to_owned(),
from: from.to_owned(),
to: to.clone(),
is8bit: true,
expected_data,
cursor: Cursor::new(Vec::with_capacity(80)),
helo_called: false,
mail_called: false,
rcpt_called: false,
data_called: false,
data_start_called: false,
data_end_called: false,
};
let mut session = smtp::SessionBuilder::new("server.domain").build(ip, &mut handler);
let helo = format!("helo {domain}\r\n").into_bytes();
session.process(&helo);
let mail = format!("mail from:<{from}> body=8bitmime\r\n").into_bytes();
session.process(&mail);
let rcpt0 = format!("rcpt to:<{}>\r\n", &to[0]).into_bytes();
let rcpt1 = format!("rcpt to:<{}>\r\n", &to[1]).into_bytes();
session.process(&rcpt0);
session.process(&rcpt1);
session.process(b"data\r\n");
for line in data {
session.process(line);
}
session.process(b".\r\n");
assert!(handler.helo_called);
assert!(handler.mail_called);
assert!(handler.rcpt_called);
assert!(handler.data_called);
}
}