Skip to main content

io_email/message/smtp/
send.rs

1//! SMTP message-send coroutine wrapping
2//! [`io_smtp::message::SmtpMessageSend`]: runs the RFC 5321 mail
3//! transaction (MAIL FROM, RCPT TO, DATA) on an authenticated stream.
4//!
5//! Reverse path is the From: header; forward paths are To:/Cc:/Bcc:.
6//! Override the envelope sender via
7//! [`SmtpClientStd::default_reverse_path`].
8//!
9//! # Example
10//!
11//! ```rust,ignore
12//! use io_email::message::smtp::send::SmtpMessageSend;
13//!
14//! client.run(SmtpMessageSend::new(raw, None)?)?;
15//! ```
16//!
17//! [`SmtpClientStd::default_reverse_path`]: crate::smtp::client::SmtpClientStd::default_reverse_path
18
19use alloc::{
20    borrow::Cow,
21    string::{String, ToString},
22    vec::Vec,
23};
24
25use io_smtp::{
26    coroutine::{SmtpCoroutine, SmtpCoroutineState, SmtpYield},
27    message::{SmtpMessageSend as InnerSend, SmtpMessageSendError as InnerErr},
28    rfc5321::types::{
29        domain::Domain, ehlo_domain::EhloDomain, forward_path::ForwardPath, local_part::LocalPart,
30        mailbox::Mailbox as SmtpMailbox, reverse_path::ReversePath,
31    },
32};
33use log::trace;
34use mail_parser::{Address as MailParserAddress, MessageParser};
35use thiserror::Error;
36
37/// Errors produced by [`SmtpMessageSend`].
38#[derive(Debug, Error)]
39pub enum SmtpMessageSendError {
40    #[error(transparent)]
41    Send(#[from] InnerErr),
42    #[error("could not parse raw RFC 5322 message")]
43    Parse,
44    #[error("no `From:` header found in raw message and no SMTP override set")]
45    MissingReversePath,
46    #[error("no `To:` / `Cc:` / `Bcc:` recipients found in raw message")]
47    MissingForwardPaths,
48    #[error("invalid email address `{0}` in envelope")]
49    InvalidAddress(String),
50}
51
52/// I/O-free coroutine running the RFC 5321 mail transaction over an
53/// already authenticated stream.
54pub struct SmtpMessageSend {
55    inner: InnerSend,
56}
57
58impl SmtpMessageSend {
59    /// `override_reverse_path` (e.g. from
60    /// [`SmtpClientStd::default_reverse_path`]) takes precedence over
61    /// the message's From: header.
62    ///
63    /// [`SmtpClientStd::default_reverse_path`]: crate::smtp::client::SmtpClientStd::default_reverse_path
64    pub fn new(
65        raw: Vec<u8>,
66        override_reverse_path: Option<&str>,
67    ) -> Result<Self, SmtpMessageSendError> {
68        trace!("prepare SMTP message send");
69
70        let parsed = MessageParser::default()
71            .parse_headers(&raw)
72            .ok_or(SmtpMessageSendError::Parse)?;
73
74        let reverse = match override_reverse_path {
75            Some(addr) => parse_smtp_mailbox(addr)?,
76            None => parsed
77                .from()
78                .and_then(first_address)
79                .ok_or(SmtpMessageSendError::MissingReversePath)
80                .and_then(|addr| parse_smtp_mailbox(&addr))?,
81        };
82
83        let mut forwards: Vec<SmtpMailbox<'static>> = Vec::new();
84        for addrs in [parsed.to(), parsed.cc(), parsed.bcc()]
85            .into_iter()
86            .flatten()
87        {
88            for addr in addresses(addrs) {
89                forwards.push(parse_smtp_mailbox(&addr)?);
90            }
91        }
92        if forwards.is_empty() {
93            return Err(SmtpMessageSendError::MissingForwardPaths);
94        }
95
96        let reverse_path = ReversePath::Mailbox(reverse);
97        let forward_paths = forwards.into_iter().map(ForwardPath::from);
98        Ok(Self {
99            inner: InnerSend::new(reverse_path, forward_paths, raw),
100        })
101    }
102}
103
104/// Flattens a mail-parser address group into bare local-part@domain
105/// strings.
106fn addresses(addrs: &MailParserAddress<'_>) -> Vec<String> {
107    addrs
108        .clone()
109        .into_list()
110        .into_iter()
111        .filter_map(|a| {
112            let email = a.address?.into_owned();
113            if email.is_empty() { None } else { Some(email) }
114        })
115        .collect()
116}
117
118/// First address in a group; used to pick the From: envelope sender.
119fn first_address(addrs: &MailParserAddress<'_>) -> Option<String> {
120    addresses(addrs).into_iter().next()
121}
122
123impl SmtpCoroutine for SmtpMessageSend {
124    type Yield = SmtpYield;
125    type Return = Result<(), SmtpMessageSendError>;
126
127    fn resume(&mut self, bytes: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
128        match self.inner.resume(bytes) {
129            SmtpCoroutineState::Yielded(y) => SmtpCoroutineState::Yielded(y),
130            SmtpCoroutineState::Complete(r) => SmtpCoroutineState::Complete(r.map_err(Into::into)),
131        }
132    }
133}
134
135/// Parses local-part@domain into a 'static [`SmtpMailbox`] (owned).
136fn parse_smtp_mailbox(addr: &str) -> Result<SmtpMailbox<'static>, SmtpMessageSendError> {
137    let (local, domain) = addr
138        .rsplit_once('@')
139        .ok_or_else(|| SmtpMessageSendError::InvalidAddress(addr.to_string()))?;
140    if local.is_empty() || domain.is_empty() {
141        return Err(SmtpMessageSendError::InvalidAddress(addr.to_string()));
142    }
143    Ok(SmtpMailbox {
144        local_part: LocalPart(Cow::Owned(local.to_string())),
145        domain: EhloDomain::Domain(Domain(Cow::Owned(domain.to_string()))),
146    })
147}