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::{mail::SmtpMail, types::reverse_path::ReversePath},
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(ReversePath::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::trace;
49use thiserror::Error;
50
51use crate::{
52    coroutine::*,
53    rfc5321::types::{parameter::Parameter, reply_code::ReplyCode, reverse_path::ReversePath},
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: ReversePath<'a>,
62    /// Optional ESMTP parameters (e.g. `SIZE=`, `BODY=`).
63    pub parameters: Vec<Parameter<'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    #[error("SMTP MAIL FROM failed: rejected {code} {message}")]
83    Rejected { code: u16, message: String },
84    #[error("SMTP MAIL FROM failed: {0}")]
85    Send(#[from] SendSmtpCommandError),
86}
87
88/// I/O-free SMTP MAIL FROM coroutine.
89pub struct SmtpMail {
90    state: State,
91}
92
93impl SmtpMail {
94    /// Pass an empty `parameters` vector for the bare `MAIL FROM`
95    /// form; non-empty entries are appended after the reverse path
96    /// (e.g. `SIZE=`, `BODY=`, DSN).
97    pub fn new(reverse_path: ReversePath<'_>, parameters: Vec<Parameter<'_>>) -> Self {
98        let cmd = SmtpMailCommand {
99            reverse_path: reverse_path.into_static(),
100            parameters: parameters.into_iter().map(|p| p.into_static()).collect(),
101        };
102
103        Self {
104            state: State::Send(SendSmtpCommand::new(cmd)),
105        }
106    }
107}
108
109impl SmtpCoroutine for SmtpMail {
110    type Yield = SmtpYield;
111    type Return = Result<(), SmtpMailError>;
112
113    fn resume(&mut self, arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
114        loop {
115            trace!("mail: {}", self.state);
116
117            match &mut self.state {
118                State::Send(send) => {
119                    let out = smtp_try!(send, arg);
120
121                    if out.response.code == ReplyCode::OK {
122                        return SmtpCoroutineState::Complete(Ok(()));
123                    }
124
125                    let code = out.response.code.code();
126                    let message = out.response.text().to_string();
127                    return SmtpCoroutineState::Complete(Err(SmtpMailError::Rejected {
128                        code,
129                        message,
130                    }));
131                }
132            }
133        }
134    }
135}
136
137enum State {
138    Send(SendSmtpCommand<SmtpMailCommand<'static>>),
139}
140
141impl fmt::Display for State {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        match self {
144            Self::Send(_) => f.write_str("send mail from"),
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    fn null_path() -> ReversePath<'static> {
154        ReversePath::Null
155    }
156
157    #[test]
158    fn success_returns_ok() {
159        let mut mail = SmtpMail::new(null_path(), Vec::new());
160
161        let bytes = expect_wants_write(&mut mail, None);
162        assert!(bytes.starts_with(b"MAIL FROM:"));
163
164        expect_wants_read(&mut mail);
165        expect_complete_ok(&mut mail, b"250 sender ok\r\n");
166    }
167
168    #[test]
169    fn rejected_returns_rejected_error() {
170        let mut mail = SmtpMail::new(null_path(), Vec::new());
171        let _ = expect_wants_write(&mut mail, None);
172        expect_wants_read(&mut mail);
173
174        let err = expect_complete_err(&mut mail, b"550 mailbox unavailable\r\n");
175        let SmtpMailError::Rejected { code, message } = err else {
176            panic!("expected SmtpMailError::Rejected, got {err:?}");
177        };
178        assert_eq!(code, 550);
179        assert_eq!(message, "mailbox unavailable");
180    }
181
182    #[test]
183    fn eof_returns_eof_error() {
184        let mut mail = SmtpMail::new(null_path(), Vec::new());
185        let _ = expect_wants_write(&mut mail, None);
186        expect_wants_read(&mut mail);
187
188        let err = expect_complete_err(&mut mail, b"");
189        assert!(matches!(
190            err,
191            SmtpMailError::Send(SendSmtpCommandError::Eof)
192        ));
193    }
194
195    // --- utils
196
197    fn expect_wants_write(cor: &mut SmtpMail, arg: Option<&[u8]>) -> Vec<u8> {
198        match cor.resume(arg) {
199            SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
200            state => panic!("expected WantsWrite, got {state:?}"),
201        }
202    }
203
204    fn expect_wants_read(cor: &mut SmtpMail) {
205        match cor.resume(None) {
206            SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
207            state => panic!("expected WantsRead, got {state:?}"),
208        }
209    }
210
211    fn expect_complete_ok(cor: &mut SmtpMail, reply: &[u8]) {
212        match cor.resume(Some(reply)) {
213            SmtpCoroutineState::Complete(Ok(())) => {}
214            state => panic!("expected Complete(Ok), got {state:?}"),
215        }
216    }
217
218    fn expect_complete_err(cor: &mut SmtpMail, reply: &[u8]) -> SmtpMailError {
219        match cor.resume(Some(reply)) {
220            SmtpCoroutineState::Complete(Err(err)) => err,
221            state => panic!("expected Complete(Err), got {state:?}"),
222        }
223    }
224}