Skip to main content

io_smtp/rfc5321/
quit.rs

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