Skip to main content

io_smtp/rfc3207/
starttls.rs

1//! SMTP STARTTLS coroutine; returns any bytes received past the
2//! `220` reply. RFC 3207 ยง6 forbids trailing bytes, so a non-empty
3//! return value is a STARTTLS-injection signal: refuse the upgrade.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use std::{
9//!     io::{Read, Write},
10//!     net::TcpStream,
11//! };
12//!
13//! use io_smtp::{
14//!     coroutine::{SmtpCoroutine, SmtpCoroutineState, SmtpYield},
15//!     rfc3207::starttls::SmtpStartTls,
16//! };
17//!
18//! // Ready stream needed (TCP-connected, plain SMTP, greeting + EHLO done)
19//! let mut stream = TcpStream::connect("localhost:25").unwrap();
20//!
21//! let mut buf = [0u8; 4096];
22//!
23//! let mut coroutine = SmtpStartTls::new();
24//! let mut arg = None;
25//!
26//! let remaining = loop {
27//!     match coroutine.resume(arg.take()) {
28//!         SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
29//!             stream.write_all(&bytes).unwrap();
30//!         }
31//!         SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
32//!             let n = stream.read(&mut buf).unwrap();
33//!             arg = Some(&buf[..n]);
34//!         }
35//!         SmtpCoroutineState::Complete(Ok(remaining)) => break remaining,
36//!         SmtpCoroutineState::Complete(Err(err)) => panic!("{err}"),
37//!     }
38//! };
39//!
40//! assert!(remaining.is_empty(), "STARTTLS-injection: refuse the upgrade");
41//! // Now upgrade `stream` to TLS before sending further SMTP commands.
42//! ```
43
44use core::fmt;
45
46use alloc::{
47    string::{String, ToString},
48    vec::Vec,
49};
50
51use log::debug;
52use thiserror::Error;
53
54use crate::{coroutine::*, rfc5321::SmtpReplyCode, send::*, smtp_try};
55
56/// The STARTTLS command (RFC 3207).
57pub struct SmtpStartTlsCommand;
58
59impl From<SmtpStartTlsCommand> for Vec<u8> {
60    fn from(_: SmtpStartTlsCommand) -> Vec<u8> {
61        b"STARTTLS\r\n".to_vec()
62    }
63}
64
65/// Failure causes during the SMTP STARTTLS handshake.
66#[derive(Clone, Debug, Error)]
67pub enum SmtpStartTlsError {
68    /// The server rejected the STARTTLS command.
69    #[error("SMTP STARTTLS failed: rejected {code} {message}")]
70    Rejected {
71        /// The reply code.
72        code: u16,
73        /// The reply text.
74        message: String,
75    },
76    /// The underlying command exchange failed.
77    #[error("SMTP STARTTLS failed: {0}")]
78    Send(#[from] SmtpCommandSendError),
79}
80
81/// I/O-free SMTP STARTTLS coroutine.
82pub struct SmtpStartTls {
83    state: State,
84}
85
86impl SmtpStartTls {
87    /// Creates the coroutine.
88    pub fn new() -> Self {
89        Self {
90            state: State::Send(SmtpCommandSend::new(SmtpStartTlsCommand)),
91        }
92    }
93}
94
95impl Default for SmtpStartTls {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl SmtpCoroutine for SmtpStartTls {
102    type Yield = SmtpYield;
103    type Return = Result<Vec<u8>, SmtpStartTlsError>;
104
105    fn resume(&mut self, arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
106        match &mut self.state {
107            State::Send(send) => {
108                let out = smtp_try!(send, arg);
109
110                if out.response.code == SmtpReplyCode::SERVICE_READY {
111                    debug!("starttls accepted, ready to upgrade");
112                    return SmtpCoroutineState::Complete(Ok(Vec::new()));
113                }
114
115                let code = out.response.code.code();
116                let message = out.response.text().to_string();
117                SmtpCoroutineState::Complete(Err(SmtpStartTlsError::Rejected { code, message }))
118            }
119        }
120    }
121}
122
123enum State {
124    Send(SmtpCommandSend<SmtpStartTlsCommand>),
125}
126
127impl fmt::Display for State {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            Self::Send(_) => f.write_str("send starttls"),
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use alloc::vec::Vec;
138
139    use crate::{coroutine::*, rfc3207::starttls::*, send::SmtpCommandSendError};
140
141    #[test]
142    fn success_returns_empty_remaining() {
143        let mut starttls = SmtpStartTls::new();
144
145        let bytes = expect_wants_write(&mut starttls, None);
146        assert_eq!(bytes, b"STARTTLS\r\n");
147
148        expect_wants_read(&mut starttls);
149        let remaining = expect_complete_ok(&mut starttls, b"220 ready\r\n");
150        assert!(remaining.is_empty());
151    }
152
153    #[test]
154    fn rejected_returns_rejected_error() {
155        let mut starttls = SmtpStartTls::new();
156        let _ = expect_wants_write(&mut starttls, None);
157        expect_wants_read(&mut starttls);
158
159        let err = expect_complete_err(&mut starttls, b"454 TLS not available\r\n");
160        let SmtpStartTlsError::Rejected { code, message } = err else {
161            panic!("expected SmtpStartTlsError::Rejected, got {err:?}");
162        };
163        assert_eq!(code, 454);
164        assert_eq!(message, "TLS not available");
165    }
166
167    #[test]
168    fn eof_returns_eof_error() {
169        let mut starttls = SmtpStartTls::new();
170        let _ = expect_wants_write(&mut starttls, None);
171        expect_wants_read(&mut starttls);
172
173        let err = expect_complete_err(&mut starttls, b"");
174        assert!(matches!(
175            err,
176            SmtpStartTlsError::Send(SmtpCommandSendError::Eof)
177        ));
178    }
179
180    fn expect_wants_write(cor: &mut SmtpStartTls, arg: Option<&[u8]>) -> Vec<u8> {
181        match cor.resume(arg) {
182            SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
183            state => panic!("expected WantsWrite, got {state:?}"),
184        }
185    }
186
187    fn expect_wants_read(cor: &mut SmtpStartTls) {
188        match cor.resume(None) {
189            SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
190            state => panic!("expected WantsRead, got {state:?}"),
191        }
192    }
193
194    fn expect_complete_ok(cor: &mut SmtpStartTls, reply: &[u8]) -> Vec<u8> {
195        match cor.resume(Some(reply)) {
196            SmtpCoroutineState::Complete(Ok(remaining)) => remaining,
197            state => panic!("expected Complete(Ok), got {state:?}"),
198        }
199    }
200
201    fn expect_complete_err(cor: &mut SmtpStartTls, reply: &[u8]) -> SmtpStartTlsError {
202        match cor.resume(Some(reply)) {
203            SmtpCoroutineState::Complete(Err(err)) => err,
204            state => panic!("expected Complete(Err), got {state:?}"),
205        }
206    }
207}