aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMroik <mroik@delayed.space>2026-04-09 03:26:58 +0200
committerMroik <mroik@delayed.space>2026-04-13 06:56:11 +0200
commit1254bf15b2dee313a9b1c79be779c6b0f6789f1c (patch)
treec28c775582f2d86a5bdc37d5d9b09023f4b07378
parent8d55b7c1c3cac3e6cff00be866f1b48a7300268b (diff)
Add SMTP DATA command for mail input
Forwarding and storing are yet to be implemented, but this commit does implement the handling of the DATA command. Signed-off-by: Mroik <mroik@delayed.space>
-rw-r--r--src/smtp.rs58
1 files changed, 55 insertions, 3 deletions
diff --git a/src/smtp.rs b/src/smtp.rs
index 00e2ae8..30cf490 100644
--- a/src/smtp.rs
+++ b/src/smtp.rs
@@ -32,6 +32,7 @@ impl SmtpServer {
client_host: String::new(),
from: None,
to: Vec::new(),
+ data: String::new(),
};
spawn(session.run(stream));
}
@@ -45,6 +46,7 @@ struct SessionHandler {
client_host: String,
from: Option<String>,
to: Vec<String>,
+ data: String,
}
impl SessionHandler {
@@ -60,11 +62,24 @@ impl SessionHandler {
)?;
loop {
+ buffer.clear();
if r.read_line(&mut buffer)? == 0 {
break;
}
log::debug!("Received '{}' from '{}'", buffer.trim(), self.addr);
+ // In this state the server is not expecting commands as the input is part of the email
+ // message
+ if self.state == SessionState::AwaitingMailInput {
+ if buffer.starts_with(".\r\n") {
+ self.process_mail().await?;
+ continue;
+ }
+
+ self.data.push_str(&buffer);
+ continue;
+ }
+
let command = match Command::try_from(buffer.as_str()) {
Err(_) => {
writer.write_all(Reply::InvalidCommand.to_string().as_bytes())?;
@@ -74,8 +89,6 @@ impl SessionHandler {
};
let res = self.apply(command).await?;
writer.write_all(res.to_string().as_bytes())?;
-
- buffer.clear();
}
log::info!("Connection closed by {}", self.addr);
@@ -87,6 +100,7 @@ impl SessionHandler {
Command::HELO(hostname) => self.helo(hostname).await,
Command::MAIL(from) => self.mail(from).await,
Command::RCPT(to) => self.rcpt(to).await,
+ Command::DATA => self.start_data().await,
}
}
@@ -127,12 +141,43 @@ impl SessionHandler {
self.to.push(String::from(to));
Ok(Reply::Completed(String::from("Ok")))
}
+
+ /// Only after having started a mail transaction
+ async fn start_data(&mut self) -> std::result::Result<Reply, anyhow::Error> {
+ if self.state != SessionState::MailTransaction {
+ return Ok(Reply::BadSequence);
+ }
+
+ // Should I return a more meaningful string with this error code?
+ if self.from.is_none() || self.to.is_empty() {
+ return Ok(Reply::InvalidCommand);
+ }
+
+ self.state = SessionState::AwaitingMailInput;
+
+ Ok(Reply::StartMailInput)
+ }
+
+ // TODO: Forward and store email
+ async fn process_mail(&mut self) -> Result<Reply> {
+ if self.from.is_none() || self.to.is_empty() || self.data.trim().is_empty() {
+ return Ok(Reply::InvalidCommand);
+ }
+
+ self.state = SessionState::Normal;
+ self.from = None;
+ self.to.clear();
+ self.data.clear();
+
+ todo!()
+ }
}
#[derive(PartialEq)]
enum SessionState {
WaitingHelo,
MailTransaction,
+ AwaitingMailInput,
Normal,
}
@@ -145,6 +190,7 @@ impl Default for SessionState {
enum Reply {
Ready(String),
Completed(String),
+ StartMailInput,
InvalidCommand,
InvalidParameter,
BadSequence,
@@ -155,9 +201,10 @@ impl ToString for Reply {
match self {
Reply::Ready(hostname) => format!("220 {}", hostname),
Reply::Completed(hostname) => format!("250 {}", hostname),
- Reply::InvalidCommand => format!("500 Command not recognized"),
+ Reply::InvalidCommand => format!("500 Invalid command"),
Reply::InvalidParameter => format!("501 Parameter error"),
Reply::BadSequence => format!("503 Bad sequence of commands"),
+ Reply::StartMailInput => format!("354 <CRLF>.<CRLF>"),
}
}
}
@@ -166,6 +213,7 @@ enum Command {
HELO(String),
MAIL(String),
RCPT(String),
+ DATA,
}
impl TryFrom<&str> for Command {
@@ -190,6 +238,10 @@ impl TryFrom<&str> for Command {
return Ok(Command::RCPT(String::from(to)));
}
+ if value.to_lowercase().starts_with("data") {
+ return Ok(Command::DATA);
+ }
+
Err(anyhow::format_err!("Invalid command"))
}
}
XMR address: 854DmXNrxULU3ZFJVs4Wc8PFhbq29RhqHhY8W6cdWrtFN3qmooKyyeYPcDzZTNRxphhJ5UzASQfAdEMwSteVqymk28aLhqj