Skip to main content

daaki_smtp/types/
mod.rs

1//! SMTP protocol types.
2//!
3//! # References
4//! - RFC 5321 (SMTP)
5//! - RFC 2033 (LMTP)
6//! - RFC 2034 (Enhanced Status Codes)
7//! - RFC 4954 (SMTP AUTH)
8
9pub(crate) mod validated;
10
11pub use validated::{
12    AddressLiteral, Domain, DomainOrLiteral, EnvidValue, ForwardPath, Mailbox, ReversePath,
13    ValidationError, XtextSafe,
14};
15
16/// Transport protocol for the connection.
17///
18/// Determines the greeting command and DATA response handling.
19/// SMTP uses EHLO and returns one reply after DATA (RFC 5321 Section 3.1).
20/// LMTP uses LHLO and returns one reply per recipient after DATA (RFC 2033 Section 4.2).
21#[non_exhaustive]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Protocol {
25    /// Standard SMTP (RFC 5321). Uses EHLO; one reply after DATA.
26    Smtp,
27    /// Local Mail Transfer Protocol (RFC 2033). Uses LHLO; one reply per recipient after DATA.
28    Lmtp,
29}
30
31/// Per-recipient delivery result for LMTP (RFC 2033 Section 4.2).
32///
33/// In LMTP, the server sends one response per RCPT TO after the final DATA dot,
34/// rather than a single aggregate response as in SMTP.
35#[non_exhaustive]
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub struct RecipientResult {
39    /// The recipient address (RFC 5321 Section 4.1.2).
40    pub recipient: ForwardPath,
41    /// The server's response for this recipient.
42    pub response: SmtpResponse,
43}
44
45/// A recipient whose RCPT TO command was rejected by the server.
46///
47/// RFC 5321 Section 3.3: when some but not all RCPT TO commands are
48/// rejected, the server accepts the message for the remaining recipients.
49/// This struct preserves the rejection details so callers can report or
50/// retry individual failures.
51#[non_exhaustive]
52#[derive(Debug, Clone, PartialEq, Eq, Hash)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub struct RejectedRecipient {
55    /// The rejected recipient address (RFC 5321 Section 4.1.2).
56    pub recipient: ForwardPath,
57    /// The server's rejection response (4xx or 5xx).
58    pub response: SmtpResponse,
59}
60
61/// Result of a successful SMTP send operation (RFC 5321 Section 3.3).
62///
63/// When the server accepts at least one recipient and the DATA (or BDAT)
64/// transfer succeeds, the message is delivered to the accepted recipients.
65/// Any recipients whose RCPT TO was rejected are listed in
66/// `rejected_recipients` so callers can take appropriate action (e.g. log,
67/// retry, or notify the sender).
68///
69/// If **all** recipients are rejected, the send methods return
70/// [`Error::AllRecipientsFailed`](crate::error::Error::AllRecipientsFailed)
71/// instead of an `Ok(SendResult)`.
72#[non_exhaustive]
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct SendResult {
76    /// Recipients whose RCPT TO command was rejected (RFC 5321 Section 3.3).
77    ///
78    /// Empty when all recipients were accepted.
79    pub rejected_recipients: Vec<RejectedRecipient>,
80}
81
82impl SendResult {
83    /// Returns `true` if all recipients were accepted.
84    pub fn all_accepted(&self) -> bool {
85        self.rejected_recipients.is_empty()
86    }
87
88    /// Returns `true` if some (but not all) recipients were rejected.
89    pub fn has_rejections(&self) -> bool {
90        !self.rejected_recipients.is_empty()
91    }
92}
93
94/// Result of a successful LMTP send operation (RFC 2033 Section 4.2).
95///
96/// Combines per-recipient delivery results (from the server's per-recipient
97/// DATA/BDAT responses) with any recipients rejected during RCPT TO.
98///
99/// LMTP differs from SMTP in that the server sends one response per accepted
100/// recipient after the final DATA dot (RFC 2033 Section 4.2), rather than a
101/// single aggregate response as in SMTP. This struct captures both sets of
102/// information so callers have full visibility into which recipients were
103/// accepted, which were rejected at RCPT TO time, and the delivery status
104/// of each accepted recipient.
105#[non_exhaustive]
106#[derive(Debug, Clone, PartialEq, Eq, Hash)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108pub struct LmtpSendResult {
109    /// Per-recipient delivery results from the server after DATA/BDAT
110    /// (RFC 2033 Section 4.2).
111    ///
112    /// Each entry corresponds to a recipient whose RCPT TO was accepted.
113    /// The response may still indicate a delivery failure (e.g. 452, 550)
114    /// — this is normal in LMTP where each recipient can independently
115    /// succeed or fail during delivery.
116    pub results: Vec<RecipientResult>,
117    /// Recipients whose RCPT TO command was rejected (RFC 5321 Section 3.3).
118    ///
119    /// These recipients never received a per-recipient DATA response because
120    /// they were rejected before the message was transmitted. Empty when all
121    /// recipients were accepted at RCPT TO time.
122    pub rejected_recipients: Vec<RejectedRecipient>,
123}
124
125impl LmtpSendResult {
126    /// Returns `true` if all recipients were accepted at RCPT TO time.
127    pub fn all_accepted(&self) -> bool {
128        self.rejected_recipients.is_empty()
129    }
130
131    /// Returns `true` if some (but not all) recipients were rejected at RCPT TO time.
132    pub fn has_rejections(&self) -> bool {
133        !self.rejected_recipients.is_empty()
134    }
135}
136
137/// A parsed SMTP server response (RFC 5321 Section 4.2).
138///
139/// Multi-line responses are collected into a single `SmtpResponse` with the final
140/// reply code.
141#[non_exhaustive]
142#[derive(Debug, Clone, PartialEq, Eq, Hash)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub struct SmtpResponse {
145    /// Three-digit reply code (e.g. 250, 354, 550).
146    pub code: u16,
147    /// Enhanced status code (RFC 2034), if present.
148    pub enhanced_code: Option<EnhancedStatusCode>,
149    /// Response text lines (one per line in a multi-line response).
150    pub lines: Vec<String>,
151}
152
153impl SmtpResponse {
154    /// Returns `true` if this is a positive completion reply (2xx).
155    pub fn is_success(&self) -> bool {
156        (200..300).contains(&self.code)
157    }
158
159    /// Returns `true` if this is a positive intermediate reply (3xx).
160    pub fn is_intermediate(&self) -> bool {
161        (300..400).contains(&self.code)
162    }
163
164    /// Returns `true` if this is the 354 "Start mail input" response to DATA.
165    ///
166    /// RFC 5321 Section 4.1.1.4: the only valid intermediate response to the
167    /// DATA command is 354. Other 3xx codes are not defined for DATA and must
168    /// not be treated as a go-ahead to send message content.
169    pub fn is_data_ready(&self) -> bool {
170        self.code == 354
171    }
172
173    /// Returns `true` if this is a transient negative reply (4xx).
174    pub fn is_transient_error(&self) -> bool {
175        (400..500).contains(&self.code)
176    }
177
178    /// Returns `true` if this is a permanent negative reply (5xx).
179    pub fn is_permanent_error(&self) -> bool {
180        (500..600).contains(&self.code)
181    }
182
183    /// Join all response lines into a single string, separated by newlines.
184    pub fn text(&self) -> String {
185        self.lines.join("\n")
186    }
187}
188
189impl std::fmt::Display for SmtpResponse {
190    /// Formats the response as `{code} {text}`, joining multi-line responses
191    /// with newlines (RFC 5321 Section 4.2).
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        for (i, line) in self.lines.iter().enumerate() {
194            if i > 0 {
195                f.write_str("\n")?;
196            }
197            write!(f, "{} {}", self.code, line)?;
198        }
199        if self.lines.is_empty() {
200            write!(f, "{}", self.code)?;
201        }
202        Ok(())
203    }
204}
205
206/// Enhanced status code (RFC 1893 Section 2 / RFC 2034 Section 3).
207///
208/// Format: `class.subject.detail` (e.g. `2.1.0` for "success, mailbox address").
209#[non_exhaustive]
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
211#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
212pub struct EnhancedStatusCode {
213    /// Class: 2 (success), 4 (transient), 5 (permanent).
214    pub class: u8,
215    /// Subject component.
216    pub subject: u16,
217    /// Detail component.
218    pub detail: u16,
219}
220
221impl std::fmt::Display for EnhancedStatusCode {
222    /// Formats as `class.subject.detail` per RFC 2034 Section 2.
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        write!(f, "{}.{}.{}", self.class, self.subject, self.detail)
225    }
226}
227
228/// SMTP server extension capabilities, parsed from EHLO response
229/// (RFC 5321 Section 4.1.1.1).
230#[non_exhaustive]
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
233pub enum SmtpExtension {
234    /// `8BITMIME` (RFC 1652).
235    EightBitMime,
236    /// `PIPELINING` (RFC 1854).
237    Pipelining,
238    /// `SIZE [limit]` (RFC 1870).
239    Size(Option<u64>),
240    /// `STARTTLS` (RFC 3207).
241    StartTls,
242    /// `AUTH mechanisms...` (RFC 4954).
243    Auth(Vec<AuthMechanism>),
244    /// `CHUNKING` (RFC 3030).
245    Chunking,
246    /// `BINARYMIME` (RFC 3030).
247    BinaryMime,
248    /// `SMTPUTF8` (RFC 6531).
249    SmtpUtf8,
250    /// `ENHANCEDSTATUSCODES` (RFC 2034).
251    EnhancedStatusCodes,
252    /// Legacy, non-standard `SASL-IR` EHLO keyword seen on some SMTP servers.
253    ///
254    /// RFC 4954 Section 4 already defines `AUTH mechanism [initial-response]`,
255    /// so SMTP does not require or standardize a separate capability for
256    /// initial responses. We still preserve this keyword for compatibility
257    /// and introspection.
258    SaslIr,
259    /// `DSN` (RFC 3461). Delivery Status Notification extension.
260    Dsn,
261    /// `REQUIRETLS` (RFC 8689). Per-message TLS enforcement.
262    RequireTls,
263    /// `FUTURERELEASE` (RFC 4865). Scheduled message delivery.
264    ///
265    /// The server may advertise a maximum hold interval (seconds) and/or
266    /// a maximum hold-until datetime.
267    FutureRelease {
268        /// Maximum hold interval in seconds, if advertised (RFC 4865 Section 4).
269        max_interval: Option<u64>,
270        /// Maximum hold-until datetime string, if advertised (RFC 4865 Section 4).
271        max_datetime: Option<String>,
272    },
273    /// `DELIVERBY` (RFC 2852). Time-bound delivery.
274    ///
275    /// Optional value is the server's minimum delivery time in seconds
276    /// (RFC 2852 Section 2).
277    DeliverBy(Option<u64>),
278    /// `MT-PRIORITY` (RFC 6758). Message priority signaling.
279    MtPriority,
280    /// `VRFY` (RFC 5321 Section 4.1.1.6). Server supports VRFY command.
281    Vrfy,
282    /// `EXPN` (RFC 5321 Section 4.1.1.7). Server supports EXPN command.
283    Expn,
284    /// `NO-SOLICITING` (RFC 3865). Advertising policy extension.
285    ///
286    /// Optional value is a soliciting keyword.
287    NoSoliciting(Option<String>),
288    /// An unrecognized extension — keyword preserved verbatim.
289    Other(String),
290}
291
292impl std::fmt::Display for SmtpExtension {
293    /// Formats as the canonical EHLO keyword (RFC 5321 Section 4.1.1.1).
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        match self {
296            Self::EightBitMime => f.write_str("8BITMIME"),
297            Self::Pipelining => f.write_str("PIPELINING"),
298            Self::Size(Some(n)) => write!(f, "SIZE {n}"),
299            Self::Size(None) => f.write_str("SIZE"),
300            Self::StartTls => f.write_str("STARTTLS"),
301            Self::Auth(mechs) => {
302                f.write_str("AUTH")?;
303                for m in mechs {
304                    write!(f, " {m}")?;
305                }
306                Ok(())
307            }
308            Self::Chunking => f.write_str("CHUNKING"),
309            Self::BinaryMime => f.write_str("BINARYMIME"),
310            Self::SmtpUtf8 => f.write_str("SMTPUTF8"),
311            Self::EnhancedStatusCodes => f.write_str("ENHANCEDSTATUSCODES"),
312            Self::SaslIr => f.write_str("SASL-IR"),
313            Self::Dsn => f.write_str("DSN"),
314            Self::RequireTls => f.write_str("REQUIRETLS"),
315            Self::FutureRelease {
316                max_interval,
317                max_datetime,
318            } => {
319                f.write_str("FUTURERELEASE")?;
320                if let Some(interval) = max_interval {
321                    write!(f, " {interval}")?;
322                }
323                if let Some(datetime) = max_datetime {
324                    write!(f, " {datetime}")?;
325                }
326                Ok(())
327            }
328            Self::DeliverBy(Some(n)) => write!(f, "DELIVERBY {n}"),
329            Self::DeliverBy(None) => f.write_str("DELIVERBY"),
330            Self::MtPriority => f.write_str("MT-PRIORITY"),
331            Self::Vrfy => f.write_str("VRFY"),
332            Self::Expn => f.write_str("EXPN"),
333            Self::NoSoliciting(Some(kw)) => write!(f, "NO-SOLICITING {kw}"),
334            Self::NoSoliciting(None) => f.write_str("NO-SOLICITING"),
335            Self::Other(s) => f.write_str(s),
336        }
337    }
338}
339
340/// SMTP authentication mechanism (RFC 4954 Section 3 / RFC 4422 Section 3.1).
341///
342/// Comparison and hashing are case-insensitive per RFC 4954 Section 3.
343#[non_exhaustive]
344#[derive(Debug, Clone)]
345#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
346pub enum AuthMechanism {
347    /// `PLAIN` (RFC 4616).
348    Plain,
349    /// `LOGIN` (draft-murchison-sasl-login, de-facto standard).
350    ///
351    /// AUTH LOGIN is a two-step challenge-response mechanism widely
352    /// deployed by corporate and legacy servers. The SASL exchange
353    /// follows the pattern in RFC 4954 Section 4.
354    Login,
355    /// `OAUTHBEARER` (RFC 7628 Section 3.1).
356    ///
357    /// Modern OAuth 2.0 bearer token SASL mechanism, replacing XOAUTH2.
358    OAuthBearer,
359    /// `XOAUTH2` (Google extension).
360    XOAuth2,
361    /// Unrecognized mechanism.
362    Other(String),
363}
364
365impl AuthMechanism {
366    /// Returns the canonical SASL mechanism name used on the wire.
367    ///
368    /// RFC 4954 Section 3 / RFC 4422 Section 3.1: mechanism names are
369    /// case-insensitive atoms.
370    fn as_mechanism_name(&self) -> &str {
371        match self {
372            Self::Plain => "PLAIN",
373            Self::Login => "LOGIN",
374            Self::OAuthBearer => "OAUTHBEARER",
375            Self::XOAuth2 => "XOAUTH2",
376            Self::Other(name) => name,
377        }
378    }
379
380    /// Case-insensitive mechanism name comparison.
381    ///
382    /// RFC 4954 Section 3 / RFC 4422 Section 3.1: SASL mechanism names
383    /// are case-insensitive. Known variants (`Plain`, `XOAuth2`) match
384    /// by identity. `Other` variants are compared using
385    /// `eq_ignore_ascii_case`, and cross-variant comparisons (e.g.
386    /// `Other("PLAIN")` vs `Plain`) are resolved by mapping `Other`
387    /// to its canonical name before comparing.
388    pub(crate) fn eq_mechanism(&self, other: &Self) -> bool {
389        self.as_mechanism_name()
390            .eq_ignore_ascii_case(other.as_mechanism_name())
391    }
392}
393
394impl PartialEq for AuthMechanism {
395    fn eq(&self, other: &Self) -> bool {
396        self.eq_mechanism(other)
397    }
398}
399
400impl Eq for AuthMechanism {}
401
402impl std::hash::Hash for AuthMechanism {
403    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
404        for byte in self.as_mechanism_name().as_bytes() {
405            byte.to_ascii_lowercase().hash(state);
406        }
407    }
408}
409
410impl std::fmt::Display for AuthMechanism {
411    /// Formats as the canonical SASL mechanism name (RFC 4954 Section 3).
412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413        f.write_str(self.as_mechanism_name())
414    }
415}
416
417/// Server capabilities parsed from EHLO response
418/// (RFC 5321 Section 4.1.1.1).
419#[non_exhaustive]
420#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
421#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
422pub struct ServerCapabilities {
423    /// Server greeting name from the EHLO response.
424    pub(crate) greeting_name: String,
425    /// Extensions advertised by the server.
426    pub(crate) extensions: Vec<SmtpExtension>,
427}
428
429impl ServerCapabilities {
430    /// Returns the server's greeting name from the EHLO response.
431    pub fn greeting_name(&self) -> &str {
432        &self.greeting_name
433    }
434
435    /// Returns the server's advertised extensions.
436    pub fn extensions(&self) -> &[SmtpExtension] {
437        &self.extensions
438    }
439
440    /// Check if the server supports a given auth mechanism.
441    ///
442    /// RFC 4954 Section 3 / RFC 4422 Section 3.1: SASL mechanism names
443    /// are case-insensitive, so this method performs case-insensitive
444    /// matching for [`AuthMechanism::Other`] variants.
445    pub fn supports_auth(&self, mechanism: &AuthMechanism) -> bool {
446        self.extensions.iter().any(|ext| {
447            if let SmtpExtension::Auth(mechs) = ext {
448                mechs.iter().any(|m| m.eq_mechanism(mechanism))
449            } else {
450                false
451            }
452        })
453    }
454
455    /// Check if the server advertises the AUTH extension at all.
456    ///
457    /// RFC 4954 Section 3: the EHLO AUTH keyword advertises support for the
458    /// AUTH command and the MAIL FROM AUTH parameter.
459    pub fn supports_auth_extension(&self) -> bool {
460        self.has_extension(|ext| matches!(ext, SmtpExtension::Auth(_)))
461    }
462
463    /// Check if any extension matches the given predicate.
464    fn has_extension(&self, predicate: fn(&SmtpExtension) -> bool) -> bool {
465        self.extensions.iter().any(predicate)
466    }
467
468    /// Check if the server advertises STARTTLS.
469    pub fn supports_starttls(&self) -> bool {
470        self.has_extension(|ext| matches!(ext, SmtpExtension::StartTls))
471    }
472
473    /// Check if the server supports CHUNKING (BDAT).
474    pub fn supports_chunking(&self) -> bool {
475        self.has_extension(|ext| matches!(ext, SmtpExtension::Chunking))
476    }
477
478    /// Check if the server supports the SIZE extension (RFC 1870).
479    ///
480    /// Returns `true` when the server advertises SIZE, regardless of
481    /// whether a numeric limit was included. Use [`Self::size_limit`] to
482    /// retrieve the limit value.
483    pub fn supports_size(&self) -> bool {
484        self.has_extension(|ext| matches!(ext, SmtpExtension::Size(_)))
485    }
486
487    /// Get the SIZE limit, if advertised.
488    pub fn size_limit(&self) -> Option<u64> {
489        self.extensions.iter().find_map(|ext| {
490            if let SmtpExtension::Size(limit) = ext {
491                *limit
492            } else {
493                None
494            }
495        })
496    }
497
498    /// Check if the server supports 8BITMIME (RFC 1652).
499    pub fn supports_8bitmime(&self) -> bool {
500        self.has_extension(|ext| matches!(ext, SmtpExtension::EightBitMime))
501    }
502
503    /// Check if the server supports BINARYMIME (RFC 3030).
504    pub fn supports_binarymime(&self) -> bool {
505        self.has_extension(|ext| matches!(ext, SmtpExtension::BinaryMime))
506    }
507
508    /// Check if the server supports either 8BITMIME (RFC 1652) or BINARYMIME (RFC 3030).
509    ///
510    /// RFC 1652 Section 1 / RFC 3030 Section 2: when neither extension is
511    /// advertised, the SMTP client is limited to 7-bit US-ASCII content.
512    /// Either extension satisfies the requirement for non-7-bit BODY parameters.
513    pub fn supports_8bit_or_binary(&self) -> bool {
514        self.supports_8bitmime() || self.supports_binarymime()
515    }
516
517    /// Check if the server supports PIPELINING (RFC 1854).
518    pub fn supports_pipelining(&self) -> bool {
519        self.has_extension(|ext| matches!(ext, SmtpExtension::Pipelining))
520    }
521
522    /// Check if the server supports SMTPUTF8 (RFC 6531).
523    pub fn supports_smtputf8(&self) -> bool {
524        self.has_extension(|ext| matches!(ext, SmtpExtension::SmtpUtf8))
525    }
526
527    /// Check whether the server advertised the legacy `SASL-IR` keyword.
528    ///
529    /// RFC 4954 Section 4 already allows SMTP AUTH initial responses
530    /// without a separate capability. This accessor is retained only so
531    /// callers can inspect the EHLO response as advertised.
532    pub fn supports_sasl_ir(&self) -> bool {
533        self.has_extension(|ext| matches!(ext, SmtpExtension::SaslIr))
534    }
535
536    /// Check if the server supports DSN (RFC 3461).
537    ///
538    /// When advertised, the server accepts Delivery Status Notification
539    /// parameters on MAIL FROM (RET, ENVID) and RCPT TO (NOTIFY, ORCPT)
540    /// per RFC 3461 Sections 4.1–4.4.
541    pub fn supports_dsn(&self) -> bool {
542        self.has_extension(|ext| matches!(ext, SmtpExtension::Dsn))
543    }
544
545    /// Check if the server supports REQUIRETLS (RFC 8689).
546    ///
547    /// When advertised, the client may include the REQUIRETLS parameter
548    /// on MAIL FROM to enforce TLS on every hop (RFC 8689 Sections 2–4).
549    pub fn supports_requiretls(&self) -> bool {
550        self.has_extension(|ext| matches!(ext, SmtpExtension::RequireTls))
551    }
552
553    /// Check if the server supports FUTURERELEASE (RFC 4865).
554    pub fn supports_future_release(&self) -> bool {
555        self.has_extension(|ext| matches!(ext, SmtpExtension::FutureRelease { .. }))
556    }
557
558    /// Get the server-advertised FUTURERELEASE maximum hold interval in
559    /// seconds, if any (RFC 4865 Section 4).
560    pub fn future_release_max_interval(&self) -> Option<u64> {
561        self.extensions.iter().find_map(|ext| {
562            if let SmtpExtension::FutureRelease { max_interval, .. } = ext {
563                *max_interval
564            } else {
565                None
566            }
567        })
568    }
569
570    /// Get the server-advertised FUTURERELEASE maximum hold-until datetime
571    /// string, if any (RFC 4865 Section 4).
572    pub fn future_release_max_datetime(&self) -> Option<&str> {
573        self.extensions.iter().find_map(|ext| {
574            if let SmtpExtension::FutureRelease { max_datetime, .. } = ext {
575                max_datetime.as_deref()
576            } else {
577                None
578            }
579        })
580    }
581
582    /// Check if the server supports DELIVERBY (RFC 2852).
583    pub fn supports_deliver_by(&self) -> bool {
584        self.has_extension(|ext| matches!(ext, SmtpExtension::DeliverBy(_)))
585    }
586
587    /// Get the server-advertised DELIVERBY minimum time in seconds, if any
588    /// (RFC 2852 Section 2).
589    pub fn deliver_by_min(&self) -> Option<u64> {
590        self.extensions.iter().find_map(|ext| {
591            if let SmtpExtension::DeliverBy(min) = ext {
592                *min
593            } else {
594                None
595            }
596        })
597    }
598
599    /// Check if the server supports MT-PRIORITY (RFC 6758).
600    pub fn supports_mt_priority(&self) -> bool {
601        self.has_extension(|ext| matches!(ext, SmtpExtension::MtPriority))
602    }
603
604    /// Check if the server supports VRFY (RFC 5321 Section 4.1.1.6).
605    pub fn supports_vrfy(&self) -> bool {
606        self.has_extension(|ext| matches!(ext, SmtpExtension::Vrfy))
607    }
608
609    /// Check if the server supports EXPN (RFC 5321 Section 4.1.1.7).
610    pub fn supports_expn(&self) -> bool {
611        self.has_extension(|ext| matches!(ext, SmtpExtension::Expn))
612    }
613
614    /// Check if the server supports Enhanced Status Codes (RFC 2034).
615    ///
616    /// When advertised, the server includes enhanced status codes
617    /// (`class.subject.detail`) in its response text per RFC 2034 Section 3.
618    pub fn supports_enhanced_status_codes(&self) -> bool {
619        self.has_extension(|ext| matches!(ext, SmtpExtension::EnhancedStatusCodes))
620    }
621}
622
623/// AUTH= ESMTP parameter value for MAIL FROM (RFC 4954 Section 5).
624///
625/// When relaying a message, an SMTP server SHOULD include `AUTH=<mailbox>`
626/// to declare the original authenticated sender, or `AUTH=<>` when the
627/// identity is unknown or unauthenticated.
628#[non_exhaustive]
629#[derive(Debug, Clone, PartialEq, Eq, Hash)]
630#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
631pub enum SmtpAuthParam {
632    /// Authenticated sender mailbox — encoded as xtext on the wire
633    /// (RFC 4954 Section 5).
634    Mailbox(Mailbox),
635    /// Unknown/unauthenticated origin — encoded as `<>` on the wire
636    /// (RFC 4954 Section 5).
637    Empty,
638}
639
640/// Parameters for the MAIL FROM command extensions.
641///
642/// Includes optional ESMTP parameters in the MAIL FROM command.
643///
644/// RFC 5321 Section 4.1.1.2.
645#[non_exhaustive]
646#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
647#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
648pub struct MailFromParams {
649    /// Message size estimate in bytes (RFC 1870 Section 3).
650    pub size: Option<u64>,
651    /// Body transfer type (RFC 1652, RFC 3030).
652    pub body: Option<BodyType>,
653    /// Include SMTPUTF8 parameter (RFC 6531 Section 3.4).
654    pub smtputf8: bool,
655    /// Require TLS on every hop (RFC 8689 Section 3).
656    pub requiretls: bool,
657    /// DSN RET parameter: controls which part of the message is returned
658    /// in a delivery status notification (RFC 3461 Section 4.3).
659    pub ret: Option<DsnRet>,
660    /// DSN ENVID parameter: sender-chosen envelope identifier included
661    /// in any delivery status notifications (RFC 3461 Section 4.4).
662    pub envid: Option<EnvidValue>,
663    /// Hold message for N seconds before delivery (RFC 4865 Section 5).
664    pub hold_for: Option<u64>,
665    /// Hold message until a specific datetime (RFC 4865 Section 5).
666    /// ISO 8601 timestamp string.
667    pub hold_until: Option<String>,
668    /// Deliver within N seconds or return (RFC 2852 Section 3).
669    pub deliver_by: Option<DeliverBy>,
670    /// Message transfer priority, -9 to +9 (RFC 6758 Section 4).
671    pub mt_priority: Option<i8>,
672    /// AUTH= parameter: original authenticated sender identity
673    /// (RFC 4954 Section 5). `None` omits the parameter entirely.
674    pub auth: Option<SmtpAuthParam>,
675}
676
677/// Parameters for the RCPT TO command extensions.
678///
679/// Includes optional ESMTP parameters in the RCPT TO command.
680///
681/// RFC 5321 Section 4.1.1.3.
682#[non_exhaustive]
683#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
684#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
685pub struct RcptToParams {
686    /// DSN NOTIFY parameter: conditions under which a DSN should be
687    /// generated for this recipient (RFC 3461 Section 4.1).
688    pub notify: Option<Vec<DsnNotify>>,
689    /// DSN ORCPT parameter: original recipient address for accurate
690    /// DSN generation (RFC 3461 Section 4.2).
691    pub orcpt: Option<String>,
692}
693
694impl RcptToParams {
695    /// Returns `true` if no parameters are set.
696    ///
697    /// When empty, the RCPT TO command is encoded without any extension
698    /// parameters.
699    pub fn is_empty(&self) -> bool {
700        let notify_is_empty = self.notify.as_ref().map_or(true, Vec::is_empty);
701        notify_is_empty && self.orcpt.is_none()
702    }
703}
704
705/// DELIVERBY parameters for the MAIL FROM command (RFC 2852 Section 3).
706#[non_exhaustive]
707#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
708#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
709pub struct DeliverBy {
710    /// Number of seconds within which the message should be delivered.
711    /// Negative values indicate the message has already been in transit
712    /// for that many seconds (RFC 2852 Section 4).
713    pub seconds: i64,
714    /// Delivery mode (RFC 2852 Section 3).
715    pub mode: DeliverByMode,
716    /// RFC 2852 Section 4: optional trace flag (`T`) requesting return of
717    /// trace information with any delivery status notification.
718    pub trace: bool,
719}
720
721/// DELIVERBY mode (RFC 2852 Section 3).
722#[non_exhaustive]
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
724#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
725pub enum DeliverByMode {
726    /// `N` — Notify sender if delivery fails within the time limit.
727    Notify,
728    /// `R` — Return the message if delivery fails within the time limit.
729    Return,
730}
731
732/// DSN RET parameter value (RFC 3461 Section 4.3).
733///
734/// Controls which part of the original message is returned in a
735/// delivery status notification.
736#[non_exhaustive]
737#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
738#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
739pub enum DsnRet {
740    /// Return the full message in DSNs (RFC 3461 Section 4.3).
741    Full,
742    /// Return only the headers in DSNs (RFC 3461 Section 4.3).
743    Hdrs,
744}
745
746/// DSN NOTIFY condition (RFC 3461 Section 4.1).
747///
748/// Specifies under which conditions a delivery status notification
749/// should be generated for a given recipient.
750#[non_exhaustive]
751#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
752#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
753pub enum DsnNotify {
754    /// Notify on successful delivery (RFC 3461 Section 4.1).
755    Success,
756    /// Notify on delivery failure (RFC 3461 Section 4.1).
757    Failure,
758    /// Notify on delivery delay (RFC 3461 Section 4.1).
759    Delay,
760    /// Never send a DSN for this recipient (RFC 3461 Section 4.1).
761    ///
762    /// NEVER must not be combined with other values.
763    Never,
764}
765
766/// Body transfer type for the MAIL FROM `BODY=` parameter.
767///
768/// RFC 1652 Section 3 (8BITMIME), RFC 3030 Section 2 (BINARYMIME).
769#[non_exhaustive]
770#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
771#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
772pub enum BodyType {
773    /// 7-bit content (default per RFC 5321 Section 4.5.2).
774    SevenBit,
775    /// 8-bit content (RFC 1652 Section 3).
776    EightBitMime,
777    /// Binary content (RFC 3030 Section 2).
778    BinaryMime,
779}
780
781#[cfg(test)]
782#[path = "tests.rs"]
783mod tests;