Skip to main content

io_smtp/rfc5321/
helo.rs

1//! SMTP HELO coroutine; legacy handshake. Prefer
2//! [`crate::rfc5321::ehlo`] and fall back to HELO only when the
3//! server rejects EHLO with `500`/`502`.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use std::{
9//!     io::{Read, Write},
10//!     net::TcpStream,
11//! };
12//!
13//! use std::borrow::Cow;
14//!
15//! use io_smtp::{
16//!     coroutine::{SmtpCoroutine, SmtpCoroutineState, SmtpYield},
17//!     rfc5321::{SmtpDomain, helo::SmtpHelo},
18//! };
19//!
20//! // Ready stream needed (TCP-connected, TLS-negociated, greeting consumed)
21//! let mut stream = TcpStream::connect("localhost:25").unwrap();
22//!
23//! let mut buf = [0u8; 4096];
24//!
25//! let domain = SmtpDomain(Cow::Borrowed("example.com"));
26//! let mut coroutine = SmtpHelo::new(domain);
27//! let mut arg = None;
28//!
29//! loop {
30//!     match coroutine.resume(arg.take()) {
31//!         SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
32//!             stream.write_all(&bytes).unwrap();
33//!         }
34//!         SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
35//!             let n = stream.read(&mut buf).unwrap();
36//!             arg = Some(&buf[..n]);
37//!         }
38//!         SmtpCoroutineState::Complete(Ok(())) => break,
39//!         SmtpCoroutineState::Complete(Err(err)) => panic!("{err}"),
40//!     }
41//! }
42//! ```
43
44use core::fmt;
45
46use alloc::{
47    string::{String, ToString},
48    vec::Vec,
49};
50
51use bounded_static::IntoBoundedStatic;
52use log::debug;
53use thiserror::Error;
54
55use crate::{
56    coroutine::*,
57    rfc5321::{SmtpDomain, SmtpReplyCode},
58    send::*,
59    smtp_try,
60};
61
62/// The HELO command (RFC 5321 ยง4.1.1.1).
63pub struct SmtpHeloCommand<'a> {
64    /// The client's domain.
65    pub domain: SmtpDomain<'a>,
66}
67
68impl<'a> From<SmtpHeloCommand<'a>> for Vec<u8> {
69    fn from(cmd: SmtpHeloCommand<'a>) -> Vec<u8> {
70        let mut buf = String::from("HELO ");
71        buf.push_str(cmd.domain.as_ref());
72        buf.push_str("\r\n");
73        buf.into_bytes()
74    }
75}
76
77/// Failure causes during the SMTP HELO exchange.
78#[derive(Clone, Debug, Error)]
79pub enum SmtpHeloError {
80    /// The server rejected the HELO command.
81    #[error("SMTP HELO failed: rejected {code} {message}")]
82    Rejected {
83        /// The reply code.
84        code: u16,
85        /// The reply text.
86        message: String,
87    },
88    /// The underlying command exchange failed.
89    #[error("SMTP HELO failed: {0}")]
90    Send(#[from] SmtpCommandSendError),
91}
92
93/// I/O-free SMTP HELO coroutine.
94pub struct SmtpHelo {
95    state: State,
96}
97
98impl SmtpHelo {
99    /// Creates the coroutine from the client domain sent to the
100    /// server.
101    pub fn new(domain: SmtpDomain<'_>) -> Self {
102        let cmd = SmtpHeloCommand {
103            domain: domain.into_static(),
104        };
105
106        Self {
107            state: State::Send(SmtpCommandSend::new(cmd)),
108        }
109    }
110}
111
112impl SmtpCoroutine for SmtpHelo {
113    type Yield = SmtpYield;
114    type Return = Result<(), SmtpHeloError>;
115
116    fn resume(&mut self, arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
117        match &mut self.state {
118            State::Send(send) => {
119                let out = smtp_try!(send, arg);
120
121                if out.response.code == SmtpReplyCode::OK {
122                    debug!("helo accepted");
123                    return SmtpCoroutineState::Complete(Ok(()));
124                }
125
126                let code = out.response.code.code();
127                let message = out.response.text().to_string();
128                SmtpCoroutineState::Complete(Err(SmtpHeloError::Rejected { code, message }))
129            }
130        }
131    }
132}
133
134enum State {
135    Send(SmtpCommandSend<SmtpHeloCommand<'static>>),
136}
137
138impl fmt::Display for State {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            Self::Send(_) => f.write_str("send helo"),
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use alloc::{borrow::Cow, vec::Vec};
149
150    use crate::{
151        coroutine::*,
152        rfc5321::{SmtpDomain, helo::*},
153        send::SmtpCommandSendError,
154    };
155
156    fn domain() -> SmtpDomain<'static> {
157        SmtpDomain(Cow::Borrowed("example.com"))
158    }
159
160    #[test]
161    fn success_returns_ok() {
162        let mut helo = SmtpHelo::new(domain());
163
164        let bytes = expect_wants_write(&mut helo, None);
165        assert_eq!(bytes, b"HELO example.com\r\n");
166
167        expect_wants_read(&mut helo);
168        expect_complete_ok(&mut helo, b"250 server.example.com\r\n");
169    }
170
171    #[test]
172    fn rejected_returns_rejected_error() {
173        let mut helo = SmtpHelo::new(domain());
174        let _ = expect_wants_write(&mut helo, None);
175        expect_wants_read(&mut helo);
176
177        let err = expect_complete_err(&mut helo, b"550 bad domain\r\n");
178        let SmtpHeloError::Rejected { code, message } = err else {
179            panic!("expected SmtpHeloError::Rejected, got {err:?}");
180        };
181        assert_eq!(code, 550);
182        assert_eq!(message, "bad domain");
183    }
184
185    #[test]
186    fn eof_returns_eof_error() {
187        let mut helo = SmtpHelo::new(domain());
188        let _ = expect_wants_write(&mut helo, None);
189        expect_wants_read(&mut helo);
190
191        let err = expect_complete_err(&mut helo, b"");
192        assert!(matches!(
193            err,
194            SmtpHeloError::Send(SmtpCommandSendError::Eof)
195        ));
196    }
197
198    fn expect_wants_write(cor: &mut SmtpHelo, arg: Option<&[u8]>) -> Vec<u8> {
199        match cor.resume(arg) {
200            SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
201            state => panic!("expected WantsWrite, got {state:?}"),
202        }
203    }
204
205    fn expect_wants_read(cor: &mut SmtpHelo) {
206        match cor.resume(None) {
207            SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
208            state => panic!("expected WantsRead, got {state:?}"),
209        }
210    }
211
212    fn expect_complete_ok(cor: &mut SmtpHelo, reply: &[u8]) {
213        match cor.resume(Some(reply)) {
214            SmtpCoroutineState::Complete(Ok(())) => {}
215            state => panic!("expected Complete(Ok), got {state:?}"),
216        }
217    }
218
219    fn expect_complete_err(cor: &mut SmtpHelo, reply: &[u8]) -> SmtpHeloError {
220        match cor.resume(Some(reply)) {
221            SmtpCoroutineState::Complete(Err(err)) => err,
222            state => panic!("expected Complete(Err), got {state:?}"),
223        }
224    }
225}