1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use super::{ESMTPCommand, Rfc5321};
use crate::{
common::*,
mail::{StartMailFailure, StartMailResult, Transaction},
smtp::{ApplyCommand, SmtpMail, SmtpSessionCommand, SmtpState},
};
impl SmtpSessionCommand for ESMTPCommand<SmtpMail> {
fn verb(&self) -> &str {
match self.instruction {
SmtpMail::Mail(_, _) => "MAIL",
SmtpMail::Send(_, _) => "SEND",
SmtpMail::Saml(_, _) => "SAML",
SmtpMail::Soml(_, _) => "SOML",
}
}
fn apply(&self, state: SmtpState) -> S2Fut<SmtpState> {
Rfc5321::apply_cmd(&self.instruction, state)
}
}
impl ApplyCommand<SmtpMail> for Rfc5321 {
fn apply_cmd(cmd: &SmtpMail, mut state: SmtpState) -> S2Fut<SmtpState> {
if state.session.peer_name.is_none() {
state.say_command_sequence_fail();
return Box::pin(ready(state));
}
state.reset();
let transaction = Transaction {
mail: Some(cmd.clone()),
..Transaction::default()
};
let fut = async move {
use StartMailResult as R;
match state.service.start_mail(&state.session, transaction).await {
R::Failed(StartMailFailure::TerminateSession, description) => {
state.say_shutdown_err(description);
}
R::Failed(failure, description) => {
state.say_mail_failed(failure, description);
}
R::Accepted(mut transaction) => {
if transaction.id.is_empty() {
fn nunnumber(input: char) -> bool {
!input.is_ascii_digit()
}
let id = format!("{:?}", std::time::Instant::now()).replace(nunnumber, "");
warn!(
"Mail transaction ID is empty. Will use time based ID {}",
id
);
transaction.id = id;
}
state.say_ok_info(format!("Ok! Transaction {} started.", transaction.id));
state.transaction = transaction;
}
};
state
};
Box::pin(fut)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
mail::{Builder, Recipient},
smtp::{CodecControl, SmtpMail, SmtpPath},
};
use futures_await_test::async_test;
#[async_test]
async fn transaction_gets_reset() {
let mut set = SmtpState::new(Builder::default());
set.session.peer_name = Some("xx.io".to_owned());
set.transaction.id = "someid".to_owned();
set.transaction.mail = Some(SmtpMail::Mail(SmtpPath::Null, vec![]));
set.transaction.rcpts.push(Recipient::null());
set.transaction.extra_headers.insert_str(0, "feeeha");
let sut = Rfc5321::command(SmtpMail::Mail(SmtpPath::Postmaster, vec![]));
let mut res = sut.apply(set).await;
match res.writes.pop_front() {
Some(CodecControl::Response(bytes)) if bytes.starts_with(b"250 ") => {}
otherwise => panic!("Expected OK, got {:?}", otherwise),
}
assert_ne!(res.transaction.id, "someid");
assert!(res.transaction.rcpts.is_empty());
assert!(res.transaction.extra_headers.is_empty());
}
#[async_test]
async fn mail_is_set() {
let mut set = SmtpState::new(Builder::default());
set.session.peer_name = Some("xx.io".to_owned());
let sut = Rfc5321::command(SmtpMail::Mail(SmtpPath::Postmaster, vec![]));
let mut res = sut.apply(set).await;
match res.writes.pop_front() {
Some(CodecControl::Response(bytes)) if bytes.starts_with(b"250 ") => {}
otherwise => panic!("Expected OK, got {:?}", otherwise),
}
assert_eq!(
res.transaction.mail,
Some(SmtpMail::Mail(SmtpPath::Postmaster, vec![]))
);
}
#[async_test]
async fn command_sequence_is_enforced() {
let set = SmtpState::new(Builder::default());
let sut = Rfc5321::command(SmtpMail::Mail(SmtpPath::Postmaster, vec![]));
let mut res = sut.apply(set).await;
match res.writes.pop_front() {
Some(CodecControl::Response(bytes)) if bytes.starts_with(b"503 ") => {}
otherwise => panic!("Expected command sequence failure, got {:?}", otherwise),
}
assert_eq!(res.transaction.mail, None);
}
}