Skip to main content

io_smtp/rfc5321/
ehlo.rs

1//! SMTP EHLO coroutine; returns the raw capability lines the
2//! server advertises in its multi-line `250` reply.
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! use std::{
8//!     borrow::Cow,
9//!     io::{Read, Write},
10//!     net::TcpStream,
11//! };
12//!
13//! use io_smtp::{
14//!     coroutine::{SmtpCoroutine, SmtpCoroutineState, SmtpYield},
15//!     rfc5321::{
16//!         ehlo::SmtpEhlo,
17//!         SmtpDomain, SmtpEhloDomain,
18//!     },
19//! };
20//!
21//! // Ready stream needed (TCP-connected, TLS-negociated, greeting consumed)
22//! let mut stream = TcpStream::connect("localhost:25").unwrap();
23//!
24//! let mut buf = [0u8; 4096];
25//!
26//! let domain = SmtpEhloDomain::SmtpDomain(SmtpDomain(Cow::Borrowed("example.com")));
27//! let mut coroutine = SmtpEhlo::new(domain);
28//! let mut arg = None;
29//!
30//! let capabilities = loop {
31//!     match coroutine.resume(arg.take()) {
32//!         SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
33//!             stream.write_all(&bytes).unwrap();
34//!         }
35//!         SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
36//!             let n = stream.read(&mut buf).unwrap();
37//!             arg = Some(&buf[..n]);
38//!         }
39//!         SmtpCoroutineState::Complete(Ok(caps)) => break caps,
40//!         SmtpCoroutineState::Complete(Err(err)) => panic!("{err}"),
41//!     }
42//! };
43//!
44//! println!("{capabilities:?}");
45//! ```
46
47use 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
65/// The EHLO command (RFC 5321 ยง4.1.1.1).
66pub struct SmtpEhloCommand<'a> {
67    /// The client's domain or address literal.
68    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/// Failure causes during the SMTP EHLO exchange.
81#[derive(Clone, Debug, Error)]
82pub enum SmtpEhloError {
83    /// The stream reached EOF before a complete reply arrived.
84    #[error("SMTP EHLO failed: reached unexpected EOF on stream")]
85    Eof,
86    /// The reply could not be parsed as an EHLO response.
87    #[error("SMTP EHLO failed: parse error: {0}")]
88    ParseResponse(String),
89}
90
91/// I/O-free SMTP EHLO coroutine.
92pub struct SmtpEhlo {
93    state: State,
94    wants_write: Option<Vec<u8>>,
95    wants_read: bool,
96    buf: Vec<u8>,
97}
98
99impl SmtpEhlo {
100    /// Creates the coroutine from the client identity sent to the
101    /// server.
102    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}