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::trace;
52use thiserror::Error;
53
54use crate::{coroutine::*, rfc5321::types::reply_code::ReplyCode, 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    #[error("SMTP STARTTLS failed: rejected {code} {message}")]
69    Rejected { code: u16, message: String },
70    #[error("SMTP STARTTLS failed: {0}")]
71    Send(#[from] SendSmtpCommandError),
72}
73
74/// I/O-free SMTP STARTTLS coroutine.
75pub struct SmtpStartTls {
76    state: State,
77}
78
79impl SmtpStartTls {
80    pub fn new() -> Self {
81        Self {
82            state: State::Send(SendSmtpCommand::new(SmtpStartTlsCommand)),
83        }
84    }
85}
86
87impl Default for SmtpStartTls {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl SmtpCoroutine for SmtpStartTls {
94    type Yield = SmtpYield;
95    type Return = Result<Vec<u8>, SmtpStartTlsError>;
96
97    fn resume(&mut self, arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
98        loop {
99            trace!("starttls: {}", self.state);
100
101            match &mut self.state {
102                State::Send(send) => {
103                    let out = smtp_try!(send, arg);
104
105                    if out.response.code == ReplyCode::SERVICE_READY {
106                        return SmtpCoroutineState::Complete(Ok(Vec::new()));
107                    }
108
109                    let code = out.response.code.code();
110                    let message = out.response.text().to_string();
111                    return SmtpCoroutineState::Complete(Err(SmtpStartTlsError::Rejected {
112                        code,
113                        message,
114                    }));
115                }
116            }
117        }
118    }
119}
120
121enum State {
122    Send(SendSmtpCommand<SmtpStartTlsCommand>),
123}
124
125impl fmt::Display for State {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self {
128            Self::Send(_) => f.write_str("send starttls"),
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn success_returns_empty_remaining() {
139        let mut starttls = SmtpStartTls::new();
140
141        let bytes = expect_wants_write(&mut starttls, None);
142        assert_eq!(bytes, b"STARTTLS\r\n");
143
144        expect_wants_read(&mut starttls);
145        let remaining = expect_complete_ok(&mut starttls, b"220 ready\r\n");
146        assert!(remaining.is_empty());
147    }
148
149    #[test]
150    fn rejected_returns_rejected_error() {
151        let mut starttls = SmtpStartTls::new();
152        let _ = expect_wants_write(&mut starttls, None);
153        expect_wants_read(&mut starttls);
154
155        let err = expect_complete_err(&mut starttls, b"454 TLS not available\r\n");
156        let SmtpStartTlsError::Rejected { code, message } = err else {
157            panic!("expected SmtpStartTlsError::Rejected, got {err:?}");
158        };
159        assert_eq!(code, 454);
160        assert_eq!(message, "TLS not available");
161    }
162
163    #[test]
164    fn eof_returns_eof_error() {
165        let mut starttls = SmtpStartTls::new();
166        let _ = expect_wants_write(&mut starttls, None);
167        expect_wants_read(&mut starttls);
168
169        let err = expect_complete_err(&mut starttls, b"");
170        assert!(matches!(
171            err,
172            SmtpStartTlsError::Send(SendSmtpCommandError::Eof)
173        ));
174    }
175
176    // --- utils
177
178    fn expect_wants_write(cor: &mut SmtpStartTls, arg: Option<&[u8]>) -> Vec<u8> {
179        match cor.resume(arg) {
180            SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
181            state => panic!("expected WantsWrite, got {state:?}"),
182        }
183    }
184
185    fn expect_wants_read(cor: &mut SmtpStartTls) {
186        match cor.resume(None) {
187            SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
188            state => panic!("expected WantsRead, got {state:?}"),
189        }
190    }
191
192    fn expect_complete_ok(cor: &mut SmtpStartTls, reply: &[u8]) -> Vec<u8> {
193        match cor.resume(Some(reply)) {
194            SmtpCoroutineState::Complete(Ok(remaining)) => remaining,
195            state => panic!("expected Complete(Ok), got {state:?}"),
196        }
197    }
198
199    fn expect_complete_err(cor: &mut SmtpStartTls, reply: &[u8]) -> SmtpStartTlsError {
200        match cor.resume(Some(reply)) {
201            SmtpCoroutineState::Complete(Err(err)) => err,
202            state => panic!("expected Complete(Err), got {state:?}"),
203        }
204    }
205}