Skip to main content

io_smtp/rfc5321/
mail.rs

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