mail_send/smtp/
message.rs

1/*
2 * Copyright Stalwart Labs Ltd.
3 *
4 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
7 * option. This file may not be copied, modified, or distributed
8 * except according to those terms.
9 */
10
11use std::{
12    borrow::Cow,
13    fmt::{Debug, Display},
14};
15
16#[cfg(feature = "builder")]
17use mail_builder::{
18    headers::{address, HeaderType},
19    MessageBuilder,
20};
21#[cfg(feature = "parser")]
22use mail_parser::{HeaderName, HeaderValue};
23use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
24
25use crate::SmtpClient;
26
27#[derive(Debug, Default, Clone)]
28pub struct Message<'x> {
29    pub mail_from: Address<'x>,
30    pub rcpt_to: Vec<Address<'x>>,
31    pub body: Cow<'x, [u8]>,
32}
33
34#[derive(Debug, Default, Clone)]
35pub struct Address<'x> {
36    pub email: Cow<'x, str>,
37    pub parameters: Parameters<'x>,
38}
39
40#[derive(Debug, Default, Clone)]
41pub struct Parameters<'x> {
42    params: Vec<Parameter<'x>>,
43}
44
45#[derive(Debug, Default, Clone)]
46pub struct Parameter<'x> {
47    key: Cow<'x, str>,
48    value: Option<Cow<'x, str>>,
49}
50
51impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> {
52    /// Sends a message to the server.
53    pub async fn send<'x>(&mut self, message: impl IntoMessage<'x>) -> crate::Result<()> {
54        // Send mail-from
55        let message = message.into_message()?;
56        self.mail_from(
57            message.mail_from.email.as_ref(),
58            &message.mail_from.parameters,
59        )
60        .await?;
61
62        // Send rcpt-to
63        for rcpt in &message.rcpt_to {
64            self.rcpt_to(rcpt.email.as_ref(), &rcpt.parameters).await?;
65        }
66
67        // Send message
68        self.data(message.body.as_ref()).await
69    }
70
71    /// Sends a message to the server.
72    #[cfg(feature = "dkim")]
73    pub async fn send_signed<'x, V: mail_auth::common::crypto::SigningKey>(
74        &mut self,
75        message: impl IntoMessage<'x>,
76        signer: &mail_auth::dkim::DkimSigner<V, mail_auth::dkim::Done>,
77    ) -> crate::Result<()> {
78        // Send mail-from
79
80        use mail_auth::common::headers::HeaderWriter;
81        let message = message.into_message()?;
82        self.mail_from(
83            message.mail_from.email.as_ref(),
84            &message.mail_from.parameters,
85        )
86        .await?;
87
88        // Send rcpt-to
89        for rcpt in &message.rcpt_to {
90            self.rcpt_to(rcpt.email.as_ref(), &rcpt.parameters).await?;
91        }
92
93        // Sign message
94        let signature = signer
95            .sign(message.body.as_ref())
96            .map_err(|_| crate::Error::MissingCredentials)?;
97        let mut signed_message = Vec::with_capacity(message.body.len() + 64);
98        signature.write_header(&mut signed_message);
99        signed_message.extend_from_slice(message.body.as_ref());
100
101        // Send message
102        self.data(&signed_message).await
103    }
104
105    pub async fn write_message(&mut self, message: &[u8]) -> tokio::io::Result<()> {
106        // Transparency procedure
107        let mut is_cr_or_lf = false;
108
109        // As per RFC 5322bis, section 2.3:
110        // CR and LF MUST only occur together as CRLF; they MUST NOT appear
111        // independently in the body.
112        // For this reason, we apply the transparency procedure when there is
113        // a CR or LF followed by a dot.
114
115        let mut last_pos = 0;
116        for (pos, byte) in message.iter().enumerate() {
117            if *byte == b'.' && is_cr_or_lf {
118                if let Some(bytes) = message.get(last_pos..pos) {
119                    self.stream.write_all(bytes).await?;
120                    self.stream.write_all(b".").await?;
121                    last_pos = pos;
122                }
123                is_cr_or_lf = false;
124            } else {
125                is_cr_or_lf = *byte == b'\n' || *byte == b'\r';
126            }
127        }
128        if let Some(bytes) = message.get(last_pos..) {
129            self.stream.write_all(bytes).await?;
130        }
131        self.stream.write_all("\r\n.\r\n".as_bytes()).await?;
132        self.stream.flush().await
133    }
134}
135
136impl<'x> Message<'x> {
137    /// Create a new message
138    pub fn new<T, U, V>(from: T, to: U, body: V) -> Self
139    where
140        T: Into<Address<'x>>,
141        U: IntoIterator<Item = T>,
142        V: Into<Cow<'x, [u8]>>,
143    {
144        Message {
145            mail_from: from.into(),
146            rcpt_to: to.into_iter().map(Into::into).collect(),
147            body: body.into(),
148        }
149    }
150
151    /// Create a new empty message.
152    pub fn empty() -> Self {
153        Message {
154            mail_from: Address::default(),
155            rcpt_to: Vec::new(),
156            body: Default::default(),
157        }
158    }
159
160    /// Set the sender of the message.
161    pub fn from(mut self, address: impl Into<Address<'x>>) -> Self {
162        self.mail_from = address.into();
163        self
164    }
165
166    /// Add a message recipient.
167    pub fn to(mut self, address: impl Into<Address<'x>>) -> Self {
168        self.rcpt_to.push(address.into());
169        self
170    }
171
172    /// Set the message body.
173    pub fn body(mut self, body: impl Into<Cow<'x, [u8]>>) -> Self {
174        self.body = body.into();
175        self
176    }
177}
178
179impl<'x> From<&'x str> for Address<'x> {
180    fn from(email: &'x str) -> Self {
181        Address {
182            email: email.into(),
183            parameters: Parameters::default(),
184        }
185    }
186}
187
188impl From<String> for Address<'_> {
189    fn from(email: String) -> Self {
190        Address {
191            email: email.into(),
192            parameters: Parameters::default(),
193        }
194    }
195}
196
197impl<'x> Address<'x> {
198    pub fn new(email: impl Into<Cow<'x, str>>, parameters: Parameters<'x>) -> Self {
199        Address {
200            email: email.into(),
201            parameters,
202        }
203    }
204}
205
206impl<'x> Parameters<'x> {
207    pub fn new() -> Self {
208        Self { params: Vec::new() }
209    }
210
211    pub fn add(&mut self, param: impl Into<Parameter<'x>>) -> &mut Self {
212        self.params.push(param.into());
213        self
214    }
215}
216
217impl<'x> From<&'x str> for Parameter<'x> {
218    fn from(value: &'x str) -> Self {
219        Parameter {
220            key: value.into(),
221            value: None,
222        }
223    }
224}
225
226impl<'x> From<(&'x str, &'x str)> for Parameter<'x> {
227    fn from(value: (&'x str, &'x str)) -> Self {
228        Parameter {
229            key: value.0.into(),
230            value: Some(value.1.into()),
231        }
232    }
233}
234
235impl From<(String, String)> for Parameter<'_> {
236    fn from(value: (String, String)) -> Self {
237        Parameter {
238            key: value.0.into(),
239            value: Some(value.1.into()),
240        }
241    }
242}
243
244impl From<String> for Parameter<'_> {
245    fn from(value: String) -> Self {
246        Parameter {
247            key: value.into(),
248            value: None,
249        }
250    }
251}
252
253impl Display for Parameters<'_> {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        if !self.params.is_empty() {
256            for param in &self.params {
257                f.write_str(" ")?;
258                Display::fmt(&param, f)?;
259            }
260        }
261        Ok(())
262    }
263}
264
265impl Display for Parameter<'_> {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        if let Some(value) = &self.value {
268            write!(f, "{}={}", self.key, value)
269        } else {
270            f.write_str(&self.key)
271        }
272    }
273}
274
275pub trait IntoMessage<'x> {
276    fn into_message(self) -> crate::Result<Message<'x>>;
277}
278
279impl<'x> IntoMessage<'x> for Message<'x> {
280    fn into_message(self) -> crate::Result<Message<'x>> {
281        Ok(self)
282    }
283}
284
285#[cfg(feature = "builder")]
286impl<'x> IntoMessage<'x> for MessageBuilder<'_> {
287    fn into_message(self) -> crate::Result<Message<'x>> {
288        let mut mail_from = None;
289        let mut rcpt_to = std::collections::HashSet::new();
290
291        for (key, value) in self.headers.iter() {
292            if key.eq_ignore_ascii_case("from") {
293                if let HeaderType::Address(address::Address::Address(addr)) = value {
294                    let email = addr.email.trim();
295                    if !email.is_empty() {
296                        mail_from = email.to_string().into();
297                    }
298                }
299            } else if key.eq_ignore_ascii_case("to")
300                || key.eq_ignore_ascii_case("cc")
301                || key.eq_ignore_ascii_case("bcc")
302            {
303                if let HeaderType::Address(addr) = value {
304                    match addr {
305                        address::Address::Address(addr) => {
306                            let email = addr.email.trim();
307                            if !email.is_empty() {
308                                rcpt_to.insert(email.to_string());
309                            }
310                        }
311                        address::Address::Group(group) => {
312                            for addr in &group.addresses {
313                                if let address::Address::Address(addr) = addr {
314                                    let email = addr.email.trim();
315                                    if !email.is_empty() {
316                                        rcpt_to.insert(email.to_string());
317                                    }
318                                }
319                            }
320                        }
321                        address::Address::List(list) => {
322                            for addr in list {
323                                if let address::Address::Address(addr) = addr {
324                                    let email = addr.email.trim();
325                                    if !email.is_empty() {
326                                        rcpt_to.insert(email.to_string());
327                                    }
328                                }
329                            }
330                        }
331                    }
332                }
333            }
334        }
335
336        if rcpt_to.is_empty() {
337            return Err(crate::Error::MissingRcptTo);
338        }
339
340        Ok(Message {
341            mail_from: mail_from.ok_or(crate::Error::MissingMailFrom)?.into(),
342            rcpt_to: rcpt_to
343                .into_iter()
344                .map(|email| Address {
345                    email: email.into(),
346                    parameters: Parameters::default(),
347                })
348                .collect(),
349            body: self.write_to_vec()?.into(),
350        })
351    }
352}
353
354#[cfg(feature = "parser")]
355impl<'x> IntoMessage<'x> for mail_parser::Message<'x> {
356    fn into_message(self) -> crate::Result<Message<'x>> {
357        let mut mail_from = None;
358        let mut rcpt_to = std::collections::HashSet::new();
359
360        let find_address = |addr: &mail_parser::Addr| -> Option<String> {
361            addr.address
362                .as_ref()
363                .filter(|address| !address.trim().is_empty())
364                .map(|address| address.trim().to_string())
365        };
366
367        for header in self.headers() {
368            match &header.name {
369                HeaderName::From => match header.value() {
370                    HeaderValue::Address(mail_parser::Address::List(addrs)) => {
371                        if let Some(email) = addrs.iter().find_map(find_address) {
372                            mail_from = email.to_string().into();
373                        }
374                    }
375                    HeaderValue::Address(mail_parser::Address::Group(groups)) => {
376                        if let Some(grps) = groups.first() {
377                            if let Some(email) = grps.addresses.iter().find_map(find_address) {
378                                mail_from = email.to_string().into();
379                            }
380                        }
381                    }
382                    _ => (),
383                },
384                HeaderName::To | HeaderName::Cc | HeaderName::Bcc => match header.value() {
385                    HeaderValue::Address(mail_parser::Address::List(addrs)) => {
386                        rcpt_to.extend(addrs.iter().filter_map(find_address));
387                    }
388                    HeaderValue::Address(mail_parser::Address::Group(grps)) => {
389                        rcpt_to.extend(
390                            grps.iter()
391                                .flat_map(|grp| grp.addresses.iter())
392                                .filter_map(find_address),
393                        );
394                    }
395                    _ => (),
396                },
397                _ => (),
398            };
399        }
400
401        if rcpt_to.is_empty() {
402            return Err(crate::Error::MissingRcptTo);
403        }
404
405        Ok(Message {
406            mail_from: mail_from.ok_or(crate::Error::MissingMailFrom)?.into(),
407            rcpt_to: rcpt_to
408                .into_iter()
409                .map(|email| Address {
410                    email: email.into(),
411                    parameters: Parameters::default(),
412                })
413                .collect(),
414            body: self.raw_message,
415        })
416    }
417}