Skip to main content

smtp_proto/
lib.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7#![doc = include_str!("../README.md")]
8#![deny(rust_2018_idioms)]
9
10use std::borrow::Cow;
11use std::fmt::Display;
12
13pub mod request;
14pub mod response;
15mod tokens;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[cfg_attr(
20    feature = "rkyv",
21    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
22)]
23pub enum Request<T> {
24    Ehlo { host: T },
25    Lhlo { host: T },
26    Helo { host: T },
27    Mail { from: MailFrom<T> },
28    Rcpt { to: RcptTo<T> },
29    Bdat { chunk_size: usize, is_last: bool },
30    Auth { mechanism: u64, initial_response: T },
31    Noop { value: T },
32    Vrfy { value: T },
33    Expn { value: T },
34    Help { value: T },
35    Etrn { name: T },
36    Atrn { domains: Vec<T> },
37    Burl { uri: T, is_last: bool },
38    StartTls,
39    Data,
40    Rset,
41    Quit,
42}
43
44impl<T> Request<T> {
45    /// Returns the SMTP command name for this request variant.
46    pub fn name(&self) -> &'static str {
47        match self {
48            Request::Ehlo { .. } => "EHLO",
49            Request::Helo { .. } => "HELO",
50            Request::Lhlo { .. } => "LHLO",
51            Request::Mail { .. } => "MAIL",
52            Request::Rcpt { .. } => "RCPT",
53            Request::Data => "DATA",
54            Request::Bdat { .. } => "BDAT",
55            Request::Auth { .. } => "AUTH",
56            Request::Noop { .. } => "NOOP",
57            Request::Vrfy { .. } => "VRFY",
58            Request::Expn { .. } => "EXPN",
59            Request::Help { .. } => "HELP",
60            Request::Etrn { .. } => "ETRN",
61            Request::Atrn { .. } => "ATRN",
62            Request::Burl { .. } => "BURL",
63            Request::StartTls => "STARTTLS",
64            Request::Rset => "RSET",
65            Request::Quit => "QUIT",
66        }
67    }
68}
69
70impl Request<Cow<'_, str>> {
71    pub fn into_owned(self) -> Request<String> {
72        match self {
73            Request::Ehlo { host } => Request::Ehlo {
74                host: host.into_owned(),
75            },
76            Request::Lhlo { host } => Request::Lhlo {
77                host: host.into_owned(),
78            },
79            Request::Helo { host } => Request::Helo {
80                host: host.into_owned(),
81            },
82            Request::Mail { from } => Request::Mail {
83                from: from.into_owned(),
84            },
85            Request::Rcpt { to } => Request::Rcpt {
86                to: to.into_owned(),
87            },
88            Request::Bdat {
89                chunk_size,
90                is_last,
91            } => Request::Bdat {
92                chunk_size,
93                is_last,
94            },
95            Request::Auth {
96                mechanism,
97                initial_response,
98            } => Request::Auth {
99                mechanism,
100                initial_response: initial_response.into_owned(),
101            },
102            Request::Noop { value } => Request::Noop {
103                value: value.into_owned(),
104            },
105            Request::Vrfy { value } => Request::Vrfy {
106                value: value.into_owned(),
107            },
108            Request::Expn { value } => Request::Expn {
109                value: value.into_owned(),
110            },
111            Request::Help { value } => Request::Help {
112                value: value.into_owned(),
113            },
114            Request::Etrn { name } => Request::Etrn {
115                name: name.into_owned(),
116            },
117            Request::Atrn { domains } => Request::Atrn {
118                domains: domains.into_iter().map(Cow::into_owned).collect(),
119            },
120            Request::Burl { uri, is_last } => Request::Burl {
121                uri: uri.into_owned(),
122                is_last,
123            },
124            Request::StartTls => Request::StartTls,
125            Request::Data => Request::Data,
126            Request::Rset => Request::Rset,
127            Request::Quit => Request::Quit,
128        }
129    }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134#[cfg_attr(
135    feature = "rkyv",
136    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
137)]
138pub struct MailFrom<T> {
139    pub address: T,
140    pub flags: u64,
141    pub size: usize,
142    pub trans_id: Option<T>,
143    pub by: i64,
144    pub env_id: Option<T>,
145    pub solicit: Option<T>,
146    pub mtrk: Option<Mtrk<T>>,
147    pub auth: Option<T>,
148    pub hold_for: u64,
149    pub hold_until: u64,
150    pub mt_priority: i64,
151}
152
153impl MailFrom<Cow<'_, str>> {
154    pub fn into_owned(self) -> MailFrom<String> {
155        MailFrom {
156            address: self.address.into_owned(),
157            flags: self.flags,
158            size: self.size,
159            trans_id: self.trans_id.map(Cow::into_owned),
160            by: self.by,
161            env_id: self.env_id.map(Cow::into_owned),
162            solicit: self.solicit.map(Cow::into_owned),
163            mtrk: self.mtrk.map(Mtrk::into_owned),
164            auth: self.auth.map(Cow::into_owned),
165            hold_for: self.hold_for,
166            hold_until: self.hold_until,
167            mt_priority: self.mt_priority,
168        }
169    }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174#[cfg_attr(
175    feature = "rkyv",
176    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
177)]
178pub struct RcptTo<T> {
179    pub address: T,
180    pub orcpt: Option<T>,
181    pub rrvs: i64,
182    pub flags: u64,
183}
184
185impl RcptTo<Cow<'_, str>> {
186    pub fn into_owned(self) -> RcptTo<String> {
187        RcptTo {
188            address: self.address.into_owned(),
189            orcpt: self.orcpt.map(Cow::into_owned),
190            rrvs: self.rrvs,
191            flags: self.flags,
192        }
193    }
194}
195
196pub const MAIL_BODY_7BIT: u64 = 1 << 0;
197pub const MAIL_BODY_8BITMIME: u64 = 1 << 1;
198pub const MAIL_BODY_BINARYMIME: u64 = 1 << 2;
199pub const MAIL_RET_FULL: u64 = 1 << 3;
200pub const MAIL_RET_HDRS: u64 = 1 << 4;
201pub const MAIL_SMTPUTF8: u64 = 1 << 5;
202pub const MAIL_REQUIRETLS: u64 = 1 << 6;
203pub const MAIL_CONPERM: u64 = 1 << 7;
204pub const MAIL_BY_NOTIFY: u64 = 1 << 8;
205pub const MAIL_BY_RETURN: u64 = 1 << 9;
206pub const MAIL_BY_TRACE: u64 = 1 << 10;
207
208pub const RCPT_NOTIFY_SUCCESS: u64 = 1 << 0;
209pub const RCPT_NOTIFY_FAILURE: u64 = 1 << 1;
210pub const RCPT_NOTIFY_DELAY: u64 = 1 << 2;
211pub const RCPT_NOTIFY_NEVER: u64 = 1 << 3;
212pub const RCPT_CONNEG: u64 = 1 << 4;
213pub const RCPT_RRVS_REJECT: u64 = 1 << 5;
214pub const RCPT_RRVS_CONTINUE: u64 = 1 << 6;
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218#[cfg_attr(
219    feature = "rkyv",
220    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
221)]
222pub struct Mtrk<T> {
223    pub certifier: T,
224    pub timeout: u64,
225}
226
227impl Mtrk<Cow<'_, str>> {
228    pub fn into_owned(self) -> Mtrk<String> {
229        Mtrk {
230            certifier: self.certifier.into_owned(),
231            timeout: self.timeout,
232        }
233    }
234}
235
236pub const AUTH_SCRAM_SHA_256_PLUS: u64 = 1u64 << 0;
237pub const AUTH_SCRAM_SHA_256: u64 = 1u64 << 1;
238pub const AUTH_SCRAM_SHA_1_PLUS: u64 = 1u64 << 2;
239pub const AUTH_SCRAM_SHA_1: u64 = 1u64 << 3;
240pub const AUTH_OAUTHBEARER: u64 = 1u64 << 4;
241pub const AUTH_XOAUTH: u64 = 1u64 << 5;
242pub const AUTH_XOAUTH2: u64 = 1u64 << 6;
243pub const AUTH_9798_M_DSA_SHA1: u64 = 1u64 << 7;
244pub const AUTH_9798_M_ECDSA_SHA1: u64 = 1u64 << 8;
245pub const AUTH_9798_M_RSA_SHA1_ENC: u64 = 1u64 << 9;
246pub const AUTH_9798_U_DSA_SHA1: u64 = 1u64 << 10;
247pub const AUTH_9798_U_ECDSA_SHA1: u64 = 1u64 << 11;
248pub const AUTH_9798_U_RSA_SHA1_ENC: u64 = 1u64 << 12;
249pub const AUTH_EAP_AES128: u64 = 1u64 << 13;
250pub const AUTH_EAP_AES128_PLUS: u64 = 1u64 << 14;
251pub const AUTH_ECDH_X25519_CHALLENGE: u64 = 1u64 << 15;
252pub const AUTH_ECDSA_NIST256P_CHALLENGE: u64 = 1u64 << 16;
253pub const AUTH_EXTERNAL: u64 = 1u64 << 17;
254pub const AUTH_GS2_KRB5: u64 = 1u64 << 18;
255pub const AUTH_GS2_KRB5_PLUS: u64 = 1u64 << 19;
256pub const AUTH_GSS_SPNEGO: u64 = 1u64 << 20;
257pub const AUTH_GSSAPI: u64 = 1u64 << 21;
258pub const AUTH_KERBEROS_V4: u64 = 1u64 << 22;
259pub const AUTH_KERBEROS_V5: u64 = 1u64 << 23;
260pub const AUTH_NMAS_SAMBA_AUTH: u64 = 1u64 << 24;
261pub const AUTH_NMAS_AUTHEN: u64 = 1u64 << 25;
262pub const AUTH_NMAS_LOGIN: u64 = 1u64 << 26;
263pub const AUTH_NTLM: u64 = 1u64 << 27;
264pub const AUTH_OAUTH10A: u64 = 1u64 << 28;
265pub const AUTH_OPENID20: u64 = 1u64 << 29;
266pub const AUTH_OTP: u64 = 1u64 << 30;
267pub const AUTH_SAML20: u64 = 1u64 << 31;
268pub const AUTH_SECURID: u64 = 1u64 << 32;
269pub const AUTH_SKEY: u64 = 1u64 << 33;
270pub const AUTH_SPNEGO: u64 = 1u64 << 34;
271pub const AUTH_SPNEGO_PLUS: u64 = 1u64 << 35;
272pub const AUTH_SXOVER_PLUS: u64 = 1u64 << 36;
273pub const AUTH_CRAM_MD5: u64 = 1u64 << 37;
274pub const AUTH_DIGEST_MD5: u64 = 1u64 << 38;
275pub const AUTH_LOGIN: u64 = 1u64 << 39;
276pub const AUTH_PLAIN: u64 = 1u64 << 40;
277pub const AUTH_ANONYMOUS: u64 = 1u64 << 41;
278
279pub const EXT_8BIT_MIME: u32 = 1 << 0;
280pub const EXT_ATRN: u32 = 1 << 1;
281pub const EXT_AUTH: u32 = 1 << 2;
282pub const EXT_BINARY_MIME: u32 = 1 << 3;
283pub const EXT_BURL: u32 = 1 << 4;
284pub const EXT_CHECKPOINT: u32 = 1 << 5;
285pub const EXT_CHUNKING: u32 = 1 << 6;
286pub const EXT_CONNEG: u32 = 1 << 7;
287pub const EXT_CONPERM: u32 = 1 << 8;
288pub const EXT_DELIVER_BY: u32 = 1 << 9;
289pub const EXT_DSN: u32 = 1 << 10;
290pub const EXT_ENHANCED_STATUS_CODES: u32 = 1 << 11;
291pub const EXT_ETRN: u32 = 1 << 12;
292pub const EXT_FUTURE_RELEASE: u32 = 1 << 13;
293pub const EXT_HELP: u32 = 1 << 14;
294pub const EXT_MT_PRIORITY: u32 = 1 << 15;
295pub const EXT_MTRK: u32 = 1 << 16;
296pub const EXT_NO_SOLICITING: u32 = 1 << 17;
297pub const EXT_ONEX: u32 = 1 << 18;
298pub const EXT_PIPELINING: u32 = 1 << 19;
299pub const EXT_REQUIRE_TLS: u32 = 1 << 20;
300pub const EXT_RRVS: u32 = 1 << 21;
301pub const EXT_SIZE: u32 = 1 << 22;
302pub const EXT_SMTP_UTF8: u32 = 1 << 23;
303pub const EXT_START_TLS: u32 = 1 << 24;
304pub const EXT_VERB: u32 = 1 << 25;
305pub const EXT_EXPN: u32 = 1 << 26;
306pub const EXT_VRFY: u32 = 1 << 27;
307
308#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
309#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
310#[cfg_attr(
311    feature = "rkyv",
312    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
313)]
314pub enum MtPriority {
315    #[default]
316    Mixer,
317    Stanag4406,
318    Nsep,
319}
320
321#[derive(Debug, Default, Clone, PartialEq, Eq)]
322#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
323#[cfg_attr(
324    feature = "rkyv",
325    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
326)]
327pub struct EhloResponse<T: Display> {
328    pub hostname: T,
329    pub capabilities: u32,
330
331    pub auth_mechanisms: u64,
332    pub deliver_by: u64,
333    pub future_release_interval: u64,
334    pub future_release_datetime: u64,
335    pub mt_priority: MtPriority,
336    pub no_soliciting: Option<String>,
337    pub size: usize,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq)]
341#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
342#[cfg_attr(
343    feature = "rkyv",
344    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
345)]
346pub struct Response<T: Display> {
347    pub code: u16,
348    pub esc: [u8; 3],
349    pub message: T,
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354#[cfg_attr(
355    feature = "rkyv",
356    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
357)]
358pub enum Severity {
359    PositiveCompletion = 2,
360    PositiveIntermediate = 3,
361    TransientNegativeCompletion = 4,
362    PermanentNegativeCompletion = 5,
363    Invalid = 0,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
368#[cfg_attr(
369    feature = "rkyv",
370    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
371)]
372pub enum Category {
373    Syntax = 0,
374    Information = 1,
375    Connections = 2,
376    Unspecified3 = 3,
377    Unspecified4 = 4,
378    MailSystem = 5,
379    Invalid = 6,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub enum Error {
384    NeedsMoreData { bytes_left: usize },
385    UnknownCommand,
386    InvalidSenderAddress,
387    InvalidRecipientAddress,
388    SyntaxError { syntax: &'static str },
389    InvalidParameter { param: &'static str },
390    UnsupportedParameter { param: String },
391    ResponseTooLong,
392    InvalidResponse { code: u16 },
393}
394
395pub(crate) const LF: u8 = b'\n';
396pub(crate) const SP: u8 = b' ';
397
398pub trait IntoString: Sized {
399    fn into_string(self) -> String;
400}
401
402impl IntoString for Vec<u8> {
403    fn into_string(self) -> String {
404        String::from_utf8(self)
405            .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
406    }
407}
408
409impl<T: Default> Default for MailFrom<T> {
410    fn default() -> Self {
411        Self {
412            address: Default::default(),
413            flags: Default::default(),
414            size: Default::default(),
415            trans_id: Default::default(),
416            by: Default::default(),
417            env_id: Default::default(),
418            solicit: Default::default(),
419            mtrk: Default::default(),
420            auth: Default::default(),
421            hold_for: Default::default(),
422            hold_until: Default::default(),
423            mt_priority: Default::default(),
424        }
425    }
426}
427
428impl<T: Default> Default for RcptTo<T> {
429    fn default() -> Self {
430        Self {
431            address: Default::default(),
432            orcpt: Default::default(),
433            rrvs: Default::default(),
434            flags: Default::default(),
435        }
436    }
437}
438
439impl<T: Display> AsRef<EhloResponse<T>> for EhloResponse<T> {
440    fn as_ref(&self) -> &EhloResponse<T> {
441        self
442    }
443}
444
445impl Display for Error {
446    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447        match self {
448            Error::NeedsMoreData { bytes_left } => {
449                write!(f, "Needs more data: {} bytes left", bytes_left)
450            }
451            Error::UnknownCommand => write!(f, "Unknown command"),
452            Error::InvalidSenderAddress => write!(f, "Invalid sender address"),
453            Error::InvalidRecipientAddress => write!(f, "Invalid recipient address"),
454            Error::SyntaxError { syntax } => write!(f, "Syntax error: {}", syntax),
455            Error::InvalidParameter { param } => write!(f, "Invalid parameter: {}", param),
456            Error::UnsupportedParameter { param } => {
457                write!(f, "Unsupported parameter: {}", param)
458            }
459            Error::ResponseTooLong => write!(f, "Response too long"),
460            Error::InvalidResponse { code } => write!(f, "Invalid response: {}", code),
461        }
462    }
463}
464
465impl std::error::Error for Error {}