1use core::{fmt, mem};
48
49use alloc::{
50 borrow::Cow,
51 string::{String, ToString},
52 vec::Vec,
53};
54
55use bounded_static::IntoBoundedStatic;
56use log::{debug, trace};
57use thiserror::Error;
58
59use crate::{
60 coroutine::*,
61 rfc5321::{SmtpEhloDomain, SmtpEhloResponse},
62 utils::{escape_byte_string, parsers::format_rich_errors},
63};
64
65pub struct SmtpEhloCommand<'a> {
67 pub domain: SmtpEhloDomain<'a>,
69}
70
71impl<'a> From<SmtpEhloCommand<'a>> for Vec<u8> {
72 fn from(cmd: SmtpEhloCommand<'a>) -> Vec<u8> {
73 let mut buf = String::from("EHLO ");
74 buf.push_str(&cmd.domain.to_string());
75 buf.push_str("\r\n");
76 buf.into_bytes()
77 }
78}
79
80#[derive(Clone, Debug, Error)]
82pub enum SmtpEhloError {
83 #[error("SMTP EHLO failed: reached unexpected EOF on stream")]
85 Eof,
86 #[error("SMTP EHLO failed: parse error: {0}")]
88 ParseResponse(String),
89}
90
91pub struct SmtpEhlo {
93 state: State,
94 wants_write: Option<Vec<u8>>,
95 wants_read: bool,
96 buf: Vec<u8>,
97}
98
99impl SmtpEhlo {
100 pub fn new(domain: SmtpEhloDomain<'_>) -> Self {
103 let bytes = SmtpEhloCommand {
104 domain: domain.into_static(),
105 }
106 .into();
107
108 Self {
109 state: State::Write,
110 wants_write: Some(bytes),
111 wants_read: false,
112 buf: Vec::new(),
113 }
114 }
115}
116
117impl SmtpCoroutine for SmtpEhlo {
118 type Yield = SmtpYield;
119 type Return = Result<Vec<Cow<'static, str>>, SmtpEhloError>;
120
121 fn resume(&mut self, mut arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
122 loop {
123 if let Some(bytes) = self.wants_write.take() {
124 self.state = State::Read;
125 debug!("ehlo sent, awaiting response");
126 return SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes));
127 }
128
129 if mem::take(&mut self.wants_read) {
130 return SmtpCoroutineState::Yielded(SmtpYield::WantsRead);
131 }
132
133 match &mut self.state {
134 State::Write => unreachable!("Write state handled above"),
135 State::Read => match arg.take() {
136 Some(&[]) => {
137 return SmtpCoroutineState::Complete(Err(SmtpEhloError::Eof));
138 }
139 Some(data) => {
140 trace!("read bytes: {}", escape_byte_string(data));
141 self.buf.extend_from_slice(data);
142
143 if !SmtpEhloResponse::is_complete(&self.buf) {
144 self.wants_read = true;
145 continue;
146 }
147
148 self.state = State::Parse;
149 debug!("ehlo response complete, parsing");
150 }
151 None => {
152 self.wants_read = true;
153 }
154 },
155 State::Parse => {
156 return match SmtpEhloResponse::parse(&self.buf) {
157 Ok(response) => {
158 let capabilities = response.into_static().capabilities;
159 let _ = mem::take(&mut self.buf);
160 debug!("ehlo response parsed");
161 trace!("{capabilities:?}");
162 SmtpCoroutineState::Complete(Ok(capabilities))
163 }
164 Err(errors) => {
165 let reason = format_rich_errors(errors);
166 SmtpCoroutineState::Complete(Err(SmtpEhloError::ParseResponse(reason)))
167 }
168 };
169 }
170 }
171 }
172 }
173}
174
175enum State {
176 Write,
177 Read,
178 Parse,
179}
180
181impl fmt::Display for State {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 match self {
184 Self::Write => f.write_str("send ehlo"),
185 Self::Read => f.write_str("read ehlo response"),
186 Self::Parse => f.write_str("parse ehlo response"),
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use alloc::{borrow::Cow, vec::Vec};
194
195 use crate::{
196 coroutine::*,
197 rfc5321::{SmtpDomain, SmtpEhloDomain, ehlo::*},
198 };
199
200 fn ehlo_domain() -> SmtpEhloDomain<'static> {
201 SmtpEhloDomain::SmtpDomain(SmtpDomain(Cow::Borrowed("example.com")))
202 }
203
204 #[test]
205 fn single_line_success_returns_empty_capabilities() {
206 let mut ehlo = SmtpEhlo::new(ehlo_domain());
207
208 let bytes = expect_wants_write(&mut ehlo, None);
209 assert_eq!(bytes, b"EHLO example.com\r\n");
210
211 expect_wants_read(&mut ehlo);
212 let caps = expect_complete_ok(&mut ehlo, b"250 server.example.com\r\n");
213 assert!(caps.is_empty());
214 }
215
216 #[test]
217 fn multi_line_success_returns_capabilities() {
218 let mut ehlo = SmtpEhlo::new(ehlo_domain());
219 let _ = expect_wants_write(&mut ehlo, None);
220 expect_wants_read(&mut ehlo);
221
222 let reply = b"250-server.example.com\r\n250-AUTH PLAIN LOGIN\r\n250 SIZE 10485760\r\n";
223 let caps = expect_complete_ok(&mut ehlo, reply);
224 assert_eq!(caps.len(), 2);
225 assert!(caps.iter().any(|c| c.as_ref() == "AUTH PLAIN LOGIN"));
226 assert!(caps.iter().any(|c| c.as_ref() == "SIZE 10485760"));
227 }
228
229 #[test]
230 fn incomplete_response_re_yields_read() {
231 let mut ehlo = SmtpEhlo::new(ehlo_domain());
232 let _ = expect_wants_write(&mut ehlo, None);
233 expect_wants_read(&mut ehlo);
234
235 match ehlo.resume(Some(b"250-server.example.com\r\n250-AUTH PLAIN")) {
236 SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
237 state => panic!("expected WantsRead, got {state:?}"),
238 }
239 }
240
241 #[test]
242 fn parse_error_returns_parse_error() {
243 let mut ehlo = SmtpEhlo::new(ehlo_domain());
244 let _ = expect_wants_write(&mut ehlo, None);
245 expect_wants_read(&mut ehlo);
246
247 let err = expect_complete_err(&mut ehlo, b"500 syntax error\r\n");
248 assert!(matches!(err, SmtpEhloError::ParseResponse(_)));
249 }
250
251 #[test]
252 fn eof_returns_eof_error() {
253 let mut ehlo = SmtpEhlo::new(ehlo_domain());
254 let _ = expect_wants_write(&mut ehlo, None);
255 expect_wants_read(&mut ehlo);
256
257 let err = expect_complete_err(&mut ehlo, b"");
258 assert!(matches!(err, SmtpEhloError::Eof));
259 }
260
261 fn expect_wants_write(cor: &mut SmtpEhlo, arg: Option<&[u8]>) -> Vec<u8> {
262 match cor.resume(arg) {
263 SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
264 state => panic!("expected WantsWrite, got {state:?}"),
265 }
266 }
267
268 fn expect_wants_read(cor: &mut SmtpEhlo) {
269 match cor.resume(None) {
270 SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
271 state => panic!("expected WantsRead, got {state:?}"),
272 }
273 }
274
275 fn expect_complete_ok(cor: &mut SmtpEhlo, reply: &[u8]) -> Vec<Cow<'static, str>> {
276 match cor.resume(Some(reply)) {
277 SmtpCoroutineState::Complete(Ok(value)) => value,
278 state => panic!("expected Complete(Ok), got {state:?}"),
279 }
280 }
281
282 fn expect_complete_err(cor: &mut SmtpEhlo, reply: &[u8]) -> SmtpEhloError {
283 match cor.resume(Some(reply)) {
284 SmtpCoroutineState::Complete(Err(err)) => err,
285 state => panic!("expected Complete(Err), got {state:?}"),
286 }
287 }
288}