Skip to main content

io_smtp/rfc5321/
rcpt.rs

1//! SMTP RCPT TO coroutine; declares one recipient.
2//!
3//! # Example
4//!
5//! ```rust,no_run
6//! use std::{
7//!     borrow::Cow,
8//!     io::{Read, Write},
9//!     net::TcpStream,
10//! };
11//!
12//! use io_smtp::{
13//!     coroutine::{SmtpCoroutine, SmtpCoroutineState, SmtpYield},
14//!     rfc5321::{
15//!         rcpt::SmtpRcpt,
16//!         SmtpDomain, SmtpEhloDomain, SmtpForwardPath,
17//!         SmtpLocalPart, SmtpMailbox,
18//!     },
19//! };
20//!
21//! // Ready stream needed (TCP-connected, TLS-negociated, MAIL FROM accepted)
22//! let mut stream = TcpStream::connect("localhost:25").unwrap();
23//!
24//! let mut buf = [0u8; 4096];
25//!
26//! let forward_path = SmtpForwardPath(SmtpMailbox {
27//!     local_part: SmtpLocalPart(Cow::Borrowed("alice")),
28//!     domain: SmtpEhloDomain::SmtpDomain(SmtpDomain(Cow::Borrowed("example.com"))),
29//! });
30//! let mut coroutine = SmtpRcpt::new(forward_path, Vec::new());
31//! let mut arg = None;
32//!
33//! loop {
34//!     match coroutine.resume(arg.take()) {
35//!         SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
36//!             stream.write_all(&bytes).unwrap();
37//!         }
38//!         SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
39//!             let n = stream.read(&mut buf).unwrap();
40//!             arg = Some(&buf[..n]);
41//!         }
42//!         SmtpCoroutineState::Complete(Ok(())) => break,
43//!         SmtpCoroutineState::Complete(Err(err)) => panic!("{err}"),
44//!     }
45//! }
46//! ```
47
48use core::fmt;
49
50use alloc::{
51    string::{String, ToString},
52    vec::Vec,
53};
54
55use bounded_static::IntoBoundedStatic;
56use log::debug;
57use thiserror::Error;
58
59use crate::{
60    coroutine::*,
61    rfc5321::{SmtpForwardPath, SmtpParameter, SmtpReplyCode},
62    send::*,
63    smtp_try,
64};
65
66/// The RCPT TO command (RFC 5321 ยง4.1.1.3).
67pub struct SmtpRcptCommand<'a> {
68    /// The recipient's forward path.
69    pub forward_path: SmtpForwardPath<'a>,
70    /// Optional ESMTP parameters (e.g. DSN `NOTIFY=`, `ORCPT=`).
71    pub parameters: Vec<SmtpParameter<'a>>,
72}
73
74impl<'a> From<SmtpRcptCommand<'a>> for Vec<u8> {
75    fn from(cmd: SmtpRcptCommand<'a>) -> Vec<u8> {
76        let mut buf = String::from("RCPT TO:");
77        buf.push_str(&cmd.forward_path.to_string());
78        for p in cmd.parameters {
79            buf.push(' ');
80            buf.push_str(&p.to_string());
81        }
82        buf.push_str("\r\n");
83        buf.into_bytes()
84    }
85}
86
87/// Failure causes during the SMTP RCPT TO exchange.
88#[derive(Clone, Debug, Error)]
89pub enum SmtpRcptError {
90    /// The server rejected the RCPT TO command.
91    #[error("SMTP RCPT TO failed: rejected {code} {message}")]
92    Rejected {
93        /// The reply code.
94        code: u16,
95        /// The reply text.
96        message: String,
97    },
98    /// The underlying command exchange failed.
99    #[error("SMTP RCPT TO failed: {0}")]
100    Send(#[from] SmtpCommandSendError),
101}
102
103/// I/O-free SMTP RCPT TO coroutine.
104pub struct SmtpRcpt {
105    state: State,
106}
107
108impl SmtpRcpt {
109    /// Pass an empty `parameters` vector for the bare `RCPT TO`
110    /// form; non-empty entries are appended after the forward path
111    /// (e.g. DSN `NOTIFY=`, `ORCPT=`).
112    pub fn new(forward_path: SmtpForwardPath<'_>, parameters: Vec<SmtpParameter<'_>>) -> Self {
113        let cmd = SmtpRcptCommand {
114            forward_path: forward_path.into_static(),
115            parameters: parameters.into_iter().map(|p| p.into_static()).collect(),
116        };
117
118        Self {
119            state: State::Send(SmtpCommandSend::new(cmd)),
120        }
121    }
122}
123
124impl SmtpCoroutine for SmtpRcpt {
125    type Yield = SmtpYield;
126    type Return = Result<(), SmtpRcptError>;
127
128    fn resume(&mut self, arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
129        match &mut self.state {
130            State::Send(send) => {
131                let out = smtp_try!(send, arg);
132
133                if out.response.code == SmtpReplyCode::OK
134                    || out.response.code == SmtpReplyCode::USER_NOT_LOCAL_WILL_FORWARD
135                {
136                    debug!("rcpt to accepted");
137                    return SmtpCoroutineState::Complete(Ok(()));
138                }
139
140                let code = out.response.code.code();
141                let message = out.response.text().to_string();
142                SmtpCoroutineState::Complete(Err(SmtpRcptError::Rejected { code, message }))
143            }
144        }
145    }
146}
147
148enum State {
149    Send(SmtpCommandSend<SmtpRcptCommand<'static>>),
150}
151
152impl fmt::Display for State {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            Self::Send(_) => f.write_str("send rcpt to"),
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use alloc::{borrow::Cow, vec::Vec};
163
164    use crate::{
165        coroutine::*,
166        rfc5321::{
167            SmtpDomain, SmtpEhloDomain, SmtpForwardPath, SmtpLocalPart, SmtpMailbox, rcpt::*,
168        },
169        send::SmtpCommandSendError,
170    };
171
172    fn forward_path() -> SmtpForwardPath<'static> {
173        SmtpForwardPath(SmtpMailbox {
174            local_part: SmtpLocalPart(Cow::Borrowed("alice")),
175            domain: SmtpEhloDomain::SmtpDomain(SmtpDomain(Cow::Borrowed("example.com"))),
176        })
177    }
178
179    #[test]
180    fn success_returns_ok() {
181        let mut rcpt = SmtpRcpt::new(forward_path(), Vec::new());
182
183        let bytes = expect_wants_write(&mut rcpt, None);
184        assert!(bytes.starts_with(b"RCPT TO:"));
185
186        expect_wants_read(&mut rcpt);
187        expect_complete_ok(&mut rcpt, b"250 recipient ok\r\n");
188    }
189
190    #[test]
191    fn forwarded_returns_ok() {
192        let mut rcpt = SmtpRcpt::new(forward_path(), Vec::new());
193        let _ = expect_wants_write(&mut rcpt, None);
194        expect_wants_read(&mut rcpt);
195
196        expect_complete_ok(&mut rcpt, b"251 user not local, forwarding\r\n");
197    }
198
199    #[test]
200    fn rejected_returns_rejected_error() {
201        let mut rcpt = SmtpRcpt::new(forward_path(), Vec::new());
202        let _ = expect_wants_write(&mut rcpt, None);
203        expect_wants_read(&mut rcpt);
204
205        let err = expect_complete_err(&mut rcpt, b"550 no such user\r\n");
206        let SmtpRcptError::Rejected { code, message } = err else {
207            panic!("expected SmtpRcptError::Rejected, got {err:?}");
208        };
209        assert_eq!(code, 550);
210        assert_eq!(message, "no such user");
211    }
212
213    #[test]
214    fn eof_returns_eof_error() {
215        let mut rcpt = SmtpRcpt::new(forward_path(), Vec::new());
216        let _ = expect_wants_write(&mut rcpt, None);
217        expect_wants_read(&mut rcpt);
218
219        let err = expect_complete_err(&mut rcpt, b"");
220        assert!(matches!(
221            err,
222            SmtpRcptError::Send(SmtpCommandSendError::Eof)
223        ));
224    }
225
226    fn expect_wants_write(cor: &mut SmtpRcpt, arg: Option<&[u8]>) -> Vec<u8> {
227        match cor.resume(arg) {
228            SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
229            state => panic!("expected WantsWrite, got {state:?}"),
230        }
231    }
232
233    fn expect_wants_read(cor: &mut SmtpRcpt) {
234        match cor.resume(None) {
235            SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
236            state => panic!("expected WantsRead, got {state:?}"),
237        }
238    }
239
240    fn expect_complete_ok(cor: &mut SmtpRcpt, reply: &[u8]) {
241        match cor.resume(Some(reply)) {
242            SmtpCoroutineState::Complete(Ok(())) => {}
243            state => panic!("expected Complete(Ok), got {state:?}"),
244        }
245    }
246
247    fn expect_complete_err(cor: &mut SmtpRcpt, reply: &[u8]) -> SmtpRcptError {
248        match cor.resume(Some(reply)) {
249            SmtpCoroutineState::Complete(Err(err)) => err,
250            state => panic!("expected Complete(Err), got {state:?}"),
251        }
252    }
253}