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}