Skip to main content

daaki_smtp/
lib.rs

1//! `daaki` SMTP client library.
2//!
3//! An async SMTP client (RFC 5321) built on tokio and rustls.
4//! Handles connection, authentication, and message transmission. Does NOT
5//! construct RFC 5322 messages — it sends raw bytes provided by the caller.
6//!
7//! # Connection ownership
8//!
9//! [`SmtpConnection`] methods take `&self` — the connection can be shared
10//! across async tasks via `Arc<SmtpConnection>`. An internal
11//! `tokio::sync::Mutex` serializes operations, since SMTP is a strictly
12//! serial protocol (RFC 5321 Section 3.1) with no command tagging.
13//!
14//! [`daaki_imap::ImapConnection`] also takes `&self` but achieves
15//! concurrency-safety through a driver-task architecture instead: a
16//! dedicated tokio task owns the stream, and the handle communicates
17//! via channels (RFC 3501 Section 5.5 permits overlapping commands via
18//! unique tags, so true pipelining is possible).
19
20// `fuzzing` cfg is set by cargo-fuzz (nightly) — not known to check-cfg.
21#![cfg_attr(not(fuzzing), allow(unexpected_cfgs))]
22
23pub mod error;
24pub mod types;
25
26mod codec;
27mod connection;
28mod deliver_by;
29mod future_release;
30
31pub use connection::{SmtpConnection, TlsMode};
32/// Re-export the canonical `Address` type from `daaki-message` so consumers
33/// can use a single `Address` type across the IMAP, SMTP, and message crates
34/// without manual field-by-field conversion.
35pub use daaki_message::Address;
36pub use error::Error;
37pub use types::{
38    AddressLiteral, AuthMechanism, BodyType, DeliverBy, DeliverByMode, Domain, DomainOrLiteral,
39    DsnNotify, DsnRet, EnhancedStatusCode, EnvidValue, ForwardPath, LmtpSendResult, MailFromParams,
40    Mailbox, Protocol, RcptToParams, RecipientResult, RejectedRecipient, ReversePath, SendResult,
41    ServerCapabilities, SmtpAuthParam, SmtpExtension, SmtpResponse, ValidationError, XtextSafe,
42};
43
44/// Result type alias for SMTP operations.
45pub type Result<T> = std::result::Result<T, Error>;
46
47/// Fuzz-only entry points. Not part of the public API.
48///
49/// Exposed behind `#[cfg(fuzzing)]` (set automatically by `cargo-fuzz`) so
50/// that out-of-crate fuzz harnesses can reach the `pub(crate)` codec parsers.
51#[allow(unexpected_cfgs)]
52#[cfg(fuzzing)]
53#[doc(hidden)]
54pub mod fuzz {
55    use crate::codec::decode;
56
57    /// Thin wrapper around [`decode::parse_response`].
58    ///
59    /// Discards nom's remaining-input slice and returns just the parsed
60    /// [`SmtpResponse`](crate::types::SmtpResponse), or `None` on any
61    /// parse failure.
62    pub fn parse_response(input: &[u8]) -> Option<crate::types::SmtpResponse> {
63        decode::parse_response(input).ok().map(|(_, r)| r)
64    }
65
66    /// Thin wrapper around [`decode::parse_ehlo_capabilities`].
67    ///
68    /// Parses EHLO response lines into structured server capabilities
69    /// per RFC 5321 Section 4.1.1.1.
70    pub fn parse_ehlo_capabilities(
71        response: &crate::types::SmtpResponse,
72    ) -> crate::types::ServerCapabilities {
73        decode::parse_ehlo_capabilities(response)
74    }
75
76    /// Thin wrapper around [`decode::strip_enhanced_code`].
77    ///
78    /// Extracts an RFC 2034 enhanced status code from the beginning of
79    /// a response text line, returning the code and the remaining text.
80    pub fn strip_enhanced_code(
81        text: &str,
82        reply_code: u16,
83    ) -> Option<(crate::types::EnhancedStatusCode, String)> {
84        decode::strip_enhanced_code(text, reply_code).map(|(esc, rest)| (esc, rest.to_owned()))
85    }
86
87    /// Thin wrapper around [`decode::parse_enhanced_code_from_str`].
88    ///
89    /// Parses a standalone RFC 2034 enhanced status code string
90    /// (e.g. `"2.1.0"`).
91    pub fn parse_enhanced_code_from_str(s: &str) -> Option<crate::types::EnhancedStatusCode> {
92        decode::parse_enhanced_code_from_str(s)
93    }
94}
95
96/// Consumer-facing README examples must compile against the current public API.
97///
98/// This turns the README into executable doctests so stale helper signatures
99/// and example code are caught during `cargo test --doc`.
100#[cfg(doctest)]
101#[doc = include_str!("../README.md")]
102mod readme_doctests {}
103
104#[cfg(test)]
105mod tests {
106    /// Consumer-facing README claims must stay aligned with the implemented
107    /// SMTP extension surface. RFC 4954 Section 4 already allows initial
108    /// responses on AUTH, so the legacy `SASL-IR` EHLO keyword must not be
109    /// presented as a distinct SMTP extension in the support table.
110    #[test]
111    fn readme_does_not_advertise_sasl_ir_as_smtp_extension() {
112        let readme = include_str!("../README.md");
113        let auth_row = readme
114            .lines()
115            .find(|line| line.contains("| **Auth** |"))
116            .unwrap_or_default();
117        assert!(
118            !auth_row.contains("SASL-IR"),
119            "README must not present legacy SASL-IR as a standard SMTP extension"
120        );
121    }
122}