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;