mailrs_outbound_queue/worker/
smtp.rs1use std::sync::Arc;
4
5use hickory_resolver::TokioResolver;
6
7use crate::queue::QueuedMessage;
8use crate::{DeliveryEvent, DeliveryEventSender, TlsAttemptOutcome};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TlsPolicy {
13 Opportunistic,
15 Require,
17}
18
19#[allow(clippy::too_many_arguments)]
27pub async fn try_deliver_via_mx(
28 hostname: &str,
29 mx_host: &str,
30 port: u16,
31 domain: &str,
32 messages: &[QueuedMessage],
33 resolver: &TokioResolver,
34 event_sender: Option<&DeliveryEventSender>,
35) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
36 try_deliver_via_mx_with_tls(
37 hostname,
38 mx_host,
39 port,
40 domain,
41 messages,
42 TlsPolicy::Opportunistic,
43 None,
44 resolver,
45 event_sender,
46 )
47 .await
48}
49
50#[allow(clippy::too_many_arguments)]
59pub async fn try_deliver_via_mx_with_tls(
60 hostname: &str,
61 mx_host: &str,
62 port: u16,
63 domain: &str,
64 messages: &[QueuedMessage],
65 tls_policy: TlsPolicy,
66 tls_config_override: Option<Arc<rustls::ClientConfig>>,
67 resolver: &TokioResolver,
68 event_sender: Option<&DeliveryEventSender>,
69) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
70 use mailrs_smtp_client::StarttlsResult;
71
72 let emit_tls = |outcome: TlsAttemptOutcome| {
74 if let Some(es) = event_sender {
75 es(DeliveryEvent::TlsAttempt {
76 domain: domain.to_string(),
77 mx_host: mx_host.to_string(),
78 outcome,
79 });
80 }
81 };
82
83 let mut smtp = mailrs_smtp_client::SmtpConnection::connect(mx_host, port).await?;
84 let ehlo_resp = smtp.ehlo(hostname).await?;
85
86 if !ehlo_resp.is_positive() {
87 return Err(format!("EHLO rejected: {}", ehlo_resp.message()).into());
88 }
89
90 let tlsa_records = mailrs_smtp_client::resolve_tlsa(resolver, mx_host).await;
92 let has_dane = !tlsa_records.is_empty();
93 if has_dane {
94 tracing::debug!("found {} TLSA records for {mx_host}", tlsa_records.len());
95 }
96
97 if ehlo_resp.has_extension("STARTTLS") {
99 let tls_result = if has_dane {
100 smtp.try_starttls_dane(mx_host, tlsa_records).await
102 } else if let Some(ref cfg) = tls_config_override {
103 smtp.try_starttls_with_config(mx_host, (**cfg).clone())
107 .await
108 } else {
109 smtp.try_starttls(mx_host).await
111 };
112
113 match tls_result {
114 StarttlsResult::Success(tls_smtp) => {
115 smtp = tls_smtp;
116 let _ = smtp.ehlo(hostname).await?;
117 let policy: &'static str = if has_dane {
118 tracing::debug!("DANE-verified TLS established with {mx_host}");
119 "dane"
120 } else {
121 tracing::debug!("TLS established with {mx_host}");
122 "opportunistic"
123 };
124 emit_tls(TlsAttemptOutcome::Success { policy });
125 }
126 StarttlsResult::Rejected {
127 conn,
128 code,
129 message,
130 } => {
131 emit_tls(TlsAttemptOutcome::Rejected {
132 code,
133 message: message.clone(),
134 });
135 if has_dane || tls_policy == TlsPolicy::Require {
136 return Err(format!(
137 "STARTTLS rejected by {mx_host} ({code}): {message}{}",
138 if has_dane {
139 " (DANE required)"
140 } else {
141 " (TLS required)"
142 }
143 )
144 .into());
145 }
146 tracing::warn!(
147 "STARTTLS rejected by {mx_host} ({code}): {message}; continuing in plain"
148 );
149 smtp = conn;
152 }
153 StarttlsResult::HandshakeFailed { outcome, source } => {
154 emit_tls(TlsAttemptOutcome::HandshakeFailed(outcome.clone()));
155 if has_dane || tls_policy == TlsPolicy::Require {
156 return Err(format!(
157 "STARTTLS handshake failed for {mx_host} ({}): {source}{}",
158 outcome.as_str(),
159 if has_dane {
160 " (DANE required)"
161 } else {
162 " (TLS required)"
163 }
164 )
165 .into());
166 }
167 tracing::warn!(
168 "STARTTLS handshake failed for {mx_host} ({}): {source}; reconnecting in plain",
169 outcome.as_str()
170 );
171 smtp = mailrs_smtp_client::SmtpConnection::connect(mx_host, port).await?;
172 let resp = smtp.ehlo(hostname).await?;
173 if !resp.is_positive() {
174 return Err(format!("EHLO rejected on reconnect: {}", resp.message()).into());
175 }
176 }
177 }
178 } else if has_dane || tls_policy == TlsPolicy::Require {
179 emit_tls(TlsAttemptOutcome::NotAdvertised);
180 return Err(format!(
181 "{mx_host} does not advertise STARTTLS{}",
182 if has_dane {
183 " (DANE TLSA records present, TLS required)"
184 } else {
185 " and TLS is required"
186 }
187 )
188 .into());
189 } else {
190 emit_tls(TlsAttemptOutcome::NotAdvertised);
191 tracing::info!("delivering to {mx_host} without TLS (STARTTLS not advertised)");
192 }
193
194 for msg in messages {
195 let to = [msg.recipient.as_str()];
196 let resp = smtp.deliver(&msg.sender, &to, &msg.message_data).await?;
197 if !resp.is_positive() {
198 return Err(format!("delivery failed: {}", resp.message()).into());
199 }
200 }
201
202 let _ = smtp.quit().await;
203 Ok(())
204}