Skip to main content

offline_protocol/protocol/
send.rs

1//! Send pipeline, outbox management, and delivery tracking.
2
3use super::custody::CUSTODY_META_KEY;
4use super::{
5    base64_encode, internal_prefixes, lifetime_expired, lock_shared_state,
6    ConnectionAcceptedPayload, ConnectionRequestPayload, KeyPackagePayload, MediaSendOptions,
7    MediaTransferDescriptor, OfflineProtocol, OutboundMediaTransfer, OutboundSendPreparation,
8    OutboxEntry, OutboxReseal, PendingConnectionRequest, PendingMessage, PendingProvenance,
9    PresencePayload, ProtocolState, ReadReceiptPayload, RichPayloadV1, RichSendExtras,
10    SendMessageOptions, TypingIndicatorPayload, WelcomeDeliveryState, MAX_INITIAL_MESSAGE_BYTES,
11    MAX_KEY_PACKAGE_SENT_TO, MAX_MESSAGE_CONTENT_BYTES, MAX_PENDING_CONNECTION_REQUESTS,
12    MAX_PENDING_EXPIRIES_PER_PASS, MAX_PENDING_MESSAGES_GLOBAL, MAX_PENDING_MESSAGES_PER_PEER,
13    MAX_PENDING_MESSAGE_BYTES_GLOBAL, MAX_PENDING_MESSAGE_BYTES_PER_PEER, MAX_READ_RECEIPT_IDS,
14    MAX_RICH_EXTRAS_BYTES, MLS_ENVELOPE_COMPACT_V1, PENDING_CONNECTION_REQUEST_TTL,
15    RICH_PAYLOAD_V1, SEND_FAIL_REASON_RECIPIENT_UNREACHABLE, SEND_FAIL_REASON_RELAY_PUSHED,
16    SEND_FAIL_REASON_RELAY_PUSHED_STORED, SEND_FAIL_REASON_RELAY_STORED,
17    SEND_FAIL_REASON_TRANSPORT, WELCOME_NO_CARRIER_RETRY_SECS, WELCOME_UNREACHABLE_RETRY_CAP_SECS,
18};
19use super::{classify_transport_send_error, send_failure_token};
20use crate::constants::{
21    ACK_FOR_KEY, ACK_HOP_COUNT_KEY, ACK_STATUS_KEY, ACK_STATUS_UNDECRYPTABLE, ACK_TRANSPORT_KEY,
22    MAX_FORWARD_COUNT, MAX_OUTBOX_ENTRIES,
23};
24use crate::events::{DecryptionFailureCode, Event, PresenceStatus};
25use crate::file_transfer::{FileChunk, OutboundTransferState};
26use crate::media_envelope::{encode_media_envelope, MediaChunkPlaintext, MediaRichExtras};
27use crate::mls_observability::{DecryptionFailureKind, MlsErrorCategory, MlsOperationContext};
28use crate::protocol::reachability::{Claim, FactSource};
29use crate::transport_manager::TransportManager;
30use crate::{Error, Result};
31use chrono::{DateTime, Utc};
32use offline_protocol_core::{
33    AppId, ContentType, ForwardInfo, MediaMetadata, Message, MessageId, MessagePriority, UserId,
34    TTL,
35};
36use offline_protocol_mls::{EncryptedMessage, MlsManager};
37use offline_protocol_transport::TransportType;
38use std::collections::HashSet;
39use std::sync::{Arc, RwLock};
40use std::time::{Duration as StdDuration, Instant};
41use tracing::{debug, error, info, warn};
42
43/// Most delivery acknowledgements held for want of a route (see
44/// `route_ack`). ~200 bytes each, so a ceiling of ~100 KB.
45pub(crate) const MAX_UNROUTED_ACKS: usize = 500;
46
47/// How long a held acknowledgement stays worth sending. Short on purpose:
48/// it bridges a carrier coming up moments later, and a sender still waiting
49/// past it re-sends, which the duplicate re-ACK path answers.
50pub(crate) const UNROUTED_ACK_TTL: StdDuration = StdDuration::from_secs(600);
51
52impl OfflineProtocol {
53    // ========================================================================
54    // CORE SEND
55    // ========================================================================
56
57    /// Sends a message.
58    ///
59    /// # Arguments
60    ///
61    /// * `recipient` - Recipient's user ID
62    /// * `content` - Message content
63    /// * `priority` - Message priority (optional, defaults to Medium)
64    /// * `reply_to_msg` - ID of the message this is replying to (optional)
65    ///
66    /// # Returns
67    ///
68    /// Returns the message ID if successful.
69    ///
70    /// # Auto-Encryption
71    ///
72    /// When encryption is enabled and MLS is initialized, messages are automatically
73    /// encrypted before sending. If no session exists with the recipient but we have
74    /// their key package, a session is created automatically. If no key package is
75    /// available and `store_pending` is enabled, the message is queued until a key
76    /// package is received.
77    pub fn send_message(
78        &mut self,
79        recipient: impl Into<String>,
80        content: impl Into<String>,
81        priority: Option<MessagePriority>,
82        reply_to_msg: Option<impl Into<String>>,
83    ) -> Result<MessageId> {
84        self.send_message_with(
85            recipient,
86            content,
87            SendMessageOptions {
88                priority,
89                reply_to_msg: reply_to_msg.map(Into::into),
90                ..Default::default()
91            },
92        )
93    }
94
95    /// Sends a message with rich options: priority and reply threading (as
96    /// on [`Self::send_message`]), plus quoted-reply context, rich media
97    /// metadata, and forward attribution.
98    ///
99    /// The rich fields only ever travel inside the MLS-sealed `__RICH_V1__`
100    /// body, and only toward recipients whose key package advertised
101    /// `rich_versions` support (gated by
102    /// `EncryptionConfig::rich_payload_enabled`). Toward anyone else they
103    /// are silently dropped — never sent cleartext — so the message degrades
104    /// to plain text with `reply_to_msg` threading intact.
105    ///
106    /// Returns `InvalidArgument` for `ContentType::FileChunk` (an internal
107    /// transport content type — the receiver would swallow the message into
108    /// its file-transfer manager) and for rich extras whose serialized size
109    /// exceeds 32 KiB (oversized bodies inflate the MLS plaintext into heavy
110    /// transport fragmentation).
111    pub fn send_message_with(
112        &mut self,
113        recipient: impl Into<String>,
114        content: impl Into<String>,
115        options: SendMessageOptions,
116    ) -> Result<MessageId> {
117        // Check if protocol is running
118        {
119            let state = lock_shared_state(&self.shared_state)?;
120            if state.state != ProtocolState::Running {
121                return Err(Error::NotStarted);
122            }
123        }
124
125        let recipient_str: String = recipient.into();
126        let content_str: String = content.into();
127        let priority = options.priority.unwrap_or(MessagePriority::Medium);
128        Self::validate_outbound_recipient(&recipient_str)?;
129        // Parsed first, before anything can queue the send or tick the
130        // Lamport clock: a bad id must leave no durable trace.
131        let app_id = Self::parse_send_app_id(options.app_id.as_deref())?;
132
133        // Prevent sending messages to blocked users. Blocking is bidirectional:
134        // we neither receive from nor send to a blocked peer.
135        if self.is_user_blocked(&recipient_str) {
136            return Err(Error::UserBlocked(recipient_str));
137        }
138
139        Self::check_content_size(&content_str)?;
140
141        // Reject content that starts with an internal control prefix to prevent
142        // injection of protocol-level messages through the public API.
143        if Self::is_internal_prefix(&content_str) {
144            return Err(Error::InvalidArgument(
145                "Message content must not start with a reserved internal prefix".to_string(),
146            ));
147        }
148
149        // FileChunk is an internal transport content type: the receiver
150        // routes a message stamped with it into handle_incoming_file_chunk
151        // (after ACKing delivery) where a non-chunk fails to parse and is
152        // dropped without surfacing. Reject rather than silently lose it.
153        if options.content_type == Some(ContentType::FileChunk) {
154            return Err(Error::InvalidArgument(
155                "FileChunk is an internal content type and cannot be sent directly".to_string(),
156            ));
157        }
158
159        // Parse reply_to_msg if provided
160        let reply_to_msg_id = options
161            .reply_to_msg
162            .as_deref()
163            .map(MessageId::from_str)
164            .transpose()
165            .map_err(|e| Error::InvalidArgument(format!("Invalid reply_to_msg: {}", e)))?;
166
167        let rich = RichSendExtras {
168            reply_context: options.reply_context,
169            media_metadata: options.media_metadata,
170            forward_info: options.forward_info,
171        };
172
173        // Bound the extras here at the boundary — not at seal time — so the
174        // pending queue only ever holds extras that are known to seal: the
175        // flush path re-seals against current capability, and a seal-time
176        // failure there would re-queue the message forever. The cap keeps an
177        // oversized quote or thumbnail from inflating the MLS plaintext into
178        // heavy transport fragmentation.
179        rich.check_size()?;
180
181        let final_content = match self.prepare_outbound_content(
182            &recipient_str,
183            &content_str,
184            priority,
185            reply_to_msg_id.clone(),
186            None,
187            options.content_type.unwrap_or_default(),
188            None,
189            Some(&rich),
190            app_id.as_ref(),
191            None,
192            "send_message_session_pending",
193        )? {
194            OutboundSendPreparation::Ready(content) => content,
195            OutboundSendPreparation::Queued(message_id) => return Ok(message_id),
196        };
197
198        // Create message with potentially encrypted content. Rich extras are
199        // deliberately NOT copied onto the outer message — they are either
200        // inside the sealed body by now or dropped; only the coarse
201        // content_type rendering hint rides outer.
202        let reseal_reply_to = reply_to_msg_id.clone();
203        let mut message = self.create_message_for_app(
204            &recipient_str,
205            final_content,
206            Some(priority),
207            reply_to_msg_id,
208            app_id,
209        )?;
210        if let Some(content_type) = options.content_type {
211            message.content_type = content_type;
212        }
213
214        // Tier 2: if the send was encrypted, keep the plaintext provenance so
215        // each resend re-seals against the peer's current session instead of
216        // replaying dead-epoch ciphertext (see `OutboxReseal`).
217        if message.content.starts_with(internal_prefixes::ENCRYPTED) {
218            self.stage_outbox_reseal(
219                &message.id,
220                OutboxReseal {
221                    content: content_str,
222                    priority,
223                    reply_to_msg: reseal_reply_to,
224                    forwarded_from: None,
225                    content_type: message.content_type,
226                    media_metadata: None,
227                    rich: Some(rich).filter(|r| r.is_any()),
228                },
229            );
230        }
231
232        match options.via_transport {
233            Some(transport) => self.dispatch_prepared_message_via(message, transport),
234            None => self.dispatch_prepared_message(message),
235        }
236    }
237
238    /// Forwards a message to a new recipient with attribution to the original sender.
239    ///
240    /// Creates a new message with the original content and attaches `ForwardInfo`
241    /// tracking the original sender, message ID, timestamp, and forward count.
242    /// If the message was already forwarded, the original attribution is preserved
243    /// and `forward_count` is incremented.
244    ///
245    /// Toward a recipient that advertised the sealed rich payload, the
246    /// attribution and the original `media_metadata` — including any
247    /// cloud-media `encryption_key`/`iv` secrets — travel inside the MLS-sealed
248    /// `__RICH_V1__` body, so forwarded cloud media stays openable. Toward
249    /// anyone else they ride the legacy cleartext outer fields, from which
250    /// the transport chokepoint strips the secrets — attribution and display
251    /// hints survive, but the media key cannot be delivered.
252    ///
253    /// # Arguments
254    ///
255    /// * `original_message` - The message to forward
256    /// * `new_recipient` - Recipient's user ID
257    /// * `priority` - Message priority (optional, defaults to Medium)
258    ///
259    /// # Returns
260    ///
261    /// Returns the new message ID if successful.
262    pub fn forward_message(
263        &mut self,
264        original_message: &Message,
265        new_recipient: impl Into<String>,
266        priority: Option<MessagePriority>,
267    ) -> Result<MessageId> {
268        {
269            let state = lock_shared_state(&self.shared_state)?;
270            if state.state != ProtocolState::Running {
271                return Err(Error::NotStarted);
272            }
273        }
274
275        let recipient_str: String = new_recipient.into();
276        let priority = priority.unwrap_or(MessagePriority::Medium);
277        Self::validate_outbound_recipient(&recipient_str)?;
278
279        if self.is_user_blocked(&recipient_str) {
280            return Err(Error::UserBlocked(recipient_str));
281        }
282
283        // Reject content that starts with an internal control prefix to prevent
284        // injection of protocol-level messages through the forwarding API.
285        if Self::is_internal_prefix(&original_message.content) {
286            return Err(Error::InvalidArgument(
287                "Cannot forward a message with reserved internal prefix content".to_string(),
288            ));
289        }
290
291        // FileChunk is an internal transport content type (see
292        // `send_message_with`): a forwarded message stamped with it would be
293        // ACKed and then dropped by the receiver's file-transfer manager.
294        if original_message.content_type == ContentType::FileChunk {
295            return Err(Error::InvalidArgument(
296                "FileChunk is an internal content type and cannot be forwarded".to_string(),
297            ));
298        }
299
300        // Build ForwardInfo: preserve original attribution, increment count
301        let forward_info = ForwardInfo::from_message(original_message);
302
303        if forward_info.forward_count > MAX_FORWARD_COUNT {
304            return Err(Error::InvalidArgument(format!(
305                "Forward count {} exceeds maximum of {}",
306                forward_info.forward_count, MAX_FORWARD_COUNT,
307            )));
308        }
309
310        // Sealed-path extras: toward a rich-capable recipient the attribution
311        // and the original media metadata (including cloud-media key/iv
312        // secrets, which only the sealed body may carry — the wire chokepoint
313        // strips them from the cleartext outer field) travel inside the
314        // `__RICH_V1__` body. The outer copies set below remain the fallback
315        // for non-capable recipients; the receiver's sealed-body restore is
316        // authoritative and overwrites them when the seal happened.
317        let rich = RichSendExtras {
318            reply_context: None,
319            media_metadata: original_message.media_metadata.clone(),
320            forward_info: Some(forward_info.clone()),
321        };
322        // Same boundary caps as `send_message_with` — enforced here, not at
323        // seal time, so a forward queued behind session establishment is
324        // always known to seal at flush and always fits the queue's budget.
325        Self::check_content_size(&original_message.content)?;
326        rich.check_size()?;
327
328        // Prepare content (may encrypt). ForwardInfo is threaded through so
329        // it survives the pending-message queue if the session isn't ready.
330        let final_content = match self.prepare_outbound_content(
331            &recipient_str,
332            &original_message.content,
333            priority,
334            None,
335            Some(forward_info.clone()),
336            original_message.content_type,
337            original_message.media_metadata.clone(),
338            Some(&rich),
339            None,
340            None,
341            "forward_message_session_pending",
342        )? {
343            OutboundSendPreparation::Ready(content) => content,
344            OutboundSendPreparation::Queued(message_id) => return Ok(message_id),
345        };
346
347        // Create and dispatch the forwarded message
348        let mut message =
349            self.create_message_for_app(&recipient_str, final_content, Some(priority), None, None)?;
350        message.forwarded_from = Some(forward_info);
351        message.content_type = original_message.content_type;
352        message.media_metadata = original_message.media_metadata.clone();
353
354        // Tier 2 provenance (see `send_message_with`): re-seal forwarded encrypted
355        // DMs against the peer's current session on resend.
356        if message.content.starts_with(internal_prefixes::ENCRYPTED) {
357            self.stage_outbox_reseal(
358                &message.id,
359                OutboxReseal {
360                    content: original_message.content.clone(),
361                    priority,
362                    reply_to_msg: None,
363                    forwarded_from: message.forwarded_from.clone(),
364                    content_type: message.content_type,
365                    media_metadata: message.media_metadata.clone(),
366                    rich: Some(rich).filter(|r| r.is_any()),
367                },
368            );
369        }
370
371        self.dispatch_prepared_message(message)
372    }
373
374    /// Shared send path: dedup, transport send, success/failure handling, event emission.
375    ///
376    /// Used by `forward_message`, `flush_pending_messages` (for forwarded messages),
377    /// and any path that has a fully-constructed `Message` ready to send.
378    fn dispatch_prepared_message(&mut self, message: Message) -> Result<MessageId> {
379        let message_id = message.id.clone();
380
381        if self.deduplicator.is_duplicate(&message_id) {
382            return Err(crate::Error::Other("Duplicate message".to_string()));
383        }
384
385        self.deduplicator.mark_seen_local(message_id.clone());
386
387        let previous_transport = self.transport_manager.current_transport();
388
389        // A recipient this device holds a live mesh link to is reached over
390        // that link, even with infrastructure up. Without this the online
391        // sender standing next to an offline recipient routes through the
392        // relay, which answers `recipient_unreachable`, and delivery waits for
393        // the park machinery to offer the frame to the very neighbour that was
394        // addressable all along. DORS cannot make this call: it scores
395        // carriers, and the fact that decides it is about the recipient.
396        //
397        // The link is ground truth, not a stored claim, so there is nothing to
398        // go stale. A refusal here is not terminal: the frame falls through to
399        // selection exactly as if this had never been tried, which keeps a
400        // marginal radio link from stranding a message another carrier could
401        // take.
402        let direct_mesh = self
403            .transport_manager
404            .mesh_transport_for(message.recipient.as_str());
405        let mut sent_via_mesh = None;
406        if let Some(transport) = direct_mesh {
407            match self
408                .transport_manager
409                .send_via_transport(&message, transport)
410            {
411                Ok(()) => sent_via_mesh = Some(transport),
412                Err(err) => {
413                    self.transport_manager.record_retry_failure(transport);
414                    debug!(
415                        message_id = %message.id,
416                        transport = ?transport,
417                        error = %err,
418                        "Direct mesh link refused the frame; falling back to transport selection"
419                    );
420                }
421            }
422        }
423
424        let (send_result, current_transport) = match sent_via_mesh {
425            Some(transport) => (Ok(()), Some(transport)),
426            None => {
427                let result = self.transport_manager.send(&message);
428                (result, self.transport_manager.current_transport())
429            }
430        };
431
432        match send_result {
433            Ok(()) => {
434                self.handle_send_success(&message, current_transport)?;
435                self.emit_transport_switch_event(previous_transport, current_transport)?;
436                self.emit_message_sent_event(&message)?;
437                Ok(message_id)
438            }
439            Err(err) => {
440                let next_retry_at =
441                    self.handle_send_failure(&message, current_transport.or(previous_transport))?;
442                warn!(
443                    message_id = %message.id,
444                    error = %err,
445                    "Send failed, message deferred"
446                );
447                self.emit_event(Event::message_deferred(
448                    message.id.clone(),
449                    message.recipient.as_str().to_string(),
450                    send_failure_token(&err),
451                    0,
452                    next_retry_at.map(|at| at.timestamp_millis()),
453                ));
454                Ok(message_id)
455            }
456        }
457    }
458
459    /// [`Self::dispatch_prepared_message`] variant that sends via a specific
460    /// transport (bypassing DORS) — the shared tail for
461    /// `send_message_via_transport` and rich sends carrying `via_transport`.
462    /// Unlike `TransportManager::send`, `send_via_transport` does not record
463    /// retry failures internally, so the failure arm records one explicitly.
464    fn dispatch_prepared_message_via(
465        &mut self,
466        message: Message,
467        transport: TransportType,
468    ) -> Result<MessageId> {
469        let message_id = message.id.clone();
470
471        if self.deduplicator.is_duplicate(&message_id) {
472            return Err(crate::Error::Other("Duplicate message".to_string()));
473        }
474
475        self.deduplicator.mark_seen_local(message_id.clone());
476
477        let previous_transport = self.transport_manager.current_transport();
478        let send_result = self
479            .transport_manager
480            .send_via_transport(&message, transport);
481        let current_transport = Some(transport);
482
483        match send_result {
484            Ok(()) => {
485                self.handle_send_success(&message, current_transport)?;
486                self.emit_transport_switch_event(previous_transport, current_transport)?;
487                self.emit_message_sent_event(&message)?;
488                Ok(message_id)
489            }
490            Err(err) => {
491                let next_retry_at =
492                    self.handle_send_failure(&message, current_transport.or(previous_transport))?;
493                // send_via_transport does not record retry failures internally
494                // (unlike TransportManager::send), so record explicitly here.
495                self.transport_manager.record_retry_failure(transport);
496                warn!(
497                    message_id = %message.id,
498                    transport = ?transport,
499                    error = %err,
500                    "Send via forced transport failed, message deferred"
501                );
502                // The forced transport is in the `warn!` above, not on the
503                // event: it is a routing detail, and the token names the
504                // failure class either way.
505                self.emit_event(Event::message_deferred(
506                    message.id.clone(),
507                    message.recipient.as_str().to_string(),
508                    send_failure_token(&err),
509                    0,
510                    next_retry_at.map(|at| at.timestamp_millis()),
511                ));
512                Ok(message_id)
513            }
514        }
515    }
516
517    /// Sends an internal protocol message (connection requests, etc.) via DORS.
518    ///
519    /// Handles the full send orchestration: state check, deduplication, transport send,
520    /// success/failure handling, and transport switch events. Does NOT emit a
521    /// `MessageSent` event — internal messages are not user-visible content.
522    pub(crate) fn send_internal_message(
523        &mut self,
524        recipient: &str,
525        content: String,
526        priority: MessagePriority,
527    ) -> Result<MessageId> {
528        self.send_internal_message_alerting(recipient, content, priority, false)
529    }
530
531    /// [`Self::send_internal_message`], with `alert` choosing whether the
532    /// relay may push for the frame. `true` only for the control frames that
533    /// carry something the user acts on or reads: a connection request or
534    /// accept, and a group chat message. Everything else is
535    /// [`crate::constants::NO_ALERT_KEY`]-marked.
536    pub(crate) fn send_internal_message_alerting(
537        &mut self,
538        recipient: &str,
539        content: String,
540        priority: MessagePriority,
541        alert: bool,
542    ) -> Result<MessageId> {
543        {
544            let state = lock_shared_state(&self.shared_state)?;
545            if state.state != ProtocolState::Running {
546                return Err(Error::NotStarted);
547            }
548        }
549
550        let mut message = if alert {
551            self.create_message_for_app(recipient, content, Some(priority), None, None)?
552        } else {
553            self.create_message(recipient, content, Some(priority), None)?
554        };
555        self.sign_control_message(&mut message)?;
556        self.dispatch_internal_message(message)
557    }
558
559    /// Sends an internal frame already sealed for `recipient` (an
560    /// `__MLS_ENC__` body), unsigned.
561    ///
562    /// Not signed because the data plane is not: a 1:1 envelope is
563    /// authenticated by MLS, and the security gate admits `__MLS_ENC__`
564    /// without a signature for exactly that reason. `reseal` carries the
565    /// plaintext so a resend after a desync re-key is sealed again against the
566    /// new epoch rather than replaying ciphertext nobody can open any more.
567    pub(super) fn send_sealed_internal_message(
568        &mut self,
569        recipient: &str,
570        sealed: String,
571        priority: MessagePriority,
572        reseal: Option<OutboxReseal>,
573    ) -> Result<MessageId> {
574        {
575            let state = lock_shared_state(&self.shared_state)?;
576            if state.state != ProtocolState::Running {
577                return Err(Error::NotStarted);
578            }
579        }
580
581        let message = self.create_message(recipient, sealed, Some(priority), None)?;
582        if let Some(reseal) = reseal {
583            self.stage_outbox_reseal(&message.id, reseal);
584        }
585        self.dispatch_internal_message(message)
586    }
587
588    /// Transmits a built internal frame: dedup, send, the success or failure
589    /// bookkeeping (outbox, ACK, retry) and the transport-switch event. Shared
590    /// by the signed and the sealed internal senders so the two cannot drift.
591    fn dispatch_internal_message(&mut self, message: Message) -> Result<MessageId> {
592        let message_id = message.id.clone();
593
594        if self.deduplicator.is_duplicate(&message_id) {
595            // Staged re-seal provenance must never outlive its message.
596            self.pending_reseal.remove(&message_id);
597            return Err(crate::Error::Other("Duplicate message".to_string()));
598        }
599
600        self.deduplicator.mark_seen_local(message_id.clone());
601
602        let previous_transport = self.transport_manager.current_transport();
603        let send_result = self.transport_manager.send(&message);
604        let current_transport = self.transport_manager.current_transport();
605
606        match send_result {
607            Ok(()) => {
608                self.handle_send_success(&message, current_transport)?;
609            }
610            Err(err) => {
611                let _ =
612                    self.handle_send_failure(&message, current_transport.or(previous_transport))?;
613                warn!(
614                    message_id = %message.id,
615                    recipient = %message.recipient,
616                    error = %err,
617                    "Internal message send failed, message deferred"
618                );
619            }
620        }
621
622        self.emit_transport_switch_event(previous_transport, current_transport)?;
623        Ok(message_id)
624    }
625
626    /// Sends a self-addressed relay *hint* frame — `__GRP_RELAY_REG__` or
627    /// `__GRP_RELAY_BCAST__` — pinned to the Internet transport, without the
628    /// ACK ladder.
629    ///
630    /// A hint frame is not traffic. The platform bridge recognizes it via
631    /// [`Self::internet_control_op`] and **replaces** it with a relay-native
632    /// frame (`CreateGroup` + member deltas, or `SendGroupMessage`), so the
633    /// frame itself never reaches a peer and no delivery ACK can ever come
634    /// back for it. Two properties follow, and both are load-bearing:
635    ///
636    /// 1. **`requires_ack = false`.** On the ordinary ladder an un-ACKable
637    ///    frame is retransmitted `DEFAULT_MAX_RETRIES` times over ~800s, each
638    ///    resend producing another full relay fan-out (under a fresh
639    ///    relay-minted id, so receiver dedup misses it), and terminates in a
640    ///    `MessageFailed` for an id the app was never told about plus a
641    ///    `record_delivery_failure` that degrades DORS' score for the very
642    ///    transport the frame requires. Opting out of the ladder is what makes
643    ///    these frames honest one-shots; retry policy for them lives at the
644    ///    application layer instead (`RelayRegisterPending` for registration,
645    ///    the caller's per-member fallback for broadcast).
646    /// 2. **Pinned to [`TransportType::Internet`].** These frames are
647    ///    self-addressed, and DORS demotes Internet below every mesh transport
648    ///    (`INTERNET_FALLBACK_DEMOTION`), so ordinary routing hands them to
649    ///    the mesh carriers first. BLE and Wi-Fi Direct fail closed (self is
650    ///    never a connected peer), but Reticulum enqueues unconditionally and
651    ///    returns `Ok`, swallowing the frame while reporting success, which on
652    ///    the broadcast path means the group message is delivered to nobody.
653    ///
654    /// Errors propagate to the caller rather than routing through
655    /// [`Self::handle_send_failure`]: a hint frame has no outbox entry to
656    /// defer into, and its callers already have a better recovery than a
657    /// retry (fall back to per-member fan-out; leave the group unregistered
658    /// for the next sync tick). The failure is also deliberately *not*
659    /// recorded against Internet's DORS metrics — "the relay bridge isn't up"
660    /// is not evidence about the transport's delivery quality.
661    ///
662    /// No `TransportChanged` event is emitted, matching
663    /// [`Self::send_delivery_ack`] — the other fire-and-forget control frame
664    /// sent over a forced transport. A hint frame is not user traffic, and
665    /// announcing a switch for one would report a transport change the app's
666    /// messages did not take.
667    ///
668    /// `alert` is false for a registration and true for a broadcast, which
669    /// carries a group chat message (see [`Self::send_internal_message_alerting`]).
670    pub(crate) fn send_relay_hint_message(
671        &mut self,
672        content: String,
673        priority: MessagePriority,
674        alert: bool,
675    ) -> Result<MessageId> {
676        {
677            let state = lock_shared_state(&self.shared_state)?;
678            if state.state != ProtocolState::Running {
679                return Err(Error::NotStarted);
680            }
681        }
682
683        let self_id = self.local_id.clone();
684        let mut message = if alert {
685            self.create_message_for_app(&self_id, content, Some(priority), None, None)?
686        } else {
687            self.create_message(&self_id, content, Some(priority), None)?
688        };
689        // Set before signing for clarity; the canonical payload covers only
690        // sender/id/recipient/content, so ordering is not security-relevant.
691        message.requires_ack = false;
692        self.sign_control_message(&mut message)?;
693        let message_id = message.id.clone();
694
695        if self.deduplicator.is_duplicate(&message_id) {
696            return Err(crate::Error::Other("Duplicate message".to_string()));
697        }
698
699        // Mark seen so a bridge that passes the frame through verbatim (an
700        // adapter without a translator) cannot have the relay echo it back
701        // into our own receive path.
702        self.deduplicator.mark_seen_local(message_id.clone());
703
704        self.transport_manager
705            .send_via_transport(&message, TransportType::Internet)?;
706
707        Ok(message_id)
708    }
709
710    /// Sends one MLS-encrypted session-confirm marker to `peer_id`.
711    ///
712    /// Used by the Welcome-adopt path: after we adopt the peer's `session:` group
713    /// (and confirm our own side), the peer may be the both-create "owner" that
714    /// kept its own group and confirms ONLY on a group-aware decrypt from us. A
715    /// plaintext probe/ack does not count, and we may have no user traffic to
716    /// send — so without this the owner stays Pending forever and the 1:1
717    /// connection never completes. The marker is `SESSION_CONFIRM_ENCRYPTED`
718    /// wrapped in the normal `ENCRYPTED` envelope; the peer consumes it on decrypt
719    /// (never surfaced). Mirrors [`Self::send_internal_message`] but encrypts
720    /// (data-plane, so NOT signed) and never propagates errors — it is re-sent on
721    /// every received Welcome while still adopting, so a lost confirm is retried
722    /// in lockstep with the owner's Welcome retransmission.
723    pub(super) fn send_session_confirm_encrypted(&mut self, peer_id: &str) {
724        let encrypted = match self.encrypt_content_for_recipient(
725            peer_id,
726            internal_prefixes::SESSION_CONFIRM_ENCRYPTED,
727            MessagePriority::High,
728        ) {
729            Ok(content) => content,
730            Err(err) => {
731                debug!(
732                    peer = %peer_id,
733                    error = %err,
734                    "Skipped proactive session-confirm (encryption not ready); welcome retry remains"
735                );
736                return;
737            }
738        };
739
740        let message =
741            match self.create_message(peer_id, encrypted, Some(MessagePriority::High), None) {
742                Ok(message) => message,
743                Err(err) => {
744                    warn!(peer = %peer_id, error = %err, "Failed to build session-confirm message");
745                    return;
746                }
747            };
748
749        if self.deduplicator.is_duplicate(&message.id) {
750            return;
751        }
752        self.deduplicator.mark_seen_local(message.id.clone());
753
754        let previous_transport = self.transport_manager.current_transport();
755        match self.transport_manager.send(&message) {
756            Ok(()) => {
757                let current_transport = self.transport_manager.current_transport();
758                let _ = self.handle_send_success(&message, current_transport);
759                info!(peer = %peer_id, "Sent proactive encrypted session-confirm to owner");
760            }
761            Err(err) => {
762                let current_transport = self.transport_manager.current_transport();
763                let _ =
764                    self.handle_send_failure(&message, current_transport.or(previous_transport));
765                debug!(
766                    peer = %peer_id,
767                    error = %err,
768                    "Proactive session-confirm deferred (will retry with welcome)"
769                );
770            }
771        }
772    }
773
774    /// Sends a message via a specific transport type (bypassing DORS).
775    pub fn send_message_via_transport(
776        &mut self,
777        recipient: impl Into<String>,
778        content: impl Into<String>,
779        priority: Option<MessagePriority>,
780        transport: TransportType,
781        reply_to_msg: Option<impl Into<String>>,
782    ) -> Result<MessageId> {
783        self.send_message_with(
784            recipient,
785            content,
786            SendMessageOptions {
787                priority,
788                reply_to_msg: reply_to_msg.map(Into::into),
789                via_transport: Some(transport),
790                ..Default::default()
791            },
792        )
793    }
794
795    /// Rejects a recipient token that is not a well-formed [`UserId`].
796    ///
797    /// Enforced at every outbound boundary so an app-owned placeholder (an
798    /// `unresolved:token`, an empty string) cannot become durable protocol
799    /// state — an outbox entry or pending-queue record that is retried, stored,
800    /// and restored forever against an address no transport can ever resolve.
801    pub(crate) fn validate_outbound_recipient(recipient: &str) -> Result<()> {
802        UserId::new(recipient)
803            .map(|_| ())
804            .map_err(|error| Error::InvalidArgument(format!("Invalid recipient user ID: {error}")))
805    }
806
807    /// Creates a new message stamped with the configured app id.
808    ///
809    /// Every engine-originated frame goes through here: control traffic,
810    /// key packages, Welcomes, data-sync frames and forwards. Those belong
811    /// to the engine, not to an application, so they always carry
812    /// [`crate::ProtocolConfig::app_id`]. Only an application's own send
813    /// can name another id, through [`Self::create_message_for_app`].
814    ///
815    /// For the same reason every frame built here carries
816    /// [`crate::constants::NO_ALERT_KEY`]: nothing the user wrote is in it,
817    /// so the relay must not push "new message" for it. User content is
818    /// built through [`Self::create_message_for_app`], which does not mark.
819    pub(super) fn create_message(
820        &mut self,
821        recipient: impl Into<String>,
822        content: impl Into<String>,
823        priority: Option<MessagePriority>,
824        reply_to_msg: Option<MessageId>,
825    ) -> Result<Message> {
826        let mut message =
827            self.create_message_for_app(recipient, content, priority, reply_to_msg, None)?;
828        message
829            .metadata
830            .insert(crate::constants::NO_ALERT_KEY.to_string(), "1".to_string());
831        Ok(message)
832    }
833
834    /// Validates a per-send app id from the rich send surface.
835    ///
836    /// Same rules as the configured id and the wire decoder (`AppId::new`),
837    /// reported as `InvalidArgument` because it is a call argument, not
838    /// configuration.
839    pub(super) fn parse_send_app_id(app_id: Option<&str>) -> Result<Option<AppId>> {
840        app_id
841            .map(AppId::new)
842            .transpose()
843            .map_err(|e| Error::InvalidArgument(format!("Invalid app_id: {e}")))
844    }
845
846    /// Like [`Self::create_message`], stamping `app_id` when given and the
847    /// configured id otherwise, and without the no-alert marker: this is the
848    /// builder for user content, which the relay should push.
849    pub(super) fn create_message_for_app(
850        &mut self,
851        recipient: impl Into<String>,
852        content: impl Into<String>,
853        priority: Option<MessagePriority>,
854        reply_to_msg: Option<MessageId>,
855        app_id: Option<AppId>,
856    ) -> Result<Message> {
857        let sender = UserId::new(&self.local_id)?;
858        let recipient = UserId::new(recipient)?;
859        let app_id = match app_id {
860            Some(app_id) => app_id,
861            None => AppId::new(&self.config.app_id)?,
862        };
863
864        let clock_value = self.lamport_clock.tick();
865        self.persist_lamport_clock();
866
867        let mut builder = Message::builder(sender, recipient, app_id)
868            .content(content)
869            .priority(priority.unwrap_or(MessagePriority::Medium))
870            .ttl(TTL::new(self.config.initial_ttl)?)
871            .lamport_clock(clock_value);
872
873        if let Some(reply_to) = reply_to_msg {
874            builder = builder.reply_to_msg(reply_to);
875        }
876
877        Ok(builder.build())
878    }
879
880    // ========================================================================
881    // ENCRYPTION
882    // ========================================================================
883
884    pub(super) fn encrypt_content_for_recipient(
885        &mut self,
886        recipient: &str,
887        content: &str,
888        _priority: MessagePriority,
889    ) -> Result<String> {
890        let encrypted = self.encrypt_bytes_for_recipient(recipient, content.as_bytes())?;
891        self.seal_encrypted_content(recipient, &encrypted)
892    }
893
894    /// Bytes-level core of [`Self::encrypt_content_for_recipient`]: initiates
895    /// session establishment when a key package is available and encrypts once
896    /// the session is confirmed. Text content and media chunk plaintexts share
897    /// this path.
898    pub(super) fn encrypt_bytes_for_recipient(
899        &mut self,
900        recipient: &str,
901        plaintext: &[u8],
902    ) -> Result<EncryptedMessage> {
903        // Clone the Arc to avoid borrow issues
904        let mls = self.mls_manager.clone().ok_or_else(|| {
905            self.emit_mls_session_missing(
906                Some(recipient),
907                None,
908                MlsOperationContext::SessionLookup,
909                MlsErrorCategory::NotInitialized,
910            );
911            Error::MlsNotInitialized
912        })?;
913
914        // Fast path: if the session is already confirmed (in-memory cache hit),
915        // skip the has_session() storage call — we know the session exists.
916        if self.confirmed_sessions.contains(recipient) {
917            return self.encrypt_bytes_confirmed_session(&mls, recipient, plaintext);
918        }
919
920        // Every caller is a text send, whose message queues behind the
921        // Welcome; the session-confirm marker also lands here, but only
922        // after adopting a session, so it never creates one.
923        self.ensure_session_establishment(&mls, recipient, true)?;
924
925        // Only encrypt if session is confirmed (Welcome processed or successful decrypt).
926        // Confirmation truth comes from persisted session state.
927        if !self.is_session_confirmed(recipient)? {
928            debug!(recipient = %recipient, "Session exists but not confirmed, queuing message");
929            return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
930        }
931
932        // Encrypt the message
933        let encrypted = {
934            let manager = mls
935                .read()
936                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
937            manager
938                .encrypt_for_user(recipient, plaintext)
939                .map_err(|_| Error::EncryptFailed("encryption operation failed".to_string()))?
940        };
941
942        self.emit_mls_encryption_used(recipient);
943        Ok(encrypted)
944    }
945
946    /// Ensures MLS session establishment with `recipient` has at least been
947    /// initiated: when no session exists in storage, imports a stored/pending
948    /// key package, creates the session, and sends the Welcome.
949    ///
950    /// Returns `Ok(())` when a session already exists (possibly unconfirmed —
951    /// callers must still gate on `is_session_confirmed`) or when no usable
952    /// key package remains (expired). Returns `Err(SessionNotReady)` when no
953    /// key package is available, or right after creating a session with
954    /// `store_pending` enabled (the session cannot be confirmed yet).
955    ///
956    /// `user_send` is whether something the user sent prompted this, and so
957    /// whether a Welcome it sends may alert (see
958    /// [`Self::send_welcome_message`]).
959    pub(super) fn ensure_session_establishment(
960        &mut self,
961        mls: &Arc<RwLock<MlsManager>>,
962        recipient: &str,
963        user_send: bool,
964    ) -> Result<()> {
965        // Check for existing session (requires storage I/O via load_group)
966        let has_session = {
967            let manager = mls
968                .read()
969                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
970            manager.has_session(recipient)?
971        };
972
973        if !has_session {
974            // Try loading key package from storage (e.g. after restart) then create session from memory
975            self.try_load_key_package_from_storage_into_memory(recipient);
976            // Try to create session from stored key package
977            // Clone first, only remove after all operations succeed to avoid losing the key package on failure
978            if let Some(received_pkg) = self.pending_key_packages.get(recipient).cloned() {
979                // Check if key package has expired (using local clock)
980                let now_ms = Utc::now().timestamp_millis() as u64;
981                if now_ms >= received_pkg.local_expires_at_ms {
982                    warn!(recipient = %recipient, "Received key package has expired, discarding");
983                    self.pending_key_packages.remove(recipient);
984                    self.delete_peer_key_package_from_storage(recipient);
985                } else {
986                    self.import_pending_key_package(
987                        mls,
988                        recipient,
989                        &received_pkg.key_package_data,
990                    )?;
991
992                    // Create session and send welcome message
993                    let welcome = {
994                        let manager = mls
995                            .read()
996                            .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
997                        manager.create_session(recipient)?
998                    };
999                    // The handshake starts here, and this is the branch it
1000                    // usually starts on: with `auto_key_exchange` the peer's
1001                    // key package has normally arrived already, so the `else`
1002                    // below never runs and no `session_missing` is raised.
1003                    // Without this emit the latency percentile would see only
1004                    // the handshakes that had to wait for a key package.
1005                    self.emit_mls_session_establishing(
1006                        recipient,
1007                        welcome.group_id.as_str(),
1008                        MlsOperationContext::Welcome,
1009                    );
1010                    self.mark_encryption_capable(recipient);
1011
1012                    // All operations succeeded, now safe to remove the key package
1013                    self.pending_key_packages.remove(recipient);
1014                    self.delete_peer_key_package_from_storage(recipient);
1015
1016                    let group_id = welcome.group_id.as_str().to_string();
1017                    let is_session = group_id.starts_with("session:");
1018
1019                    if let Err(err) =
1020                        self.ensure_session_state_entry(recipient, "session_created_local")
1021                    {
1022                        warn!(
1023                            recipient = %recipient,
1024                            error = %err,
1025                            "Failed to persist pending session state"
1026                        );
1027                    }
1028
1029                    let welcome_sent = self.send_welcome_message(recipient, &welcome, user_send)?;
1030
1031                    debug!(
1032                        recipient = %recipient,
1033                        group_id = %group_id,
1034                        welcome_sent = welcome_sent,
1035                        "Created MLS session and scheduled welcome lifecycle"
1036                    );
1037
1038                    if welcome_sent {
1039                        debug!(recipient = %recipient, group_id = %group_id, is_session, "Welcome synchronously sent");
1040                    }
1041
1042                    // Don't encrypt immediately after creating session.
1043                    // Queue message until session is confirmed (peer processes our Welcome
1044                    // and we successfully decrypt their first message, or we receive their Welcome).
1045                    // This avoids race conditions where both peers create sessions.
1046                    if self.config.encryption.store_pending {
1047                        return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1048                    }
1049                }
1050            } else {
1051                // No key package available (memory nor storage)
1052                self.emit_mls_session_missing(
1053                    Some(recipient),
1054                    None,
1055                    MlsOperationContext::SessionLookup,
1056                    MlsErrorCategory::SessionStateMissing,
1057                );
1058                return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1059            }
1060        }
1061
1062        Ok(())
1063    }
1064
1065    /// Serializes an [`EncryptedMessage`] into the prefixed string form used
1066    /// by the text path.
1067    ///
1068    /// Recipients that advertised [`MLS_ENVELOPE_COMPACT_V1`] in their key
1069    /// package (with our own `compact_envelope_enabled` on) get the compact
1070    /// envelope — `__MLS_ENC__` + base64 of [`EncryptedMessage::to_bytes`] —
1071    /// roughly 2.7x smaller than the legacy JSON form, whose `ciphertext`
1072    /// field renders as an integer array. Everyone else gets `__MLS_ENC__` +
1073    /// JSON, the permanent floor. Receivers sniff the byte after the prefix
1074    /// (`{` = JSON), so no per-message signaling is needed, and the choice is
1075    /// per-recipient end-to-end state, valid however the message is later
1076    /// routed or retried.
1077    pub(super) fn seal_encrypted_content(
1078        &self,
1079        recipient: &str,
1080        encrypted: &EncryptedMessage,
1081    ) -> Result<String> {
1082        if self.config.encryption.compact_envelope_enabled
1083            && self.peer_compact_envelope.contains(recipient)
1084        {
1085            return Ok(format!(
1086                "{}{}",
1087                internal_prefixes::ENCRYPTED,
1088                base64_encode(&encrypted.to_bytes())
1089            ));
1090        }
1091        let serialized =
1092            serde_json::to_string(encrypted).map_err(|e| Error::Serialization(e.to_string()))?;
1093        Ok(format!("{}{}", internal_prefixes::ENCRYPTED, serialized))
1094    }
1095
1096    /// The sync versions this device advertises.
1097    ///
1098    /// Empty unless the data layer is both compiled in and switched on. A
1099    /// capability advertised with nothing behind it is worse than silence:
1100    /// the peer starts sending frames and waiting for answers that never
1101    /// come, and nothing in that exchange looks like an error to either
1102    /// side.
1103    pub(crate) fn advertised_data_versions(&self) -> Vec<u8> {
1104        #[cfg(feature = "data")]
1105        {
1106            if self.config.data.enabled {
1107                return vec![
1108                    super::types::DATA_SYNC_V1,
1109                    super::types::DATA_GROUP_V1,
1110                    super::types::DATA_MEDIA_V1,
1111                    super::types::DATA_TOMBSTONE_V1,
1112                    super::types::DATA_INTEREST_V1,
1113                    super::types::DATA_GROUP_BLOB_V1,
1114                    super::types::DATA_CUSTODY_V1,
1115                ];
1116            }
1117        }
1118        Vec::new()
1119    }
1120
1121    /// Whether document sync is live toward `recipient`: the layer is
1122    /// compiled in and enabled here, the peer is not blocked, and they
1123    /// advertised [`DATA_SYNC_V1`] in their key package.
1124    ///
1125    /// Consulted per send rather than cached at session establishment, like
1126    /// the two capability gates below it: a peer that downgrades stops
1127    /// receiving frames on their next key package, not at their next
1128    /// session.
1129    ///
1130    /// The block check is here rather than at the call sites because this is
1131    /// the only gate they share, and because replication is not reached
1132    /// through a send API: every public send surface checks blocking for
1133    /// itself, while these frames are triggered by a commit or a discovery.
1134    /// A blocked peer still holds a live session, so nothing further down
1135    /// would have refused the frame. Inbound needs no equivalent: the
1136    /// receive path drops a blocked sender before any dispatch.
1137    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1138    pub(super) fn data_sync_active(&self, recipient: &str) -> bool {
1139        #[cfg(feature = "data")]
1140        {
1141            self.config.data.enabled
1142                && self.peer_data_sync.contains(recipient)
1143                && !self.is_user_blocked(recipient)
1144        }
1145        #[cfg(not(feature = "data"))]
1146        {
1147            let _ = recipient;
1148            false
1149        }
1150    }
1151
1152    /// Whether blobs may travel to `recipient`: 1:1 replication is live with
1153    /// them and they advertised [`DATA_MEDIA_V1`], so they understand the
1154    /// fetch frames and route a data-purposed media transfer into the data
1155    /// layer rather than to their user.
1156    ///
1157    /// Strictly narrower than [`Self::data_sync_active`] and checked in
1158    /// addition to it, never instead: every blob exchange is part of a
1159    /// replication that the first gate already had to allow, and a peer who
1160    /// stops replicating stops receiving blobs at the same moment.
1161    ///
1162    /// [`DATA_MEDIA_V1`]: crate::protocol::types::DATA_MEDIA_V1
1163    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1164    pub(super) fn data_media_active(&self, recipient: &str) -> bool {
1165        #[cfg(feature = "data")]
1166        {
1167            self.data_sync_active(recipient) && self.peer_data_media.contains(recipient)
1168        }
1169        #[cfg(not(feature = "data"))]
1170        {
1171            let _ = recipient;
1172            false
1173        }
1174    }
1175
1176    /// Whether removals may be carried toward `recipient`: 1:1 replication
1177    /// is live with them and they advertised [`DATA_TOMBSTONE_V1`], so they
1178    /// read the `gone` field rather than ignoring it.
1179    ///
1180    /// Unlike the two gates above it, this one guards traffic rather than
1181    /// correctness. A peer without the entry ignores the field, keeps its
1182    /// copy and re-offers it; this device refuses the offer and any blob
1183    /// behind it against its own floor, so the removal holds here either
1184    /// way. Sending the field anyway would be harmless and useless, and
1185    /// leaving it out keeps that exchange as small as it can be.
1186    ///
1187    /// [`DATA_TOMBSTONE_V1`]: crate::protocol::types::DATA_TOMBSTONE_V1
1188    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1189    pub(super) fn data_tombstones_active(&self, recipient: &str) -> bool {
1190        #[cfg(feature = "data")]
1191        {
1192            self.data_sync_active(recipient) && self.peer_data_tombstones.contains(recipient)
1193        }
1194        #[cfg(not(feature = "data"))]
1195        {
1196            let _ = recipient;
1197            false
1198        }
1199    }
1200
1201    /// Whether a narrowed interest may be declared toward `recipient`: 1:1
1202    /// replication is live with them and they advertised
1203    /// [`DATA_INTEREST_V1`], so they answer inside it rather than ignoring
1204    /// it.
1205    ///
1206    /// A traffic gate, like the one above it. A peer without the entry sends
1207    /// everything it holds and this device refuses what it did not ask for,
1208    /// so a narrowed space is narrow either way; what the entry buys is the
1209    /// bytes never leaving the peer.
1210    ///
1211    /// [`DATA_INTEREST_V1`]: crate::protocol::types::DATA_INTEREST_V1
1212    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1213    pub(super) fn data_interest_active(&self, recipient: &str) -> bool {
1214        #[cfg(feature = "data")]
1215        {
1216            self.data_sync_active(recipient) && self.peer_data_interest.contains(recipient)
1217        }
1218        #[cfg(not(feature = "data"))]
1219        {
1220            let _ = recipient;
1221            false
1222        }
1223    }
1224
1225    /// Whether `peer` is known to intercept replication frames inside a
1226    /// group ([`DATA_GROUP_V1`]), either directly or by an inviter's
1227    /// attestation.
1228    ///
1229    /// Positive knowledge, and the opposite rule to its blob sibling below,
1230    /// because it answers a different question. This one decides whether a
1231    /// `__DATA_V1__` frame may be *put in front of* a member: one that does
1232    /// not intercept them renders the frame to its user as literal text, so
1233    /// an unknown member closes this and an application is told at the call.
1234    /// The blob sibling decides whether a member can usefully answer a
1235    /// question they will certainly receive, where the honest answer to "we
1236    /// have not heard" is to ask.
1237    ///
1238    /// The roster-wide gate is this predicate over every member, which is
1239    /// not a coincidence to be refactored apart: a directed frame can be
1240    /// promoted to a roster-wide delivery, so the two must agree about what
1241    /// a member has to be known to do before a frame reaches them.
1242    ///
1243    /// [`DATA_GROUP_V1`]: crate::protocol::types::DATA_GROUP_V1
1244    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1245    pub(super) fn data_group_frames_known_received(&self, peer: &str) -> bool {
1246        #[cfg(feature = "data")]
1247        {
1248            self.peer_data_group.contains(peer) || self.peer_data_group_attested.contains(peer)
1249        }
1250        #[cfg(not(feature = "data"))]
1251        {
1252            let _ = peer;
1253            false
1254        }
1255    }
1256
1257    /// Whether `peer` is *known* not to carry attachment bytes inside a
1258    /// group.
1259    ///
1260    /// Knowledge, never its absence. Members of a group do not exchange key
1261    /// packages with each other, so "we have not heard" is the ordinary case
1262    /// and must not read as "they cannot": that would refuse every fetch put
1263    /// to a member this device has never dealt with directly. An inviter's
1264    /// attestation fills the gap where there is one.
1265    ///
1266    /// [`DATA_GROUP_BLOB_V1`]: crate::protocol::types::DATA_GROUP_BLOB_V1
1267    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1268    pub(super) fn data_group_blob_known_absent(&self, peer: &str) -> bool {
1269        #[cfg(feature = "data")]
1270        {
1271            // Any direct knowledge of what this peer replicates, not just
1272            // the first entry. A key package carries the whole list at
1273            // once, so a peer in one of these sets is one whose
1274            // advertisement this device has actually read, and the blob
1275            // entry's absence from it is knowledge rather than silence.
1276            // Reading entry 1 alone would make a peer known only by a
1277            // later entry look like one never heard from, and the refusal
1278            // this predicate exists to make would never fire for them.
1279            let heard_directly =
1280                self.peer_data_sync.contains(peer) || self.peer_data_group.contains(peer);
1281            let attested = self.peer_data_group_blob_attested.contains(peer);
1282            let carries = self.peer_data_group_blob.contains(peer) || attested;
1283            (heard_directly || attested) && !carries
1284        }
1285        #[cfg(not(feature = "data"))]
1286        {
1287            let _ = peer;
1288            false
1289        }
1290    }
1291
1292    /// Whether rich extras seal for `recipient`: our own kill switch is on
1293    /// and the peer advertised [`RICH_PAYLOAD_V1`] in their key package.
1294    pub(super) fn rich_seal_active(&self, recipient: &str) -> bool {
1295        self.config.encryption.rich_payload_enabled && self.peer_rich_payload.contains(recipient)
1296    }
1297
1298    /// Whether rich extras may seal into a group message: our own kill
1299    /// switch is on and every non-self member is known to parse
1300    /// [`RICH_PAYLOAD_V1`] — either self-advertised in a directly received
1301    /// key package (`peer_rich_payload`) or attested by a group inviter on
1302    /// the Add commit / Welcome (`peer_rich_attested`; members added by
1303    /// someone else never exchange key packages with us directly). Group
1304    /// MLS encryption produces a single ciphertext for all members, so one
1305    /// non-capable or capability-unknown member forces the extras to drop
1306    /// for the whole group. Conservative by design: an unknown member must
1307    /// never receive a sealed body their SDK would render as literal
1308    /// `__RICH_V1__` JSON.
1309    ///
1310    /// Attestation is third-party and may be stale or forged by a rogue
1311    /// admin; the worst case is exactly the degraded display below, never a
1312    /// leak, and direct contact evicts the attested entry. Best-effort
1313    /// beyond that: callers pass the `group_mesh.members` fan-out cache,
1314    /// which trails MLS membership until the add/remove notification
1315    /// lands. In that window a just-added non-capable member can receive
1316    /// one sealed body and render it as literal JSON — degraded display,
1317    /// not a leak (every group member is entitled to the sealed contents).
1318    pub(crate) fn group_rich_seal_active(&self, members: &[String]) -> bool {
1319        self.config.encryption.rich_payload_enabled
1320            && members
1321                .iter()
1322                .filter(|m| m.as_str() != self.local_id)
1323                .all(|m| {
1324                    self.peer_rich_payload.contains(m.as_str())
1325                        || self.peer_rich_attested.contains(m.as_str())
1326                })
1327    }
1328
1329    /// Group members (non-self) not known to parse the sealed rich payload
1330    /// — neither directly advertised nor inviter-attested. These are the
1331    /// members that hold `group_rich_seal_active` closed; the set absence
1332    /// cannot distinguish "never heard from" from "known not capable".
1333    pub(crate) fn group_rich_unknown_members(&self, members: &[String]) -> Vec<String> {
1334        members
1335            .iter()
1336            .filter(|m| {
1337                m.as_str() != self.local_id
1338                    && !self.peer_rich_payload.contains(m.as_str())
1339                    && !self.peer_rich_attested.contains(m.as_str())
1340            })
1341            .cloned()
1342            .collect()
1343    }
1344
1345    /// The rich-payload versions we can attest for a peer when inviting it
1346    /// into (or welcoming it to) a group: [`RICH_PAYLOAD_V1`] when the peer
1347    /// is known capable (directly or itself attested — attestation chains
1348    /// transitively, which is how knowledge reaches members several adds
1349    /// removed from any direct exchange), `None` when unknown. `None` is
1350    /// deliberately not a downgrade signal: absence of knowledge must never
1351    /// evict what another member learned first-hand.
1352    pub(crate) fn attestable_rich_versions(&self, peer_id: &str) -> Option<Vec<u8>> {
1353        (self.peer_rich_payload.contains(peer_id) || self.peer_rich_attested.contains(peer_id))
1354            .then(|| vec![RICH_PAYLOAD_V1])
1355    }
1356
1357    /// Whether documents may replicate into a group: the layer is enabled
1358    /// here and every non-self member is known to intercept group sync
1359    /// frames — either self-advertised in a directly received key package
1360    /// (`peer_data_group`) or attested by a group inviter on the Add commit
1361    /// / Welcome (`peer_data_group_attested`).
1362    ///
1363    /// All-members rather than per-member, and that is not a convenience.
1364    /// Group MLS encryption produces one ciphertext for everyone, so a
1365    /// frame sent for the capable members is also delivered to the rest;
1366    /// one member that cannot intercept it renders it to its user as
1367    /// literal `__DATA_V1__` text. The gate is therefore closed by a single
1368    /// unknown member, which is the same conservatism
1369    /// [`Self::group_rich_seal_active`] applies to sealed rich bodies, for
1370    /// the same reason and with a worse symptom if skipped.
1371    ///
1372    /// Blocking is deliberately not consulted here. A blocked *peer* is a
1373    /// 1:1 relationship; a group send is one ciphertext to a roster, and
1374    /// dropping the whole group's replication because one member is blocked
1375    /// would be a surprising amount of collateral. The 1:1 gate keeps its
1376    /// block check because there the recipient and the relationship are the
1377    /// same thing.
1378    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1379    pub(crate) fn group_data_sync_active(&self, members: &[String]) -> bool {
1380        #[cfg(feature = "data")]
1381        {
1382            self.config.data.enabled
1383                && members
1384                    .iter()
1385                    .filter(|m| m.as_str() != self.local_id)
1386                    .all(|m| self.data_group_frames_known_received(m.as_str()))
1387        }
1388        #[cfg(not(feature = "data"))]
1389        {
1390            let _ = members;
1391            false
1392        }
1393    }
1394
1395    /// Group members (non-self) not known to intercept group sync frames.
1396    ///
1397    /// These are the members holding [`Self::group_data_sync_active`]
1398    /// closed. Handed to [`Self::backfill_group_rich_capabilities`], whose
1399    /// probe is capability-agnostic: it sends our key package so the peer's
1400    /// reply teaches us everything they advertise at once.
1401    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1402    pub(crate) fn group_data_unknown_members(&self, members: &[String]) -> Vec<String> {
1403        #[cfg(feature = "data")]
1404        {
1405            members
1406                .iter()
1407                .filter(|m| {
1408                    m.as_str() != self.local_id
1409                        && !self.peer_data_group.contains(m.as_str())
1410                        && !self.peer_data_group_attested.contains(m.as_str())
1411                })
1412                .cloned()
1413                .collect()
1414        }
1415        #[cfg(not(feature = "data"))]
1416        {
1417            let _ = members;
1418            Vec::new()
1419        }
1420    }
1421
1422    /// The sync versions we can attest for a peer when inviting it into (or
1423    /// welcoming it to) a group.
1424    ///
1425    /// [`DATA_GROUP_V1`] when the peer is known group-capable, directly or
1426    /// itself attested — attestation chains transitively, which is how
1427    /// knowledge reaches members several adds removed from any direct
1428    /// exchange. `None` when unknown, and `None` is deliberately not a
1429    /// downgrade signal: absence of knowledge must never evict what another
1430    /// member learned first-hand.
1431    ///
1432    /// [`DATA_GROUP_V1`]: crate::protocol::types::DATA_GROUP_V1
1433    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1434    pub(crate) fn attestable_data_versions(&self, peer_id: &str) -> Option<Vec<u8>> {
1435        #[cfg(feature = "data")]
1436        {
1437            // Every group-relevant entry this device knows about the peer,
1438            // rather than the one it used to be. An inviter is the only
1439            // source a member has for another member, so anything it leaves
1440            // out is something nobody else can supply.
1441            let mut versions = Vec::new();
1442            if self.peer_data_group.contains(peer_id)
1443                || self.peer_data_group_attested.contains(peer_id)
1444            {
1445                versions.push(super::types::DATA_GROUP_V1);
1446            }
1447            if self.peer_data_group_blob.contains(peer_id)
1448                || self.peer_data_group_blob_attested.contains(peer_id)
1449            {
1450                versions.push(super::types::DATA_GROUP_BLOB_V1);
1451            }
1452            // Entry 2 is what makes a member reachable at all, so an
1453            // attestation without it says nothing usable and is not sent.
1454            versions
1455                .first()
1456                .is_some_and(|first| *first == super::types::DATA_GROUP_V1)
1457                .then_some(versions)
1458        }
1459        #[cfg(not(feature = "data"))]
1460        {
1461            let _ = peer_id;
1462            None
1463        }
1464    }
1465
1466    /// Best-effort capability backfill for group members whose rich support
1467    /// is unknown (pre-attestation groups, or an attestation chain broken by
1468    /// an old-SDK inviter): send them our key package so their
1469    /// `auto_key_exchange` reply teaches us theirs. Guarded by
1470    /// `key_package_sent_to`, so repeated gate-failing group sends don't
1471    /// re-probe the same peer (the only repeat is
1472    /// `rearm_key_package_for_peer`, on its doubling backoff); a peer that
1473    /// never replies is either
1474    /// unreachable, old-SDK, or opted out — all of which correctly leave
1475    /// the gate closed.
1476    pub(crate) fn backfill_group_rich_capabilities(&mut self, unknown_members: &[String]) {
1477        for member in unknown_members {
1478            if self.key_package_sent_to.contains_key(member.as_str()) {
1479                continue;
1480            }
1481            if let Err(e) = self.send_key_package_to(member, false) {
1482                debug!(
1483                    member = %member,
1484                    error = %e,
1485                    "Rich-capability backfill key package deferred (peer unreachable)"
1486                );
1487            }
1488        }
1489    }
1490
1491    /// Wraps plaintext content and rich extras into the sealed
1492    /// `__RICH_V1__`-prefixed JSON body ([`RichPayloadV1`]). Callers must
1493    /// only invoke this on a path that MLS-encrypts the result — the sealed
1494    /// body is the sole carrier for reply context and media secrets and must
1495    /// never leave as cleartext.
1496    ///
1497    /// The outer `content_type` hint is always copied into the body (not
1498    /// just when non-Text) so the receiver can treat the sealed copy as
1499    /// authoritative and a relay cannot rewrite the hint in either
1500    /// direction on a rich message.
1501    pub(crate) fn seal_rich_payload(
1502        content: &str,
1503        extras: &RichSendExtras,
1504        content_type: ContentType,
1505    ) -> Result<String> {
1506        let payload = RichPayloadV1 {
1507            text: content.to_string(),
1508            reply_context: extras.reply_context.clone(),
1509            media_metadata: extras.media_metadata.clone(),
1510            forward_info: extras.forward_info.clone(),
1511            content_type: Some(content_type),
1512        };
1513        let serialized =
1514            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
1515        Ok(format!("{}{}", internal_prefixes::RICH_V1, serialized))
1516    }
1517
1518    /// Encrypts a plaintext for a recipient whose session is known to be
1519    /// confirmed (in-memory cache hit). Uses `encrypt_for_existing_session` to
1520    /// skip both the external `has_session()` and the internal one inside
1521    /// `encrypt_for_user`, reducing storage I/O from 2 round-trips to 1
1522    /// (`load_group` for encrypt).
1523    ///
1524    /// Only evicts the cache on `SessionNotFound` (session deleted externally).
1525    /// Transient errors (crypto, storage I/O) propagate without cache eviction
1526    /// so the fast path is preserved for the next attempt.
1527    pub(super) fn encrypt_bytes_confirmed_session(
1528        &mut self,
1529        mls: &Arc<RwLock<MlsManager>>,
1530        recipient: &str,
1531        plaintext: &[u8],
1532    ) -> Result<EncryptedMessage> {
1533        let encrypt_result = {
1534            let manager = mls
1535                .read()
1536                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
1537            manager.encrypt_for_existing_session(recipient, plaintext)
1538        };
1539
1540        let encrypted = match encrypt_result {
1541            Ok(enc) => enc,
1542            Err(offline_protocol_mls::MlsError::SessionNotFound(_)) => {
1543                // Session was deleted externally — evict stale cache entry and
1544                // return SessionNotReady so the send pipeline can queue the
1545                // message (when store_pending is enabled) rather than dropping it.
1546                warn!(
1547                    recipient = %recipient,
1548                    "Confirmed session missing from MLS storage, evicting cache"
1549                );
1550                self.confirmed_sessions.remove(recipient);
1551                let state = self
1552                    .establishment_state(recipient)
1553                    .unwrap_or(crate::EstablishmentState::NoKeyPackage);
1554                return Err(Error::SessionNotReady(state));
1555            }
1556            Err(e) => {
1557                // Transient error (crypto, storage I/O, etc.) — do NOT evict
1558                // cache, the session likely still exists.
1559                return Err(Error::EncryptFailed(format!(
1560                    "encryption operation failed: {}",
1561                    e
1562                )));
1563            }
1564        };
1565
1566        self.emit_mls_encryption_used(recipient);
1567        Ok(encrypted)
1568    }
1569
1570    pub(super) fn encrypt_content_for_recipient_strict(
1571        &mut self,
1572        recipient: &str,
1573        content: &str,
1574    ) -> Result<String> {
1575        let encrypted = self.encrypt_bytes_for_recipient_strict(recipient, content.as_bytes())?;
1576        self.seal_encrypted_content(recipient, &encrypted)
1577    }
1578
1579    /// Bytes-level core of [`Self::encrypt_content_for_recipient_strict`]:
1580    /// requires an existing confirmed session and never initiates
1581    /// establishment.
1582    pub(super) fn encrypt_bytes_for_recipient_strict(
1583        &mut self,
1584        recipient: &str,
1585        plaintext: &[u8],
1586    ) -> Result<EncryptedMessage> {
1587        let mls = self.mls_manager.clone().ok_or_else(|| {
1588            self.emit_mls_session_missing(
1589                Some(recipient),
1590                None,
1591                MlsOperationContext::SessionLookup,
1592                MlsErrorCategory::NotInitialized,
1593            );
1594            Error::MlsNotInitialized
1595        })?;
1596
1597        // Fast path: if session is confirmed (in-memory cache), skip has_session() I/O
1598        if self.confirmed_sessions.contains(recipient) {
1599            return self.encrypt_bytes_confirmed_session(&mls, recipient, plaintext);
1600        }
1601
1602        let has_session = {
1603            let manager = mls
1604                .read()
1605                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
1606            manager.has_session(recipient)?
1607        };
1608
1609        if !has_session {
1610            self.try_load_key_package_from_storage_into_memory(recipient);
1611            let now_ms = Utc::now().timestamp_millis() as u64;
1612            let has_valid_key_package = match self.pending_key_packages.get(recipient) {
1613                Some(pkg) if now_ms < pkg.local_expires_at_ms => true,
1614                Some(_) => {
1615                    self.pending_key_packages.remove(recipient);
1616                    self.delete_peer_key_package_from_storage(recipient);
1617                    false
1618                }
1619                None => false,
1620            };
1621
1622            if has_valid_key_package {
1623                return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1624            }
1625
1626            self.emit_mls_session_missing(
1627                Some(recipient),
1628                None,
1629                MlsOperationContext::SessionLookup,
1630                MlsErrorCategory::SessionStateMissing,
1631            );
1632            return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1633        }
1634
1635        if !self.is_session_confirmed(recipient)? {
1636            return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1637        }
1638
1639        let encrypted = {
1640            let manager = mls
1641                .read()
1642                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
1643            manager
1644                .encrypt_for_user(recipient, plaintext)
1645                .map_err(|_| Error::EncryptFailed("encryption operation failed".to_string()))?
1646        };
1647
1648        self.emit_mls_encryption_used(recipient);
1649        Ok(encrypted)
1650    }
1651
1652    #[allow(clippy::too_many_arguments)]
1653    pub(super) fn prepare_outbound_content(
1654        &mut self,
1655        recipient: &str,
1656        content: &str,
1657        priority: MessagePriority,
1658        reply_to_msg_id: Option<MessageId>,
1659        forwarded_from: Option<ForwardInfo>,
1660        content_type: ContentType,
1661        media_metadata: Option<MediaMetadata>,
1662        rich: Option<&RichSendExtras>,
1663        app_id: Option<&AppId>,
1664        provenance: Option<PendingProvenance>,
1665        reconciliation_reason: &'static str,
1666    ) -> Result<OutboundSendPreparation> {
1667        if self.should_auto_encrypt() {
1668            // Rich extras travel only inside the sealed body: seal for a
1669            // capable recipient, drop for everyone else — never cleartext.
1670            // The decision is made here, at actual-send time; a message
1671            // queued behind session establishment stores the raw extras and
1672            // re-evaluates capability at flush (the key package that
1673            // confirms the session may be the one that advertised it).
1674            let rich_extras = rich.filter(|extras| extras.is_any());
1675            let sealed_body;
1676            let outbound: &str = match rich_extras {
1677                Some(extras) if self.rich_seal_active(recipient) => {
1678                    sealed_body = Self::seal_rich_payload(content, extras, content_type)?;
1679                    &sealed_body
1680                }
1681                Some(_) => {
1682                    debug!(
1683                        recipient = %recipient,
1684                        "Recipient lacks sealed rich payload capability, dropping rich extras"
1685                    );
1686                    content
1687                }
1688                // No rich extras, but a non-Text content_type toward a
1689                // capable recipient still seals a hint-only body: the outer
1690                // hint is relay-writable, and restamping it FileChunk gets
1691                // the decrypted message ACKed and then dropped by the
1692                // file-transfer manager — worse than a plain relay drop,
1693                // which at least fails the ACK and retries. Only when
1694                // nothing rides outer (`forwarded_from`/`media_metadata`
1695                // both absent): fresh forwards thread their attribution and
1696                // media metadata through `rich` above, so this outer-only
1697                // shape survives solely in messages queued by an older build
1698                // (persisted `PendingMessage`s with no `rich`), where the
1699                // receiver's wholesale sealed-body restore would wipe the
1700                // outer copies a hint-only seal can't replicate. Bare Text
1701                // seals nothing: no hint to protect, and the unsealed form
1702                // keeps the plaintext floor maximal.
1703                None if content_type != ContentType::Text
1704                    && forwarded_from.is_none()
1705                    && media_metadata.is_none()
1706                    && self.rich_seal_active(recipient) =>
1707                {
1708                    sealed_body =
1709                        Self::seal_rich_payload(content, &RichSendExtras::default(), content_type)?;
1710                    &sealed_body
1711                }
1712                None => content,
1713            };
1714            if self.config.encryption.require_encryption {
1715                match self.encrypt_content_for_recipient_strict(recipient, outbound) {
1716                    Ok(encrypted) => return Ok(OutboundSendPreparation::Ready(encrypted)),
1717                    Err(Error::SessionNotReady(_)) if self.config.encryption.store_pending => {
1718                        // Session not ready but store_pending is enabled — queue
1719                        // the message so it gets encrypted and sent once the
1720                        // session is confirmed, rather than dropping it.
1721                        //
1722                        // Kick establishment first (import a stored key package,
1723                        // create the session, send the Welcome), matching the
1724                        // non-strict and media paths: without this a peer whose
1725                        // key package arrived but whose arrival-time
1726                        // auto-establish failed would stall until the peer
1727                        // initiates. SessionNotReady from the kick is the
1728                        // expected "created, awaiting confirmation" outcome;
1729                        // the message queues regardless.
1730                        if let Some(mls) = self.mls_manager.clone() {
1731                            match self.ensure_session_establishment(&mls, recipient, true) {
1732                                Ok(()) | Err(Error::SessionNotReady(_)) => {}
1733                                Err(err) => {
1734                                    warn!(
1735                                        recipient = %recipient,
1736                                        error = %err,
1737                                        "Session establishment kick failed; message queued anyway"
1738                                    );
1739                                }
1740                            }
1741                        }
1742                        let queued_id = self.queue_message_for_session_establishment(
1743                            recipient,
1744                            content,
1745                            priority,
1746                            reply_to_msg_id,
1747                            forwarded_from,
1748                            content_type,
1749                            media_metadata,
1750                            rich_extras.cloned(),
1751                            app_id,
1752                            provenance,
1753                            reconciliation_reason,
1754                        )?;
1755                        return Ok(OutboundSendPreparation::Queued(queued_id));
1756                    }
1757                    Err(e) => return Err(e),
1758                }
1759            }
1760
1761            match self.encrypt_content_for_recipient(recipient, outbound, priority) {
1762                Ok(encrypted) => Ok(OutboundSendPreparation::Ready(encrypted)),
1763                Err(Error::SessionNotReady(state)) => {
1764                    if !self.config.encryption.store_pending {
1765                        return Err(Error::SessionNotReady(state));
1766                    }
1767
1768                    let queued_id = self.queue_message_for_session_establishment(
1769                        recipient,
1770                        content,
1771                        priority,
1772                        reply_to_msg_id,
1773                        forwarded_from,
1774                        content_type,
1775                        media_metadata,
1776                        rich_extras.cloned(),
1777                        app_id,
1778                        provenance,
1779                        reconciliation_reason,
1780                    )?;
1781                    Ok(OutboundSendPreparation::Queued(queued_id))
1782                }
1783                Err(e) => Err(e),
1784            }
1785        } else if self.config.encryption.require_encryption {
1786            Err(Error::EncryptFailed(
1787                "MLS encryption is required (the default) but MLS is not initialized — \
1788                 call initialize_mls() with secure and protocol-state storage, or explicitly opt out with \
1789                 require_encryption=false to send plaintext"
1790                    .to_string(),
1791            ))
1792        } else {
1793            // Explicit opt-out: require_encryption=false with encryption
1794            // disabled or MLS uninitialized. The message leaves as plaintext;
1795            // surface that loudly (once per peer) instead of a debug log.
1796            // Rich extras are dropped here unconditionally — they only ever
1797            // travel inside the sealed body, never as cleartext.
1798            if rich.is_some_and(|extras| extras.is_any()) {
1799                debug!(
1800                    recipient = %recipient,
1801                    "Plaintext send path, dropping rich extras (sealed-only)"
1802                );
1803            }
1804            self.warn_plaintext_send(recipient);
1805            Ok(OutboundSendPreparation::Ready(content.to_string()))
1806        }
1807    }
1808
1809    // ========================================================================
1810    // PENDING / FLUSH
1811    // ========================================================================
1812
1813    /// Enforces [`MAX_MESSAGE_CONTENT_BYTES`] on application-supplied content.
1814    ///
1815    /// Enforced at the public send boundary rather than at transmit time,
1816    /// because a message that has to wait for MLS session establishment is
1817    /// queued — in memory and on disk — long before it ever reaches the
1818    /// transport's own size check. Without this cap a handful of very large
1819    /// sends could exhaust mobile memory and protocol-state storage while
1820    /// still satisfying the pending queue's entry-count limits.
1821    ///
1822    /// Applies to the plaintext the app hands us. MLS ciphertext, base64, and
1823    /// the JSON wire envelope expand it further, which is why the cap sits
1824    /// well under the transport's 1 MiB ceiling: everything accepted here can
1825    /// actually be delivered.
1826    ///
1827    /// Deliberately *not* applied to the group surface: a group send has no
1828    /// durable pre-session queue behind it (there is no per-recipient MLS
1829    /// session to wait on), so it is bounded by the transport alone — the
1830    /// documented "no send-side limit" behavior that
1831    /// `test_group_mls_send_large_content_no_send_side_limit` pins.
1832    pub(crate) fn check_content_size(content: &str) -> Result<()> {
1833        if content.len() > MAX_MESSAGE_CONTENT_BYTES {
1834            return Err(Error::InvalidArgument(format!(
1835                "Message content too large: {} bytes (max {}); use send_media for large payloads",
1836                content.len(),
1837                MAX_MESSAGE_CONTENT_BYTES
1838            )));
1839        }
1840        Ok(())
1841    }
1842
1843    #[allow(clippy::too_many_arguments)]
1844    pub(super) fn queue_message_for_session_establishment(
1845        &mut self,
1846        recipient: &str,
1847        content: &str,
1848        priority: MessagePriority,
1849        reply_to_msg_id: Option<MessageId>,
1850        forwarded_from: Option<ForwardInfo>,
1851        content_type: ContentType,
1852        media_metadata: Option<MediaMetadata>,
1853        rich: Option<RichSendExtras>,
1854        app_id: Option<&AppId>,
1855        provenance: Option<PendingProvenance>,
1856        reconciliation_reason: &'static str,
1857    ) -> Result<MessageId> {
1858        // Fresh sends mint an ID without ticking the Lamport clock — the
1859        // real tick happens when flush_pending_messages re-sends after the
1860        // session is established. A flush-time re-queue passes the id the
1861        // caller already holds so Deferred/Sent/Delivered stay correlatable,
1862        // and the timestamp it first entered the queue so the absolute
1863        // pending lifetime is not renewed by the round trip.
1864        let (message_id, first_queued_at) = match provenance {
1865            Some(provenance) => (provenance.message_id, provenance.first_queued_at),
1866            None => (MessageId::default(), None),
1867        };
1868
1869        debug!(
1870            recipient = %recipient,
1871            message_id = %message_id,
1872            "Message queued pending session establishment"
1873        );
1874        self.queue_pending_message_at(
1875            recipient,
1876            content,
1877            priority,
1878            message_id.clone(),
1879            reply_to_msg_id,
1880            forwarded_from,
1881            content_type,
1882            media_metadata,
1883            rich,
1884            app_id.map(|id| id.as_str().to_string()),
1885            first_queued_at.unwrap_or_else(Utc::now),
1886        );
1887        self.kick_pending_session_reconciliation(reconciliation_reason);
1888        if self.has_terminal_welcome_failure(recipient) {
1889            self.abort_pending_session_for_peer(
1890                recipient,
1891                crate::events::WelcomeReasonCode::RetryExhausted,
1892            );
1893            return Err(Error::Other(format!(
1894                "Welcome delivery failed for {}",
1895                recipient
1896            )));
1897        }
1898
1899        Ok(message_id)
1900    }
1901
1902    /// Queues a message with a specific message ID for later sending when
1903    /// session is established, starting a fresh pending lifetime.
1904    ///
1905    /// A re-queue must go through [`Self::queue_pending_message_at`] with the
1906    /// entry's original timestamp instead — which is why production code always
1907    /// calls that one directly (through
1908    /// `queue_message_for_session_establishment`, where the distinction is
1909    /// made) and this convenience exists only for tests.
1910    #[cfg(test)]
1911    #[allow(clippy::too_many_arguments)]
1912    pub(super) fn queue_pending_message(
1913        &mut self,
1914        recipient: &str,
1915        content: &str,
1916        priority: MessagePriority,
1917        message_id: MessageId,
1918        reply_to_msg: Option<MessageId>,
1919        forwarded_from: Option<ForwardInfo>,
1920        content_type: ContentType,
1921        media_metadata: Option<MediaMetadata>,
1922        rich: Option<RichSendExtras>,
1923    ) {
1924        self.queue_pending_message_at(
1925            recipient,
1926            content,
1927            priority,
1928            message_id,
1929            reply_to_msg,
1930            forwarded_from,
1931            content_type,
1932            media_metadata,
1933            rich,
1934            None,
1935            Utc::now(),
1936        );
1937    }
1938
1939    /// Queues a message with an explicit first-queued timestamp.
1940    ///
1941    /// `queued_at` is the instant the message *first* entered the pending
1942    /// queue, which is what `pending_message_max_lifetime_ms` is measured from.
1943    /// Re-queueing an entry (a flush that found the session still unavailable)
1944    /// must pass the original value: stamping `now()` there would hand the
1945    /// message a fresh window on every failed reconciliation, so a message
1946    /// could outlive an "absolute" lifetime indefinitely.
1947    ///
1948    /// Admission is bounded on two axes — entry count and serialized bytes,
1949    /// per peer and globally — because message content is
1950    /// application-supplied and a count alone bounds neither memory nor
1951    /// durable storage. Both axes evict oldest-first and settle each evicted
1952    /// message as failed.
1953    #[allow(clippy::too_many_arguments)]
1954    pub(super) fn queue_pending_message_at(
1955        &mut self,
1956        recipient: &str,
1957        content: &str,
1958        priority: MessagePriority,
1959        message_id: MessageId,
1960        reply_to_msg: Option<MessageId>,
1961        forwarded_from: Option<ForwardInfo>,
1962        content_type: ContentType,
1963        media_metadata: Option<MediaMetadata>,
1964        rich: Option<RichSendExtras>,
1965        app_id: Option<String>,
1966        queued_at: DateTime<Utc>,
1967    ) {
1968        let message_id_str = message_id.as_str().to_string();
1969        let mut pending = PendingMessage {
1970            content: content.to_string(),
1971            priority,
1972            message_id,
1973            reply_to_msg,
1974            forwarded_from,
1975            content_type,
1976            media_metadata,
1977            rich,
1978            app_id,
1979            queued_at,
1980            serialized_bytes: 0,
1981        };
1982        pending.measure();
1983        let incoming_bytes = pending.serialized_bytes;
1984
1985        let mut evicted = Vec::new();
1986
1987        while self
1988            .pending_encrypted_messages
1989            .get(recipient)
1990            .is_some_and(|messages| messages.len() >= MAX_PENDING_MESSAGES_PER_PEER)
1991        {
1992            if let Some(message) = self.evict_pending_message_at(recipient, 0) {
1993                evicted.push(message);
1994            } else {
1995                break;
1996            }
1997        }
1998
1999        // The running totals are decremented as entries are evicted rather
2000        // than recomputed each turn, so a queue at capacity does not make
2001        // admission quadratic.
2002        let mut peer_bytes = self.pending_message_bytes_for(recipient);
2003        while peer_bytes.saturating_add(incoming_bytes) > MAX_PENDING_MESSAGE_BYTES_PER_PEER {
2004            let Some(message) = self.evict_pending_message_at(recipient, 0) else {
2005                break;
2006            };
2007            peer_bytes = peer_bytes.saturating_sub(message.serialized_bytes);
2008            evicted.push(message);
2009        }
2010
2011        while self.total_pending_message_count() >= MAX_PENDING_MESSAGES_GLOBAL {
2012            let Some((_, message)) = self.evict_oldest_pending_message() else {
2013                break;
2014            };
2015            evicted.push(message);
2016        }
2017
2018        let mut global_bytes = self.total_pending_message_bytes();
2019        while global_bytes.saturating_add(incoming_bytes) > MAX_PENDING_MESSAGE_BYTES_GLOBAL {
2020            let Some((_, message)) = self.evict_oldest_pending_message() else {
2021                break;
2022            };
2023            global_bytes = global_bytes.saturating_sub(message.serialized_bytes);
2024            evicted.push(message);
2025        }
2026
2027        // Push to in-memory queue first, then persist (the in-memory queue
2028        // is the source of truth; storage is a crash-recovery backup).
2029        //
2030        // One record per message, so this writes the incoming entry and nothing
2031        // else — the queue it joins is already on disk, entry by entry, and the
2032        // entries the caps just evicted are deleted by id rather than rewritten
2033        // around. Which recipients were touched no longer matters.
2034        self.pending_encrypted_messages
2035            .entry(recipient.to_string())
2036            .or_default()
2037            .push(pending);
2038        self.note_pending_message_expiry(queued_at);
2039
2040        if let Some(queued) = self
2041            .pending_encrypted_messages
2042            .get(recipient)
2043            .and_then(|messages| messages.last())
2044        {
2045            self.persist_pending_message(recipient, queued);
2046        }
2047
2048        for message in evicted {
2049            self.delete_pending_message_from_storage(&message.message_id);
2050            self.emit_event(Event::message_failed(
2051                message.message_id,
2052                "Pending session queue capacity exceeded",
2053                0,
2054            ));
2055        }
2056
2057        debug!(recipient = %recipient, message_id = %message_id_str, "Queued message pending session establishment");
2058    }
2059
2060    pub(super) fn total_pending_message_count(&self) -> usize {
2061        self.pending_encrypted_messages.values().map(Vec::len).sum()
2062    }
2063
2064    /// Total serialized footprint of the pending-session queue across all
2065    /// peers. Summing precomputed per-entry sizes, so this stays a cheap
2066    /// integer walk rather than re-serializing the queue.
2067    pub(super) fn total_pending_message_bytes(&self) -> usize {
2068        self.pending_encrypted_messages
2069            .values()
2070            .flatten()
2071            .map(|message| message.serialized_bytes)
2072            .sum()
2073    }
2074
2075    /// Serialized footprint of one peer's pending-session queue.
2076    pub(super) fn pending_message_bytes_for(&self, recipient: &str) -> usize {
2077        self.pending_encrypted_messages
2078            .get(recipient)
2079            .map(|messages| {
2080                messages
2081                    .iter()
2082                    .map(|message| message.serialized_bytes)
2083                    .sum()
2084            })
2085            .unwrap_or(0)
2086    }
2087
2088    fn evict_pending_message_at(
2089        &mut self,
2090        recipient: &str,
2091        index: usize,
2092    ) -> Option<PendingMessage> {
2093        let (message, remove_recipient) = {
2094            let messages = self.pending_encrypted_messages.get_mut(recipient)?;
2095            if index >= messages.len() {
2096                return None;
2097            }
2098            let message = messages.remove(index);
2099            (message, messages.is_empty())
2100        };
2101        if remove_recipient {
2102            self.pending_encrypted_messages.remove(recipient);
2103        }
2104        Some(message)
2105    }
2106
2107    pub(super) fn evict_oldest_pending_message(&mut self) -> Option<(String, PendingMessage)> {
2108        let (recipient, index) = self
2109            .pending_encrypted_messages
2110            .iter()
2111            .flat_map(|(recipient, messages)| {
2112                messages
2113                    .iter()
2114                    .enumerate()
2115                    .map(move |(index, message)| (recipient, index, message))
2116            })
2117            .min_by(|left, right| {
2118                left.2
2119                    .queued_at
2120                    .cmp(&right.2.queued_at)
2121                    .then_with(|| left.2.message_id.as_str().cmp(&right.2.message_id.as_str()))
2122            })
2123            .map(|(recipient, index, _)| (recipient.clone(), index))?;
2124        let message = self.evict_pending_message_at(&recipient, index)?;
2125        Some((recipient, message))
2126    }
2127
2128    /// Drops a recipient's whole queue from memory *and* from storage, settling
2129    /// every dropped entry as `MessageFailed`.
2130    ///
2131    /// The storage half is a delete per entry rather than one whole-queue
2132    /// delete, which is the cost side of keying records by message id. Callers
2133    /// are the paths where every queued message for a peer becomes undeliverable
2134    /// at once — blocking, an aborted session.
2135    ///
2136    /// Settlement happens *here* rather than in the callers on purpose. The
2137    /// caller holds ids handed to the app by `send_message*` at queue time, so
2138    /// a silent drop leaves the app waiting forever on ids that will never
2139    /// resolve — the same contract the blocked-recipient branch of
2140    /// [`Self::flush_pending_messages`] and the pending-expiry pass already
2141    /// honour. Returning the messages for the caller to settle is what let all
2142    /// three call sites discard them instead, so there is nothing to discard
2143    /// any more.
2144    pub(super) fn drop_pending_queue_for_peer(
2145        &mut self,
2146        recipient: &str,
2147        reason: &'static str,
2148    ) -> usize {
2149        let messages = self
2150            .pending_encrypted_messages
2151            .remove(recipient)
2152            .unwrap_or_default();
2153        self.delete_pending_messages_from_storage(messages.iter().map(|m| &m.message_id));
2154        for message in &messages {
2155            self.emit_event(Event::message_failed(message.message_id.clone(), reason, 0));
2156        }
2157        self.recompute_next_pending_message_expiry();
2158        messages.len()
2159    }
2160
2161    fn pending_message_expiry(queued_at: DateTime<Utc>, lifetime_ms: u64) -> Option<DateTime<Utc>> {
2162        let lifetime_ms = i64::try_from(lifetime_ms).ok()?;
2163        queued_at.checked_add_signed(chrono::Duration::milliseconds(lifetime_ms))
2164    }
2165
2166    fn note_pending_message_expiry(&mut self, queued_at: DateTime<Utc>) {
2167        let Some(expiry) = Self::pending_message_expiry(
2168            queued_at,
2169            self.config
2170                .reliability
2171                .retry
2172                .pending_message_max_lifetime_ms,
2173        ) else {
2174            return;
2175        };
2176        self.next_pending_message_expiry = Some(
2177            self.next_pending_message_expiry
2178                .map_or(expiry, |current| current.min(expiry)),
2179        );
2180    }
2181
2182    pub(super) fn recompute_next_pending_message_expiry(&mut self) {
2183        let lifetime_ms = self
2184            .config
2185            .reliability
2186            .retry
2187            .pending_message_max_lifetime_ms;
2188        self.next_pending_message_expiry = self
2189            .pending_encrypted_messages
2190            .values()
2191            .flat_map(|messages| messages.iter())
2192            .filter_map(|message| Self::pending_message_expiry(message.queued_at, lifetime_ms))
2193            .min();
2194    }
2195
2196    /// Flushes pending messages for a recipient after session is established.
2197    ///
2198    /// Every message flushes through one unified path — prepare (re-making
2199    /// the seal-or-drop decision for rich extras against the recipient's
2200    /// *current* capability; the key package that confirmed this session may
2201    /// have advertised it), create, restore outer fields, then dispatch —
2202    /// and keeps the `message_id` the caller received from `send_message*`
2203    /// at queue time (no event fires on queueing — the returned id is the
2204    /// correlation anchor), so that id matches the eventual
2205    /// `MessageSent`/`MessageDelivered`/`MessageFailed`. A re-queue (session
2206    /// flapped back to not-ready) keeps the id too.
2207    pub(super) fn flush_pending_messages(&mut self, recipient: &str) -> Result<()> {
2208        if let Some(pending) = self.pending_encrypted_messages.remove(recipient) {
2209            // Blocking is bidirectional (we neither receive from nor send
2210            // to): a recipient blocked after messages were queued drops the
2211            // queue outright — retrying would fail with UserBlocked forever.
2212            if self.is_user_blocked(recipient) {
2213                info!(
2214                    recipient = %recipient,
2215                    count = pending.len(),
2216                    "Dropping pending messages for blocked recipient"
2217                );
2218                // The caller holds these ids (returned by `send_message*`
2219                // at queue time) — settle each one so the app isn't left
2220                // waiting on ids that will never resolve.
2221                for msg in &pending {
2222                    self.delete_pending_message_from_storage(&msg.message_id);
2223                    self.emit_event(Event::message_failed(
2224                        msg.message_id.clone(),
2225                        "Recipient blocked",
2226                        0,
2227                    ));
2228                }
2229                return Ok(());
2230            }
2231
2232            info!(recipient = %recipient, count = pending.len(), "Flushing pending messages");
2233            let original_order: Vec<String> =
2234                pending.iter().map(|m| m.message_id.as_str()).collect();
2235            let mut remaining = Vec::new();
2236            let now = Utc::now();
2237            let lifetime_ms = self
2238                .config
2239                .reliability
2240                .retry
2241                .pending_message_max_lifetime_ms;
2242
2243            for msg in pending {
2244                // The absolute pending lifetime is enforced here as well as in
2245                // the expiry pass, because that pass is bounded per tick
2246                // (`MAX_PENDING_EXPIRIES_PER_PASS`) — a restore can hand this
2247                // flush entries already past their deadline, and dispatching
2248                // one would settle it `MessageSent` after the lifetime promised
2249                // `message_failed`. Back into `remaining` instead: the entry
2250                // keeps its record, and the next expiry pass settles and
2251                // deletes it as a pair.
2252                if lifetime_expired(now, msg.queued_at, lifetime_ms) {
2253                    remaining.push(msg);
2254                    continue;
2255                }
2256
2257                // With stable ids the deduplicator now guards against a
2258                // double flush (e.g. a stale storage snapshot restored after
2259                // the message already went out): a dedup hit means this id
2260                // was dispatched before — drop it, don't retry forever.
2261                if self.deduplicator.is_duplicate(&msg.message_id) {
2262                    if self.deduplicator.is_exact() {
2263                        // Exact mode: the hit is authoritative — this id was
2264                        // already dispatched and (barring an internal error
2265                        // after the transport accepted it) settled then via
2266                        // MessageSent or MessageDeferred; re-dispatching
2267                        // would duplicate on the wire.
2268                        debug!(
2269                            message_id = %msg.message_id,
2270                            "Pending message already dispatched (dedup hit), dropping"
2271                        );
2272                    } else {
2273                        // Bloom mode: the hit may be a false positive for a
2274                        // message that never went out, and dispatching anyway
2275                        // would error-loop on the same filter. Settle loudly
2276                        // so the app can resend under a fresh id instead of
2277                        // losing the message silently.
2278                        warn!(
2279                            message_id = %msg.message_id,
2280                            "Pending message dropped on probabilistic dedup hit"
2281                        );
2282                        self.emit_event(Event::message_failed(
2283                            msg.message_id.clone(),
2284                            "Duplicate suppressed at flush (probabilistic dedup; possible false positive)",
2285                            0,
2286                        ));
2287                    }
2288                    // Leaves the queue for good either way, so its record goes
2289                    // with it. Only the entries that end up back in `remaining`
2290                    // below keep theirs — those were never deleted, and their
2291                    // contents have not changed, so re-queuing them costs no
2292                    // write at all.
2293                    self.delete_pending_message_from_storage(&msg.message_id);
2294                    continue;
2295                }
2296
2297                // Validated at admission and sealed at rest, so a failure
2298                // here means a corrupted record. Sending under the configured
2299                // id beats re-queueing an entry that can never flush.
2300                let flush_app_id = match Self::parse_send_app_id(msg.app_id.as_deref()) {
2301                    Ok(app_id) => app_id,
2302                    Err(e) => {
2303                        warn!(message_id = %msg.message_id, error = %e, "Pending message app id unreadable, using the configured id");
2304                        None
2305                    }
2306                };
2307
2308                let final_content = match self.prepare_outbound_content(
2309                    recipient,
2310                    &msg.content,
2311                    msg.priority,
2312                    msg.reply_to_msg.clone(),
2313                    msg.forwarded_from.clone(),
2314                    msg.content_type,
2315                    msg.media_metadata.clone(),
2316                    msg.rich.as_ref(),
2317                    flush_app_id.as_ref(),
2318                    Some(PendingProvenance::requeued(&msg)),
2319                    "flush_pending",
2320                ) {
2321                    Ok(OutboundSendPreparation::Ready(c)) => c,
2322                    Ok(OutboundSendPreparation::Queued(message_id)) => {
2323                        // Re-queued for later (session still not ready),
2324                        // keeping the original id.
2325                        debug!(message_id = %message_id, "Pending message re-queued");
2326                        continue;
2327                    }
2328                    Err(e) => {
2329                        warn!(message_id = %msg.message_id, error = %e, "Failed to prepare pending message");
2330                        remaining.push(msg);
2331                        continue;
2332                    }
2333                };
2334
2335                let message = match self.create_message_for_app(
2336                    recipient,
2337                    final_content,
2338                    Some(msg.priority),
2339                    msg.reply_to_msg.clone(),
2340                    flush_app_id,
2341                ) {
2342                    Ok(mut message) => {
2343                        // Restore outer fields: forward attribution and its
2344                        // media_metadata ride outer as the fallback for
2345                        // non-capable recipients (secrets stripped at the
2346                        // wire); when `msg.rich` sealed above, the receiver's
2347                        // sealed-body restore overwrites these wholesale.
2348                        // Plain sends store None for both — their rich
2349                        // copies are sealed or dropped by prepare above.
2350                        message.forwarded_from = msg.forwarded_from.clone();
2351                        message.content_type = msg.content_type;
2352                        message.media_metadata = msg.media_metadata.clone();
2353                        message.id = msg.message_id.clone();
2354                        message
2355                    }
2356                    Err(e) => {
2357                        warn!(message_id = %msg.message_id, error = %e, "Failed to create pending message");
2358                        remaining.push(msg);
2359                        continue;
2360                    }
2361                };
2362
2363                // Tier 2 provenance: a flushed pending message that sealed
2364                // carries its plaintext forward so later resends re-seal against
2365                // the peer's current session (see `send_message_with`).
2366                if message.content.starts_with(internal_prefixes::ENCRYPTED) {
2367                    self.stage_outbox_reseal(
2368                        &message.id,
2369                        OutboxReseal {
2370                            content: msg.content.clone(),
2371                            priority: msg.priority,
2372                            reply_to_msg: msg.reply_to_msg.clone(),
2373                            forwarded_from: msg.forwarded_from.clone(),
2374                            content_type: msg.content_type,
2375                            media_metadata: msg.media_metadata.clone(),
2376                            rich: msg.rich.clone(),
2377                        },
2378                    );
2379                }
2380
2381                match self.dispatch_prepared_message(message) {
2382                    Ok(id) => {
2383                        debug!(message_id = %id, "Sent pending message");
2384                        // Handed off to the outbox, which persists its own
2385                        // record under the same id — so the pending copy has to
2386                        // go, or a restart would restore and re-flush it.
2387                        self.delete_pending_message_from_storage(&id);
2388                    }
2389                    Err(e) => {
2390                        warn!(message_id = %msg.message_id, error = %e, "Failed to send pending message");
2391                        remaining.push(msg);
2392                    }
2393                }
2394            }
2395
2396            // Messages that re-queued above re-inserted themselves into the
2397            // map (and rewrote their own record) — merge failures in without
2398            // clobbering the entry. Nothing to clear when neither survives: each
2399            // entry that left the queue already took its own record with it.
2400            if remaining.is_empty() {
2401                return Ok(());
2402            }
2403            if self.has_terminal_welcome_failure(recipient) {
2404                // A prepare failure above came from a terminal Welcome
2405                // failure: abort_pending_session_for_peer already cleared
2406                // the queue and its storage and settled the peer with
2407                // secure_session_failed — re-inserting `remaining` would
2408                // resurrect messages the abort just settled, on every
2409                // future flush, forever.
2410                //
2411                // The abort could only delete the records of entries still in
2412                // the map, and this flush took the queue out of it at the top,
2413                // so these records are still on disk and have to go here or the
2414                // next launch restores exactly what the abort settled.
2415                debug!(
2416                    recipient = %recipient,
2417                    count = remaining.len(),
2418                    "Dropping flush failures for aborted pending session"
2419                );
2420                for msg in &remaining {
2421                    self.delete_pending_message_from_storage(&msg.message_id);
2422                }
2423            } else {
2424                // Every other path into `pending_encrypted_messages` notes the
2425                // entry's deadline. This one is a *re*-insertion of entries the
2426                // queue already held, so today the cached minimum is still
2427                // correct without it — but only because nothing recomputes that
2428                // cache while a flush is in progress. Noting them keeps
2429                // "`next_pending_message_expiry` is never later than the true
2430                // minimum" an invariant of the code rather than of the call
2431                // graph: if it ever broke, these entries would stop being
2432                // scanned and would never expire, silently, on the one queue the
2433                // absolute pending lifetime exists to bound.
2434                let merged_deadlines: Vec<DateTime<Utc>> =
2435                    remaining.iter().map(|m| m.queued_at).collect();
2436                let queue = self
2437                    .pending_encrypted_messages
2438                    .entry(recipient.to_string())
2439                    .or_default();
2440                queue.extend(remaining);
2441                // A failure that preceded a mid-flush re-queue would land
2442                // behind it — restore the original queue order (stable
2443                // sort; every id here came from this flush's queue).
2444                queue.sort_by_key(|m| {
2445                    original_order
2446                        .iter()
2447                        .position(|id| *id == m.message_id.as_str())
2448                        .unwrap_or(usize::MAX)
2449                });
2450                for queued_at in merged_deadlines {
2451                    self.note_pending_message_expiry(queued_at);
2452                }
2453                // No write: these entries never left the queue durably. Their
2454                // records were written at enqueue, were not deleted above, and
2455                // their contents have not changed — only their position in the
2456                // in-memory queue, which the restore ordering rebuilds from
2457                // `queued_at` rather than from storage order.
2458            }
2459        }
2460        Ok(())
2461    }
2462
2463    pub(super) fn flush_restored_confirmed_pending_messages(&mut self) {
2464        let recipients: Vec<String> = self.pending_encrypted_messages.keys().cloned().collect();
2465
2466        for recipient in recipients {
2467            match self.is_session_confirmed(&recipient) {
2468                Ok(true) => {
2469                    if let Err(err) = self.flush_pending_messages(&recipient) {
2470                        warn!(
2471                            recipient = %recipient,
2472                            error = %err,
2473                            "Failed to flush restored pending messages for confirmed session"
2474                        );
2475                    }
2476                }
2477                Ok(false) => {}
2478                Err(err) => {
2479                    warn!(
2480                        recipient = %recipient,
2481                        error = %err,
2482                        "Failed to read session confirmation state while restoring pending messages"
2483                    );
2484                }
2485            }
2486        }
2487    }
2488
2489    // ========================================================================
2490    // MEDIA
2491    // ========================================================================
2492
2493    /// Sends a media attachment (image, video, audio, file, etc.) to a recipient.
2494    ///
2495    /// The file data is chunked and each chunk is sent as an individual message
2496    /// with `content_type: FileChunk`. The first chunk carries the full
2497    /// `MediaMetadata` so the receiver can display a preview before all chunks
2498    /// arrive. Individual chunk messages require ACKs and participate in retry
2499    /// logic so delivery is tracked and recoverable per chunk. `MediaSent` is
2500    /// emitted only after all chunks are ACKed.
2501    ///
2502    /// # Encryption
2503    ///
2504    /// With auto-encryption active (encryption enabled and MLS initialized),
2505    /// chunk bytes — and the chunk-0 media metadata and original content type —
2506    /// travel inside MLS ciphertext, using the same session the text path uses.
2507    /// The session must already be confirmed: media is never queued pending
2508    /// establishment and never falls back to plaintext. When the session is
2509    /// not ready this returns [`Error::SessionNotReady`] (after kicking
2510    /// establishment if a key package is available); retry after the
2511    /// `secure_session_established` event. When encryption is required but MLS
2512    /// is not initialized this returns [`Error::EncryptFailed`]. Plaintext
2513    /// chunks are sent only when auto-encryption is inactive AND
2514    /// `require_encryption` was explicitly set to `false` (it defaults to
2515    /// `true`): the explicit encryption opt-out, or encryption enabled but
2516    /// MLS never initialized (matching the text path). Every plaintext
2517    /// transfer emits a
2518    /// [`crate::events::SecurityWarningCode::PlaintextSend`] warning, once
2519    /// per peer.
2520    ///
2521    /// Returns a `file_id` that can be used to track progress or cancel.
2522    pub fn send_media(
2523        &mut self,
2524        recipient: impl Into<String>,
2525        file_data: Vec<u8>,
2526        file_name: impl Into<String>,
2527        content_type: ContentType,
2528        media_metadata: Option<MediaMetadata>,
2529    ) -> Result<String> {
2530        self.send_media_with(
2531            recipient,
2532            file_data,
2533            file_name,
2534            content_type,
2535            MediaSendOptions {
2536                media_metadata,
2537                ..Default::default()
2538            },
2539        )
2540    }
2541
2542    /// Sends a media attachment with rich options: the chunk-0 media
2543    /// metadata (as on [`Self::send_media`]), plus caption, reply threading,
2544    /// quoted-reply context, forward attribution, and an optional
2545    /// caller-supplied `file_id`.
2546    ///
2547    /// The rich fields only ever travel inside the MLS-sealed chunk-0
2548    /// plaintext (under the v2 media envelope), and only toward recipients
2549    /// whose key package advertised `rich_versions` support (gated by
2550    /// `EncryptionConfig::rich_payload_enabled`). Toward anyone else —
2551    /// including every plaintext (encryption opt-out) transfer — they are
2552    /// silently dropped, never sent cleartext.
2553    ///
2554    /// Returns `InvalidArgument` for an unparsable `reply_to_msg`, rich
2555    /// extras whose serialized size exceeds 32 KiB, or a `file_id` that is
2556    /// empty, over the wire field bound, or already carried by an active
2557    /// outbound transfer. Encryption preconditions match
2558    /// [`Self::send_media`].
2559    pub fn send_media_with(
2560        &mut self,
2561        recipient: impl Into<String>,
2562        file_data: Vec<u8>,
2563        file_name: impl Into<String>,
2564        content_type: ContentType,
2565        options: MediaSendOptions,
2566    ) -> Result<String> {
2567        // The public surface never carries a data purpose. Every transfer
2568        // reachable from an application is a transfer for its user, and the
2569        // one argument that could change that is not in `MediaSendOptions`
2570        // and must never be added to it: a caller able to mark a send as
2571        // data-purposed could feed bytes of its choosing to a peer's
2572        // document engine while that peer's user saw nothing arrive.
2573        self.send_media_inner(recipient, file_data, file_name, content_type, options, None)
2574    }
2575
2576    /// The body of [`Self::send_media_with`], plus the one argument the
2577    /// public surface may not supply.
2578    ///
2579    /// `data_purpose` marks a transfer as belonging to the replicated-
2580    /// document layer. It is `pub(crate)` and reached only from the data
2581    /// layer's own send seams.
2582    #[cfg_attr(not(feature = "data"), allow(dead_code))]
2583    pub(crate) fn send_media_inner(
2584        &mut self,
2585        recipient: impl Into<String>,
2586        file_data: Vec<u8>,
2587        file_name: impl Into<String>,
2588        content_type: ContentType,
2589        options: MediaSendOptions,
2590        data_purpose: Option<crate::media_envelope::DataPurpose>,
2591    ) -> Result<String> {
2592        {
2593            let state = lock_shared_state(&self.shared_state)?;
2594            if state.state != ProtocolState::Running {
2595                return Err(Error::NotStarted);
2596            }
2597        }
2598
2599        let recipient_str: String = recipient.into();
2600        let file_name_str: String = file_name.into();
2601        let media_metadata = options.media_metadata;
2602        Self::validate_outbound_recipient(&recipient_str)?;
2603        // Before any transfer state exists, like the recipient check.
2604        let app_id = Self::parse_send_app_id(options.app_id.as_deref())?;
2605
2606        // Validate the reply id like send_message_with does — a malformed id
2607        // fails the call instead of riding sealed to the receiver.
2608        if let Some(reply_to) = options.reply_to_msg.as_deref() {
2609            MessageId::from_str(reply_to)
2610                .map_err(|e| Error::InvalidArgument(format!("Invalid reply_to_msg: {}", e)))?;
2611        }
2612
2613        let rich_extras = MediaRichExtras {
2614            caption: options.caption,
2615            reply_to_msg: options.reply_to_msg,
2616            reply_context: options.reply_context,
2617            forward_info: options.forward_info,
2618        };
2619        // Same boundary cap as send_message_with: an oversized quote or
2620        // caption would inflate the sealed chunk-0 plaintext into heavy
2621        // fragmentation. Enforced before the capability gate so the caller
2622        // hears about it even when the extras would be dropped.
2623        if rich_extras.is_any() {
2624            let extras_len = serde_json::to_vec(&rich_extras)
2625                .map_err(|e| Error::Serialization(e.to_string()))?
2626                .len();
2627            if extras_len > MAX_RICH_EXTRAS_BYTES {
2628                return Err(Error::InvalidArgument(format!(
2629                    "Rich extras too large: {} bytes serialized (max {})",
2630                    extras_len, MAX_RICH_EXTRAS_BYTES
2631                )));
2632            }
2633        }
2634
2635        if let Some(file_id) = options.file_id.as_deref() {
2636            if file_id.is_empty() || file_id.len() > FileChunk::MAX_STRING_FIELD_LEN {
2637                return Err(Error::InvalidArgument(format!(
2638                    "file_id length must be 1..={} bytes",
2639                    FileChunk::MAX_STRING_FIELD_LEN
2640                )));
2641            }
2642            if self.outbound_media_transfers.contains_key(file_id) {
2643                return Err(Error::InvalidArgument(format!(
2644                    "file_id {} already has an active outbound transfer",
2645                    file_id
2646                )));
2647            }
2648            // A restored descriptor under this file_id means this send is
2649            // the app answering MediaResendRequired: it must target the
2650            // original recipient (a redirect would consume the descriptor
2651            // and orphan the interrupted transfer's resend forever) and
2652            // re-supply the original bytes — the receiver may already hold
2653            // chunks of the interrupted attempt, and a silent content swap
2654            // under the same id would fail its integrity check at best.
2655            if let Some(descriptor) = self.restored_media_descriptors.get(file_id) {
2656                if descriptor.recipient != recipient_str {
2657                    return Err(Error::InvalidArgument(format!(
2658                        "file_id {} belongs to an interrupted transfer to a different recipient",
2659                        file_id
2660                    )));
2661                }
2662                use sha2::{Digest, Sha256};
2663                let resupplied_checksum = format!("{:x}", Sha256::digest(&file_data));
2664                if resupplied_checksum != descriptor.file_checksum {
2665                    return Err(Error::InvalidArgument(format!(
2666                        "file_id {} resend bytes do not match the interrupted transfer's checksum",
2667                        file_id
2668                    )));
2669                }
2670            }
2671        }
2672
2673        // Prevent sending media to blocked users. Blocking is bidirectional:
2674        // we neither receive from nor send to a blocked peer.
2675        if self.is_user_blocked(&recipient_str) {
2676            return Err(Error::UserBlocked(recipient_str));
2677        }
2678
2679        // A data-purposed transfer goes only to a peer that said it routes
2680        // one into its document layer. Toward anybody else the bytes would
2681        // be handed to a person as a downloaded file, so this refuses rather
2682        // than degrading: there is no useful weaker form of this send.
2683        if data_purpose.is_some() && !self.data_media_active(&recipient_str) {
2684            return Err(Error::InvalidArgument(format!(
2685                "peer {recipient_str} did not advertise attachment carriage"
2686            )));
2687        }
2688
2689        // And only where it can be sealed. The marking exists nowhere but
2690        // inside the encrypted chunk-0 plaintext, so a transfer leaving by
2691        // the plaintext opt-out path arrives stripped of it: the receiver
2692        // has no way to know what the bytes are and hands its user a
2693        // downloaded file named by a hash, which is the outcome the whole
2694        // marking exists to prevent. Refused rather than degraded, for the
2695        // same reason the capability gate above refuses.
2696        if data_purpose.is_some() && !self.should_auto_encrypt() {
2697            return Err(Error::EncryptFailed(
2698                "a document-layer transfer cannot be sent unsealed: its marking travels \
2699                 inside the encrypted chunk, and a receiver would surface the bytes to its \
2700                 user as an ordinary file"
2701                    .to_string(),
2702            ));
2703        }
2704
2705        // SEC-H1: media rides the same MLS session machinery as text. With
2706        // auto-encryption active the session must already be confirmed —
2707        // files are too large for the pending-message queue, so media is
2708        // never queued pending establishment and never falls back to
2709        // plaintext. Callers should retry after `secure_session_established`.
2710        if self.should_auto_encrypt() {
2711            if !self.is_session_confirmed(&recipient_str)? {
2712                // Kick session establishment exactly like the text path
2713                // (import a stored key package, create the session, send the
2714                // Welcome), then report not-ready: the session cannot be
2715                // confirmed synchronously, so callers retry after
2716                // `secure_session_established`.
2717                if let Some(mls) = self.mls_manager.clone() {
2718                    // A document-layer transfer is the SDK's own: the
2719                    // Welcome it opens must not alert.
2720                    self.ensure_session_establishment(
2721                        &mls,
2722                        &recipient_str,
2723                        data_purpose.is_none(),
2724                    )?;
2725                }
2726                return Err(Error::SessionNotReady(
2727                    self.establishment_state(&recipient_str)?,
2728                ));
2729            }
2730
2731            // Encrypted chunks share the recipient's session ratchet with
2732            // text, and the receiver keeps out-of-order message keys for a
2733            // bounded number of generations — cap concurrent transfers so
2734            // the combined in-flight windows cannot push a delayed chunk
2735            // beyond that tolerance and permanently stall it.
2736            use crate::constants::{
2737                MAX_CONCURRENT_INTERNAL_MEDIA_TRANSFERS_PER_PEER,
2738                MAX_CONCURRENT_MEDIA_TRANSFERS_PER_PEER,
2739            };
2740            let active_transfers = self
2741                .outbound_media_transfers
2742                .values()
2743                .filter(|transfer| transfer.recipient == recipient_str)
2744                .count();
2745            if active_transfers >= MAX_CONCURRENT_MEDIA_TRANSFERS_PER_PEER {
2746                return Err(Error::MediaTransferLimit(recipient_str));
2747            }
2748
2749            // An SDK-internal transfer takes at most one of those slots.
2750            // The cap above bounds the ratchet gap and so cannot be raised
2751            // for our own traffic; what is left is to leave the application
2752            // a slot it can account for. The in-flight dedup stops a second
2753            // copy of one transfer, not two different ones, and two
2754            // different ones are ordinary here: a document snapshot and an
2755            // answered blob request are separate errands to the same peer.
2756            // Filling both slots with them fails the app's own send with a
2757            // limit whose cause it cannot see, because what holds the slots
2758            // is invisible to it by design.
2759            if data_purpose.is_some() {
2760                let internal_transfers = self
2761                    .outbound_media_transfers
2762                    .values()
2763                    .filter(|transfer| {
2764                        transfer.recipient == recipient_str && transfer.data_purpose.is_some()
2765                    })
2766                    .count();
2767                if internal_transfers >= MAX_CONCURRENT_INTERNAL_MEDIA_TRANSFERS_PER_PEER {
2768                    return Err(Error::MediaTransferLimit(recipient_str));
2769                }
2770            }
2771        } else if self.config.encryption.require_encryption {
2772            return Err(Error::EncryptFailed(
2773                "MLS encryption is required (the default) but MLS is not initialized — \
2774                 call initialize_mls() with secure and protocol-state storage, or explicitly opt out with \
2775                 require_encryption=false to send plaintext media"
2776                    .to_string(),
2777            ));
2778        } else {
2779            // Explicit opt-out: the whole transfer leaves as legacy plaintext
2780            // chunks. Warn once per peer, not per chunk.
2781            self.warn_plaintext_send(&recipient_str);
2782        }
2783
2784        // Rich extras seal only toward recipients that advertised the rich
2785        // payload, and only on the encrypted path; everyone else — including
2786        // every plaintext (opt-out) transfer — gets the plain transfer.
2787        // Dropped here, never sent cleartext.
2788        let rich_extras = if !rich_extras.is_any() {
2789            None
2790        } else if self.should_auto_encrypt() && self.rich_seal_active(&recipient_str) {
2791            Some(rich_extras)
2792        } else {
2793            debug!(
2794                recipient = %recipient_str,
2795                "Dropping rich media extras: recipient did not advertise sealed rich payload support"
2796            );
2797            None
2798        };
2799
2800        let file_id = options
2801            .file_id
2802            .unwrap_or_else(|| format!("file_{}", MessageId::new().as_str()));
2803        let pinned_transport = self.select_media_transport(&recipient_str)?;
2804
2805        let (chunk_size, window_size) = match pinned_transport {
2806            TransportType::BLE => {
2807                use crate::constants::{CHUNK_SIZE_BLE, MEDIA_WINDOW_SIZE_BLE};
2808                (CHUNK_SIZE_BLE, MEDIA_WINDOW_SIZE_BLE)
2809            }
2810            TransportType::Internet => {
2811                use crate::constants::{CHUNK_SIZE_INTERNET, MEDIA_WINDOW_SIZE_INTERNET};
2812                (CHUNK_SIZE_INTERNET, MEDIA_WINDOW_SIZE_INTERNET)
2813            }
2814            TransportType::WiFiDirect => {
2815                use crate::constants::{DEFAULT_CHUNK_SIZE, DEFAULT_MEDIA_WINDOW_SIZE};
2816                (DEFAULT_CHUNK_SIZE, DEFAULT_MEDIA_WINDOW_SIZE)
2817            }
2818            TransportType::Reticulum => {
2819                // Reticulum is low-bandwidth (LoRa); use BLE-like chunk sizes.
2820                use crate::constants::{CHUNK_SIZE_BLE, MEDIA_WINDOW_SIZE_BLE};
2821                (CHUNK_SIZE_BLE, MEDIA_WINDOW_SIZE_BLE)
2822            }
2823            TransportType::Nostr => {
2824                // Nostr relays have decent bandwidth; use Internet-like chunk sizes.
2825                use crate::constants::{CHUNK_SIZE_INTERNET, MEDIA_WINDOW_SIZE_INTERNET};
2826                (CHUNK_SIZE_INTERNET, MEDIA_WINDOW_SIZE_INTERNET)
2827            }
2828        };
2829        let chunks = self.file_transfer_manager.chunk_file(
2830            file_id.clone(),
2831            file_name_str,
2832            file_data,
2833            Some(chunk_size),
2834        )?;
2835
2836        let total_chunks = chunks.len() as u32;
2837        self.outbound_media_transfers.insert(
2838            file_id.clone(),
2839            OutboundMediaTransfer {
2840                content_type,
2841                recipient: recipient_str.clone(),
2842                pinned_transport,
2843                total_chunks,
2844                delivered_chunks: HashSet::new(),
2845                last_updated_at: Instant::now(),
2846                media_metadata: media_metadata.clone(),
2847                rich_extras: rich_extras.clone(),
2848                data_purpose: data_purpose.clone(),
2849                app_id: app_id.clone(),
2850            },
2851        );
2852
2853        // Persist the crash-recovery descriptor (no chunk bytes) so a
2854        // restart mid-transfer can tell the app to re-initiate via
2855        // MediaResendRequired. A same-file_id resend consumes the restored
2856        // copy here — its checksum was validated at the boundary above.
2857        if let Some(first_chunk) = chunks.first() {
2858            let descriptor = MediaTransferDescriptor {
2859                file_id: file_id.clone(),
2860                recipient: recipient_str.clone(),
2861                file_name: first_chunk.file_name.clone(),
2862                file_size: first_chunk.file_size,
2863                file_checksum: first_chunk.file_checksum.clone(),
2864                content_type,
2865                queued_at: Utc::now(),
2866                data_purpose: data_purpose.clone(),
2867                app_id: app_id.as_ref().map(|id| id.as_str().to_string()),
2868            };
2869            self.restored_media_descriptors.remove(&file_id);
2870            self.persist_media_descriptor(&descriptor);
2871        }
2872
2873        let mut window_state = OutboundTransferState::new(chunks, window_size);
2874        let initial_batch = window_state.next_chunks_to_send();
2875        self.outbound_media_windows
2876            .insert(file_id.clone(), window_state);
2877
2878        // A data-purposed transfer is invisible to the application on this
2879        // side too. The receiver's user must not be shown a download they did
2880        // not start; the sender's must not be shown an upload they did not
2881        // make, and `MediaSent` for one would put a file in a conversation.
2882        if data_purpose.is_none() {
2883            let state = lock_shared_state(&self.shared_state)?;
2884            state.emit_event(Event::file_progress(file_id.clone(), 0, total_chunks));
2885            drop(state);
2886        }
2887
2888        self.send_media_chunk_batch(
2889            &file_id,
2890            initial_batch,
2891            &recipient_str,
2892            pinned_transport,
2893            content_type,
2894            media_metadata.as_ref(),
2895            rich_extras.as_ref(),
2896            data_purpose.as_ref(),
2897            app_id.as_ref(),
2898        )?;
2899
2900        Ok(file_id)
2901    }
2902
2903    /// Sends a batch of file chunks, wiring each into the outbox and media tracking.
2904    #[allow(clippy::too_many_arguments)]
2905    pub(super) fn send_media_chunk_batch(
2906        &mut self,
2907        file_id: &str,
2908        chunks: Vec<FileChunk>,
2909        recipient: &str,
2910        pinned_transport: TransportType,
2911        content_type: ContentType,
2912        media_metadata: Option<&MediaMetadata>,
2913        rich_extras: Option<&MediaRichExtras>,
2914        data_purpose: Option<&crate::media_envelope::DataPurpose>,
2915        app_id: Option<&AppId>,
2916    ) -> Result<()> {
2917        for chunk in chunks {
2918            let chunk_index = chunk.chunk_index;
2919
2920            let meta_for_chunk = if chunk_index == 0 {
2921                media_metadata.cloned()
2922            } else {
2923                None
2924            };
2925
2926            // SEC-H1: with auto-encryption active, chunk bytes AND the chunk-0
2927            // metadata (file name, preview thumbnail, original content type)
2928            // travel inside the MLS ciphertext — the wire Message carries none
2929            // of them. The plaintext fields are only populated when
2930            // auto-encryption is inactive. Rich extras (caption, reply,
2931            // forward) exist only on this sealed path, chunk 0 only; their
2932            // presence bumps the envelope to v2 so a pre-rich receiver
2933            // rejects cleanly instead of misparsing.
2934            let (binary_payload, wire_metadata, wire_original_ct) = if self.should_auto_encrypt() {
2935                let inner = MediaChunkPlaintext {
2936                    chunk_bytes: chunk.to_bytes(),
2937                    media_metadata: meta_for_chunk,
2938                    original_content_type: (chunk_index == 0).then_some(content_type),
2939                    rich_extras: if chunk_index == 0 {
2940                        rich_extras.cloned()
2941                    } else {
2942                        None
2943                    },
2944                    data_purpose: if chunk_index == 0 {
2945                        data_purpose.cloned()
2946                    } else {
2947                        None
2948                    },
2949                };
2950                let envelope_version = inner.envelope_version();
2951                let sealed = inner
2952                    .encode()
2953                    .map_err(Error::Serialization)
2954                    .and_then(|plaintext| {
2955                        self.encrypt_bytes_for_recipient_strict(recipient, &plaintext)
2956                    });
2957                let encrypted = match sealed {
2958                    Ok(encrypted) => encrypted,
2959                    Err(err) => {
2960                        // The chunks this batch popped from the window are
2961                        // marked in-flight with no outbox entry — nothing
2962                        // would ever ACK or retry them, so the transfer can
2963                        // only wedge while holding a per-peer transfer slot.
2964                        // Encryption failure is not per-chunk transient:
2965                        // abort the whole transfer loudly.
2966                        self.abort_outbound_media_transfer(file_id, "chunk encryption failed");
2967                        return Err(err);
2968                    }
2969                };
2970                (
2971                    encode_media_envelope(&encrypted, envelope_version),
2972                    None,
2973                    false,
2974                )
2975            } else {
2976                (chunk.to_bytes(), meta_for_chunk, chunk_index == 0)
2977            };
2978
2979            let mut message = self.create_media_message(
2980                recipient,
2981                String::new(),
2982                ContentType::FileChunk,
2983                wire_metadata,
2984                app_id.cloned(),
2985            )?;
2986            message.binary_content = Some(binary_payload);
2987
2988            if wire_original_ct {
2989                use crate::constants::ORIGINAL_CONTENT_TYPE_KEY;
2990                message.metadata.insert(
2991                    ORIGINAL_CONTENT_TYPE_KEY.to_string(),
2992                    content_type.to_string(),
2993                );
2994            }
2995            // A document-layer transfer is the SDK's own traffic, like the
2996            // `__DATA_V1__` frames beside it. The purpose rides sealed in
2997            // chunk 0 only, so every chunk is marked here, where it is known.
2998            if data_purpose.is_some() {
2999                message
3000                    .metadata
3001                    .insert(crate::constants::NO_ALERT_KEY.to_string(), "1".to_string());
3002            }
3003            self.outbound_media_chunks
3004                .insert(message.id.clone(), (file_id.to_string(), chunk_index));
3005
3006            let previous_transport = self.transport_manager.current_transport();
3007            let send_result = self
3008                .transport_manager
3009                .send_via_transport(&message, pinned_transport);
3010            let current_transport = Some(pinned_transport);
3011
3012            match send_result {
3013                Ok(()) => {
3014                    self.handle_send_success(&message, current_transport)?;
3015                    self.emit_transport_switch_event(previous_transport, current_transport)?;
3016                }
3017                Err(err) => {
3018                    let _ = self
3019                        .handle_send_failure(&message, current_transport.or(previous_transport))?;
3020                    // send_via_transport does not record retry failures internally.
3021                    self.transport_manager
3022                        .record_retry_failure(pinned_transport);
3023                    warn!(
3024                        file_id = %file_id,
3025                        chunk_index = chunk_index,
3026                        transport = ?pinned_transport,
3027                        error = %err,
3028                        "File chunk send failed, message deferred"
3029                    );
3030                }
3031            }
3032            if !self.outbound_media_transfers.contains_key(file_id) {
3033                return Err(Error::Other(format!(
3034                    "Media transfer {} could not be scheduled for reliable delivery",
3035                    file_id
3036                )));
3037            }
3038        }
3039        Ok(())
3040    }
3041
3042    /// Pumps all active windowed media transfers, sending the next batch of
3043    /// chunks for any transfer whose window has capacity (previous chunks ACKed).
3044    /// Should be called from the periodic tick/poll loop.
3045    pub(super) fn pump_media_transfers(&mut self) {
3046        let file_ids: Vec<String> = self.outbound_media_windows.keys().cloned().collect();
3047
3048        for file_id in file_ids {
3049            let transfer = match self.outbound_media_transfers.get(&file_id) {
3050                Some(t) => t.clone(),
3051                None => {
3052                    self.outbound_media_windows.remove(&file_id);
3053                    continue;
3054                }
3055            };
3056
3057            let window = match self.outbound_media_windows.get_mut(&file_id) {
3058                Some(w) => w,
3059                None => continue,
3060            };
3061
3062            if !window.has_capacity() {
3063                continue;
3064            }
3065
3066            let batch = window.next_chunks_to_send();
3067            if batch.is_empty() {
3068                continue;
3069            }
3070
3071            if let Err(err) = self.send_media_chunk_batch(
3072                &file_id,
3073                batch,
3074                &transfer.recipient,
3075                transfer.pinned_transport,
3076                transfer.content_type,
3077                transfer.media_metadata.as_ref(),
3078                transfer.rich_extras.as_ref(),
3079                transfer.data_purpose.as_ref(),
3080                transfer.app_id.as_ref(),
3081            ) {
3082                warn!(
3083                    file_id = %file_id,
3084                    error = %err,
3085                    "Failed to pump media transfer chunks"
3086                );
3087            }
3088        }
3089    }
3090
3091    /// Creates a message carrying media content (file chunks, etc.).
3092    ///
3093    /// Like `create_message` but sets `content_type`, `media_metadata`, marks
3094    /// the message as internet-preferred via metadata, and requires per-chunk ACKs.
3095    pub(super) fn create_media_message(
3096        &mut self,
3097        recipient: &str,
3098        content: impl Into<String>,
3099        content_type: ContentType,
3100        media_metadata: Option<MediaMetadata>,
3101        app_id: Option<AppId>,
3102    ) -> Result<Message> {
3103        use crate::constants::{TRANSPORT_PREFERENCE_INTERNET, TRANSPORT_PREFERENCE_KEY};
3104
3105        let sender = UserId::new(&self.local_id)?;
3106        let recipient = UserId::new(recipient)?;
3107        let app_id = match app_id {
3108            Some(app_id) => app_id,
3109            None => AppId::new(&self.config.app_id)?,
3110        };
3111
3112        let clock_value = self.lamport_clock.tick();
3113        self.persist_lamport_clock();
3114
3115        let mut builder = Message::builder(sender, recipient, app_id)
3116            .content(content)
3117            .content_type(content_type)
3118            .priority(MessagePriority::Medium)
3119            .ttl(TTL::new(self.config.initial_ttl)?)
3120            .lamport_clock(clock_value)
3121            .metadata(TRANSPORT_PREFERENCE_KEY, TRANSPORT_PREFERENCE_INTERNET)
3122            .requires_ack(true);
3123
3124        if let Some(meta) = media_metadata {
3125            builder = builder.media_metadata(meta);
3126        }
3127
3128        Ok(builder.build())
3129    }
3130
3131    // ========================================================================
3132    // OUTBOX
3133    // ========================================================================
3134
3135    pub(super) fn ensure_outbox_entry(&mut self, message: &Message) {
3136        if !message.requires_ack {
3137            return;
3138        }
3139
3140        let is_media = Self::is_media_outbox_message(message);
3141        let (outbox, capacity) = if is_media {
3142            use crate::constants::MAX_MEDIA_OUTBOX_ENTRIES;
3143            (&mut self.media_outbox, MAX_MEDIA_OUTBOX_ENTRIES)
3144        } else {
3145            (&mut self.outbox, MAX_OUTBOX_ENTRIES)
3146        };
3147
3148        if !outbox.contains_key(&message.id) && outbox.len() >= capacity {
3149            let oldest_id = outbox
3150                .iter()
3151                .min_by_key(|(_, entry)| entry.last_sent_at)
3152                .map(|(id, _)| id.clone());
3153            if let Some(oldest_id) = oldest_id {
3154                // Retired, not just removed: this function is also how the
3155                // retry and flush paths re-create an entry before resending,
3156                // so a retry entry left behind here would bring the message
3157                // back and evict another in its place.
3158                let attempt_count = self
3159                    .retire_undeliverable_message(&oldest_id, "outbox eviction")
3160                    .map_or(0, |entry| entry.attempt_count);
3161                if !is_media {
3162                    // Capacity eviction is as terminal as expiry: the entry
3163                    // and its persisted copy are gone, so without an event
3164                    // the app shows the message as pending forever. (Media
3165                    // chunks surface through the transfer abort above.)
3166                    self.emit_event(Event::message_failed(
3167                        oldest_id.clone(),
3168                        "Outbox capacity exceeded",
3169                        attempt_count,
3170                    ));
3171                    if let Some(recipient) = self.take_undeliverable_connection_request(&oldest_id)
3172                    {
3173                        warn!(
3174                            recipient = %recipient,
3175                            message_id = %oldest_id,
3176                            "Connection request undeliverable: outbox capacity exceeded"
3177                        );
3178                        self.emit_event(Event::connection_request_undeliverable(
3179                            recipient,
3180                            oldest_id.as_str(),
3181                            "outbox_capacity_exceeded",
3182                        ));
3183                    }
3184                }
3185            }
3186        }
3187
3188        // Consume staged re-seal provenance only when first creating a
3189        // (non-media) entry here (the failure/retry path); later calls find the
3190        // entry already present and leave its `reseal` intact.
3191        let staged_reseal = self.take_staged_reseal(message, is_media);
3192        let outbox = if is_media {
3193            &mut self.media_outbox
3194        } else {
3195            &mut self.outbox
3196        };
3197        let is_new = !outbox.contains_key(&message.id);
3198        outbox
3199            .entry(message.id.clone())
3200            .or_insert_with(|| OutboxEntry {
3201                message: message.clone(),
3202                attempt_count: 0,
3203                first_sent_at: Utc::now(),
3204                last_sent_at: Utc::now(),
3205                last_transport: None,
3206                reseal: staged_reseal,
3207                relay_pushed: false,
3208            });
3209        // Persist newly-created main-outbox entries so they survive a restart.
3210        // media entries are intentionally not persisted.
3211        if is_new && !is_media {
3212            if let Some(entry) = self.outbox.get(&message.id) {
3213                self.persist_outbox_entry(entry);
3214            }
3215        }
3216    }
3217
3218    pub(super) fn mark_message_sent(
3219        &mut self,
3220        message: &Message,
3221        transport: Option<TransportType>,
3222        attempt_hint: Option<u32>,
3223    ) {
3224        if !message.requires_ack {
3225            return;
3226        }
3227
3228        let now = Utc::now();
3229        let is_media = Self::is_media_outbox_message(message);
3230        // Consume the staged re-seal provenance only when first creating a
3231        // (non-media) outbox entry; on later attempts the entry already carries
3232        // it. Staging for media is never populated.
3233        let staged_reseal = self.take_staged_reseal(message, is_media);
3234        let outbox = if is_media {
3235            &mut self.media_outbox
3236        } else {
3237            &mut self.outbox
3238        };
3239        let is_new = !outbox.contains_key(&message.id);
3240        let entry = outbox
3241            .entry(message.id.clone())
3242            .or_insert_with(|| OutboxEntry {
3243                message: message.clone(),
3244                attempt_count: 0,
3245                first_sent_at: now,
3246                last_sent_at: now,
3247                last_transport: transport,
3248                reseal: staged_reseal,
3249                relay_pushed: false,
3250            });
3251
3252        entry.message = message.clone();
3253        if entry.attempt_count == 0 {
3254            entry.first_sent_at = now;
3255        }
3256        entry.attempt_count = attempt_hint.unwrap_or(entry.attempt_count.saturating_add(1));
3257        entry.last_sent_at = now;
3258        entry.last_transport = transport;
3259
3260        // Persist only when the entry is newly created here. The success path
3261        // never calls ensure_outbox_entry, so this is the one point a
3262        // successfully-sent message first enters the durable outbox before its
3263        // ACK. Later attempts don't re-persist: the durable copy already exists
3264        // and attempt/timestamp churn isn't restore-critical (the TTL is
3265        // refreshed on restore anyway), which keeps secure-storage writes off
3266        // the retry hot path. Media entries are never persisted.
3267        if is_new && !is_media {
3268            if let Some(entry) = self.outbox.get(&message.id) {
3269                self.persist_outbox_entry(entry);
3270            }
3271        }
3272    }
3273
3274    /// Stages re-seal provenance for a message about to be dispatched, so the
3275    /// outbox entry created during dispatch carries it. Callers gate on the
3276    /// content being a sealed encrypted DM (`__MLS_ENC__`); media and plaintext
3277    /// never stage and thus always replay verbatim.
3278    pub(super) fn stage_outbox_reseal(&mut self, id: &MessageId, reseal: OutboxReseal) {
3279        self.pending_reseal.insert(id.clone(), reseal);
3280    }
3281
3282    /// Consumes any staged re-seal provenance for `message`, attaching it to a
3283    /// (non-media) main-outbox entry being created for the first time. On later
3284    /// attempts the entry already exists and carries its `reseal`, so this
3285    /// returns `None`. Media never stages.
3286    ///
3287    /// **Always removes the staged copy** (it does not leave it behind when it
3288    /// returns `None`): the staging map is strictly transient and must never
3289    /// outlive its message, or a message that stages but is then dropped before
3290    /// an outbox entry is created (e.g. `dispatch_prepared_message` early-returns
3291    /// on a duplicate id) would strand plaintext in `pending_reseal` for the
3292    /// process lifetime. Removing unconditionally here — plus the belt-and-braces
3293    /// clear in [`Self::remove_outbox_entry`] — bounds the map to genuinely
3294    /// in-flight sends. Kept as one helper so the two outbox-entry creation sites
3295    /// ([`Self::ensure_outbox_entry`] and [`Self::mark_message_sent`]) stay in
3296    /// lockstep on the consume condition.
3297    fn take_staged_reseal(&mut self, message: &Message, is_media: bool) -> Option<OutboxReseal> {
3298        let staged = self.pending_reseal.remove(&message.id);
3299        if is_media || self.outbox.contains_key(&message.id) {
3300            return None;
3301        }
3302        staged
3303    }
3304
3305    /// Re-seals an outbound encrypted DM against the recipient's *current* MLS
3306    /// session for a resend, returning fresh sealed content, or `None` to replay
3307    /// verbatim. Returns `None` when it is not a re-sealable encrypted DM, when
3308    /// there is no stored provenance (plaintext, media, or a pre-upgrade
3309    /// persisted entry), when the session is not currently confirmed, or when
3310    /// the re-seal produced identical bytes.
3311    ///
3312    /// This is the Tier 2 core: after a desync + re-key the recipient's session
3313    /// is rebuilt at a new epoch, so replaying the ciphertext sealed at the old
3314    /// epoch is undecryptable forever. Re-sealing here rebuilds only the
3315    /// ciphertext `content`; the outer `Message.id` is preserved so the
3316    /// receiver's dedup and ACK correlation are unaffected. Gating on a
3317    /// confirmed session guarantees `prepare_outbound_content` takes the
3318    /// encrypt-now path and never the `store_pending` queueing path.
3319    fn reseal_resend_content(&mut self, message: &Message) -> Option<String> {
3320        if !message.content.starts_with(internal_prefixes::ENCRYPTED) {
3321            return None;
3322        }
3323        let recipient = message.recipient.as_str().to_string();
3324        if !self.confirmed_sessions.contains(&recipient) {
3325            return None;
3326        }
3327        let reseal = self.outbox.get(&message.id)?.reseal.as_ref()?.clone();
3328        match self.prepare_outbound_content(
3329            &recipient,
3330            &reseal.content,
3331            reseal.priority,
3332            reseal.reply_to_msg.clone(),
3333            reseal.forwarded_from.clone(),
3334            reseal.content_type,
3335            reseal.media_metadata.clone(),
3336            reseal.rich.as_ref(),
3337            Some(&message.app_id),
3338            Some(PendingProvenance::for_id(message.id.clone())),
3339            "resend_reseal",
3340        ) {
3341            Ok(OutboundSendPreparation::Ready(sealed)) if sealed != message.content => Some(sealed),
3342            // Identical bytes, or an error: replay verbatim this round. Tier 1
3343            // keeps the sender retrying until the session heals.
3344            //
3345            // The `Queued` arm is all but unreachable — the `confirmed_sessions`
3346            // gate above runs synchronously with no yield before the encrypt, so
3347            // the session cannot flip to not-ready in between. If it ever does
3348            // (e.g. `confirmed_sessions` outlived an externally-deleted MLS
3349            // session), `prepare_outbound_content` has enqueued a pending message
3350            // as a side effect, keyed by our SAME `existing_id`. That is benign:
3351            // this round still replays the verbatim outbox bytes, and when the
3352            // pending copy later flushes, the sender's own `dispatch_prepared_message`
3353            // dedup drops it as a duplicate id. So no double-delivery, just a
3354            // transient pending entry that self-clears on flush.
3355            _ => None,
3356        }
3357    }
3358
3359    /// Re-seals `message` in place against the current session if it is a
3360    /// re-sealable encrypted DM (see [`Self::reseal_resend_content`]). Used at
3361    /// the resend transmit points so the bytes that go out — and the outbox copy
3362    /// `mark_message_sent` then stores — are freshly sealed.
3363    pub(super) fn reseal_for_resend_in_place(&mut self, message: &mut Message) {
3364        if let Some(fresh) = self.reseal_resend_content(message) {
3365            message.content = fresh;
3366        }
3367    }
3368
3369    pub(super) fn remove_outbox_entry(&mut self, message_id: &MessageId) -> Option<OutboxEntry> {
3370        // Belt-and-braces: drop any still-staged re-seal provenance for this id
3371        // so it never outlives the message (see `take_staged_reseal`). Normally
3372        // staging is consumed at entry creation; this covers the id being torn
3373        // down before that ever happened.
3374        self.pending_reseal.remove(message_id);
3375        self.undecryptable_resent.remove(message_id);
3376        // A receipt names an outbox entry; with the entry gone it names
3377        // nothing, and keeping it would only grow the map.
3378        self.custody_receipts.remove(message_id);
3379        if let Some(entry) = self.outbox.remove(message_id) {
3380            if !Self::is_confirmation_probe(&entry.message) {
3381                self.clear_outbox_entry_from_storage(message_id);
3382            }
3383            return Some(entry);
3384        }
3385        self.media_outbox.remove(message_id)
3386    }
3387
3388    /// Takes `message_id` out of the three places that can send it again:
3389    /// the retry queue, the pending-ACK tracker and the outbox. One without
3390    /// the others resurrects it (see [`Self::retire_undeliverable_message`]).
3391    /// No failure is recorded against the carrier: that is the give-up
3392    /// path's, and a message withdrawn for being superseded did not fail.
3393    pub(super) fn forget_outbound_message(
3394        &mut self,
3395        message_id: &MessageId,
3396    ) -> Option<OutboxEntry> {
3397        self.retry_queue.remove(&message_id.as_str());
3398        self.ack_manager.remove_ack(message_id);
3399        self.remove_outbox_entry(message_id)
3400    }
3401
3402    /// Takes a message this device has given up on out of everything that
3403    /// could send it again, returning its outbox entry if it still had one.
3404    ///
3405    /// A settlement is only terminal if nothing can bring the message back
3406    /// afterwards, and three things can: the retry queue, the pending-ACK
3407    /// tracker (whose timeout re-queues it), and the outbox. The retry and
3408    /// flush paths re-create a missing outbox entry before resending, with
3409    /// fresh clocks, so an id dropped from the outbox but left queued for
3410    /// retry is never expired by anything. At capacity each of those resends
3411    /// evicted a live message to make room, and that message's own retry entry
3412    /// did the same: a device holding more than a full outbox of undeliverable
3413    /// messages reported a "terminal" `message_failed` on every retry,
3414    /// indefinitely, for ids it had already settled. An id left in the ACK
3415    /// tracker is settled a second time when its timeout finds no entry.
3416    ///
3417    /// Every give-up path calls this, so the teardown is written once. It
3418    /// emits nothing: which event a drop owes the application differs by
3419    /// path, and stays with the caller. `media_reason` is what the aborted
3420    /// transfer reports when the id is a media chunk.
3421    pub(super) fn retire_undeliverable_message(
3422        &mut self,
3423        message_id: &MessageId,
3424        media_reason: &str,
3425    ) -> Option<OutboxEntry> {
3426        self.handle_outbound_media_chunk_failed(message_id, media_reason);
3427        let entry = self.forget_outbound_message(message_id);
3428        if let Some(transport) = entry.as_ref().and_then(|entry| entry.last_transport) {
3429            self.transport_manager.record_delivery_failure(transport);
3430        }
3431        entry
3432    }
3433
3434    /// Settles the pending connection-request entry for `message_id` when a
3435    /// terminal drop occurs (max retries, outbox expiry, capacity eviction),
3436    /// returning the recipient the caller should emit
3437    /// `connection_request_undeliverable` for. The TTL gates emission — a
3438    /// signal that stale belongs to a request the app has long stopped
3439    /// waiting on — but the entry is removed either way.
3440    pub(super) fn take_undeliverable_connection_request(
3441        &mut self,
3442        message_id: &MessageId,
3443    ) -> Option<String> {
3444        self.pending_connection_requests
3445            .remove(&message_id.as_str())
3446            .filter(|pending| pending.sent_at.elapsed() <= PENDING_CONNECTION_REQUEST_TTL)
3447            .map(|pending| pending.recipient)
3448    }
3449
3450    /// Removes messages that have waited too long for MLS session
3451    /// establishment. This gives the pre-outbox queue the same bounded
3452    /// lifecycle guarantee as dispatched messages.
3453    pub(super) fn cleanup_expired_pending_messages(&mut self) {
3454        self.cleanup_expired_pending_messages_at(Utc::now());
3455    }
3456
3457    /// Runs pending-message expiry only when the earliest queued deadline is
3458    /// due. Bindings call `process()` every 100 ms, so blindly scanning the
3459    /// entire queue there wastes CPU and battery for almost the whole default
3460    /// seven-day lifetime.
3461    pub(super) fn cleanup_expired_pending_messages_if_due(&mut self) {
3462        let now = Utc::now();
3463        if self
3464            .next_pending_message_expiry
3465            .is_some_and(|expiry| now >= expiry)
3466        {
3467            self.cleanup_expired_pending_messages_at(now);
3468        }
3469    }
3470
3471    fn cleanup_expired_pending_messages_at(&mut self, now: DateTime<Utc>) {
3472        let lifetime_ms = self
3473            .config
3474            .reliability
3475            .retry
3476            .pending_message_max_lifetime_ms;
3477        let mut changed_recipients = Vec::new();
3478        let mut expired_ids = Vec::new();
3479
3480        for (recipient, messages) in &mut self.pending_encrypted_messages {
3481            let previous_len = messages.len();
3482            messages.retain(|pending| {
3483                // Bounded per pass: each expiry costs a durable delete, and a
3484                // burst of entries queued together comes due together. The
3485                // remainder stays queued past its deadline and drains on the
3486                // next tick — see `MAX_PENDING_EXPIRIES_PER_PASS`.
3487                if expired_ids.len() < MAX_PENDING_EXPIRIES_PER_PASS
3488                    && lifetime_expired(now, pending.queued_at, lifetime_ms)
3489                {
3490                    expired_ids.push(pending.message_id.clone());
3491                    false
3492                } else {
3493                    true
3494                }
3495            });
3496            if messages.len() != previous_len {
3497                changed_recipients.push(recipient.clone());
3498            }
3499        }
3500
3501        for recipient in changed_recipients {
3502            if self
3503                .pending_encrypted_messages
3504                .get(&recipient)
3505                .is_some_and(Vec::is_empty)
3506            {
3507                self.pending_encrypted_messages.remove(&recipient);
3508            }
3509        }
3510        // The survivors' records are untouched, so only the expired entries are
3511        // written to storage — one delete each, paired with the settlement
3512        // below.
3513        self.delete_pending_messages_from_storage(expired_ids.iter());
3514        self.recompute_next_pending_message_expiry();
3515
3516        for message_id in expired_ids {
3517            // Reached both from `process()` (emits immediately) and from
3518            // restore, which runs before the event pipeline is live.
3519            self.settle_restored_message_failure(Event::message_failed(
3520                message_id,
3521                "Pending session lifetime exceeded",
3522                0,
3523            ));
3524        }
3525    }
3526
3527    pub(super) fn cleanup_outbox(&mut self) {
3528        let now = Utc::now();
3529        let lifetime_ms = self.config.reliability.retry.outbox_max_lifetime_ms;
3530        // In-process twin of the restore path's absolute cap
3531        // (`restore_outbox`): the carrier-relative window slides on every
3532        // send, and an unreachable-parked DM's mesh reachability probe keeps
3533        // sending — without an absolute bound from `first_sent_at`, a
3534        // long-lived process would keep such an entry alive forever.
3535        let absolute_lifetime_ms =
3536            lifetime_ms.saturating_mul(crate::constants::OUTBOX_ABSOLUTE_LIFETIME_FACTOR as u64);
3537
3538        let mut expired_from_outbox = Vec::new();
3539        for (message_id, entry) in &self.outbox {
3540            // A relay-pushed entry's fixed window is a hard stop: it does not
3541            // wait out an in-flight probe, whose re-sends are exactly what
3542            // the window exists to end.
3543            let push_window_closed = super::relay_push_window_closed(now, entry);
3544            if !push_window_closed
3545                && !lifetime_expired(now, entry.last_sent_at, lifetime_ms)
3546                && !lifetime_expired(now, entry.first_sent_at, absolute_lifetime_ms)
3547            {
3548                continue;
3549            }
3550            if !push_window_closed
3551                && entry.message.requires_ack
3552                && self.ack_manager.is_waiting_for_ack(&entry.message.id)
3553            {
3554                continue;
3555            }
3556            expired_from_outbox.push((message_id.clone(), entry.attempt_count, entry.relay_pushed));
3557        }
3558        for (message_id, attempt_count, relay_pushed) in expired_from_outbox {
3559            self.retire_undeliverable_message(&message_id, "outbox lifetime exceeded");
3560
3561            // Outcome unknown, so nothing app-facing: the push may already
3562            // have delivered it (the `park_relay_pushed_dm` contract), and
3563            // `MessageSent` already told the app it left. `message_failed`
3564            // would invite a manual resend under a fresh id that the
3565            // recipient's dedup cannot collapse. Only plain DMs are ever
3566            // flagged, so there is no connection request to settle either.
3567            if relay_pushed {
3568                debug!(
3569                    message_id = %message_id,
3570                    "Relay-pushed DM reached its redelivery window; dropping it silently"
3571                );
3572                continue;
3573            }
3574
3575            // Expiry is terminal: without an event the app shows the message
3576            // as pending forever. Mirrors handle_max_retries_exceeded, which
3577            // settles the same way when the retry budget runs out.
3578            self.emit_event(Event::message_failed(
3579                message_id.clone(),
3580                "Outbox lifetime exceeded",
3581                attempt_count,
3582            ));
3583            if let Some(recipient) = self.take_undeliverable_connection_request(&message_id) {
3584                warn!(
3585                    recipient = %recipient,
3586                    message_id = %message_id,
3587                    "Connection request undeliverable: outbox lifetime exceeded"
3588                );
3589                self.emit_event(Event::connection_request_undeliverable(
3590                    recipient,
3591                    message_id.as_str(),
3592                    "outbox_lifetime_exceeded",
3593                ));
3594            }
3595        }
3596
3597        // Only peers that still hold outbox entries can retain an
3598        // unreachable-park counter (bounds the map at the outbox cap).
3599        if !self.dm_unreachable_parks.is_empty() {
3600            let active: HashSet<&str> = self
3601                .outbox
3602                .values()
3603                .map(|entry| entry.message.recipient.as_str())
3604                .collect();
3605            self.dm_unreachable_parks
3606                .retain(|peer, _| active.contains(peer.as_str()));
3607        }
3608
3609        let mut expired_from_media = Vec::new();
3610        for (message_id, entry) in &self.media_outbox {
3611            if !lifetime_expired(now, entry.last_sent_at, lifetime_ms) {
3612                continue;
3613            }
3614            if entry.message.requires_ack && self.ack_manager.is_waiting_for_ack(&entry.message.id)
3615            {
3616                continue;
3617            }
3618            expired_from_media.push(message_id.clone());
3619        }
3620        for message_id in expired_from_media {
3621            self.retire_undeliverable_message(&message_id, "outbox lifetime exceeded");
3622        }
3623    }
3624
3625    pub(super) fn ensure_ack_registration(&mut self, message: &Message) -> Result<bool> {
3626        if !message.requires_ack {
3627            return Ok(false);
3628        }
3629
3630        if self.ack_manager.is_waiting_for_ack(&message.id) {
3631            Ok(false)
3632        } else {
3633            if let Some(eviction) = self
3634                .ack_manager
3635                .register_pending_ack(message.id.clone(), None)?
3636            {
3637                self.emit_event(Event::ack_evicted(
3638                    eviction.message_id,
3639                    eviction.priority.as_str(),
3640                    "capacity".to_string(),
3641                ));
3642            }
3643            Ok(true)
3644        }
3645    }
3646
3647    pub(super) fn is_media_outbox_message(message: &Message) -> bool {
3648        message.content_type == ContentType::FileChunk
3649    }
3650
3651    /// A session confirmation probe lives in memory only. It carries no user
3652    /// data, and a restarted process does not know which probe was the last,
3653    /// so a restored one could never be superseded; the next scan asks the
3654    /// same question. Persisting it cost a secure-storage write per probe and
3655    /// a delete per supersede, under the protocol lock.
3656    pub(super) fn is_confirmation_probe(message: &Message) -> bool {
3657        message
3658            .content
3659            .starts_with(internal_prefixes::SESSION_CONFIRM_PROBE)
3660    }
3661
3662    // ========================================================================
3663    // DELIVERY TRACKING
3664    // ========================================================================
3665
3666    /// Handles successful message send.
3667    /// Whether a frame for `peer_id` can arrive without a neighbour carrying
3668    /// it, given what this device knows about *this recipient*.
3669    ///
3670    /// [`TransportManager::can_reach_without_carrying`] answers the same
3671    /// question from carrier status alone, and so says yes for every recipient
3672    /// while any infrastructure carrier is up. This is that answer with the
3673    /// per-recipient facts applied: a carrier that has already told us it
3674    /// cannot reach this recipient stops counting as a way to reach them.
3675    ///
3676    /// Two properties hold by construction:
3677    ///
3678    /// - **A carrier holding no fact is unchanged.** Nostr cannot report
3679    ///   per-recipient delivery at all, so it never produces a fact and keeps
3680    ///   its blanket claim. That is deliberate: the mixed-neighbourhood
3681    ///   residual stays open for Nostr-only devices rather than closing by
3682    ///   accident under a design that never gave them a verdict to act on.
3683    /// - **A live mesh link always wins.** It is ground truth rather than a
3684    ///   claim, and it is checked whether or not any fact exists.
3685    pub(crate) fn can_reach_recipient(&self, peer_id: &str) -> bool {
3686        let now = Instant::now();
3687        let infrastructure_reaches_peer = self
3688            .transport_manager
3689            .available_infrastructure_carriers()
3690            .into_iter()
3691            .any(|carrier| {
3692                !matches!(
3693                    self.reachability.claim_for(peer_id, carrier, now),
3694                    Some(Claim::Unreachable)
3695                )
3696            });
3697        infrastructure_reaches_peer || self.transport_manager.mesh_can_address(peer_id)
3698    }
3699
3700    pub(super) fn handle_send_success(
3701        &mut self,
3702        message: &Message,
3703        transport: Option<TransportType>,
3704    ) -> Result<()> {
3705        self.mark_message_sent(message, transport, Some(1));
3706        self.ensure_ack_registration(message)?;
3707
3708        // A transport reporting success is not always evidence the frame can
3709        // arrive. The internet transport and Reticulum accept any recipient
3710        // and return `Ok`, so a send to someone neither can reach is reported
3711        // as sent and never arrives, and because the mesh hand-off used to hang
3712        // off the *failure* path, a device with either carrier up would swallow
3713        // the frame instead of asking its neighbors to carry it. That is precisely
3714        // the out-of-range case forwarding exists for, so the question is asked
3715        // directly rather than inferred from an error that never comes.
3716        //
3717        // Offering here is additive, never a replacement: the outbox entry and
3718        // the ACK registration above stand either way, and if the accepting
3719        // carrier did deliver after all, the recipient's deduplicator absorbs
3720        // the second copy.
3721        if !self.can_reach_recipient(message.recipient.as_str()) {
3722            let handed_to_mesh = self.offer_to_mesh(message);
3723            if handed_to_mesh > 0 {
3724                debug!(
3725                    message_id = %message.id,
3726                    recipient = %message.recipient,
3727                    handed_to_mesh,
3728                    "Transport accepted a recipient it cannot reach; also handed to neighbors"
3729                );
3730            }
3731        }
3732        Ok(())
3733    }
3734
3735    /// Handles failed message send by persisting to outbox and scheduling retry.
3736    ///
3737    /// EDGE CASE HANDLING:
3738    /// - Ensures message is persisted to outbox for recovery
3739    /// - Schedules retry with exponential backoff
3740    /// - Handles case where all transports are unavailable
3741    ///
3742    /// NOTE: This does NOT call `record_retry_failure` — callers that need it
3743    /// (e.g. `send_via_forced_transport`) must record the failure themselves.
3744    /// `TransportManager::send()` already records failures internally, so
3745    /// calling it here would double-count.
3746    /// Returns the absolute time the retry was scheduled for (`None` when the
3747    /// message was already queued for retry), so callers can surface it in
3748    /// `MessageDeferred` instead of guessing.
3749    pub(super) fn handle_send_failure(
3750        &mut self,
3751        message: &Message,
3752        transport: Option<TransportType>,
3753    ) -> Result<Option<DateTime<Utc>>> {
3754        // Ensure message is persisted to outbox for recovery
3755        self.ensure_outbox_entry(message);
3756
3757        // No transport could reach the recipient directly — but a neighbor
3758        // might be able to, or know someone who can. Offer the frame to the
3759        // mesh before settling for a retry: this is the case the whole
3760        // forwarding path exists for, someone out of our range but inside the
3761        // crowd's.
3762        //
3763        // The outbox entry and retry stay in place regardless. Handing a frame
3764        // to neighbors is not proof it arrived, and the acknowledgement coming
3765        // back is what settles it.
3766        let handed_to_mesh = self.offer_to_mesh(message);
3767
3768        // Schedule retry (enqueue is infallible — no attempt limit)
3769        let next_retry_at = self.retry_queue.enqueue(message.clone(), 0);
3770
3771        warn!(
3772            message_id = %message.id,
3773            transport = ?transport,
3774            handed_to_mesh,
3775            "Deferred message due to send error"
3776        );
3777        Ok(next_retry_at)
3778    }
3779
3780    /// The mesh offer for a **resend**: the retry queue and both outbox
3781    /// flushes. A resend asks the same question a first send does, so it gets
3782    /// the same answer as [`Self::handle_send_success`] and
3783    /// [`Self::handle_send_failure`]: a send no carrier took is offered to the
3784    /// neighbors, and so is one a carrier took for a recipient it cannot
3785    /// reach.
3786    ///
3787    /// Without this a message whose first attempt a carrier accepted and lost
3788    /// (a stream that was already dead and had not noticed) never reached the
3789    /// mesh at all: by the time its acknowledgement timed out the direct link
3790    /// was gone, every resend was refused, and each refusal only went back on
3791    /// the retry queue while a neighbor that could reach the recipient sat
3792    /// unused for the life of the outbox entry.
3793    ///
3794    /// Re-offering on every resend is bounded for the reasons the park path
3795    /// gives: the retry backoff bounds how often we ask, each offer spends the
3796    /// own-send tokens any other frame of ours does, and a neighbor that took
3797    /// the frame carries our re-send again only within its per-neighbor
3798    /// allowance for us (it has to: the recipient re-acknowledges only a
3799    /// re-send that reaches it). Every device further on still suppresses the
3800    /// id for the `RelaySeenCache` retention window. Excluding neighbors we already handed it to
3801    /// would be wrong, because a neighbor records an id only when it *accepts*
3802    /// the frame, and one that refused it (rate limit, full queue) is exactly
3803    /// the one a later offer should reach.
3804    pub(super) fn offer_resend_to_mesh(&mut self, message: &Message, accepted: bool) -> usize {
3805        if !self.needs_mesh_copy(message.recipient.as_str(), accepted) {
3806            return 0;
3807        }
3808        let handed_to_mesh = self.offer_to_mesh(message);
3809        if handed_to_mesh > 0 {
3810            debug!(
3811                message_id = %message.id,
3812                recipient = %message.recipient,
3813                accepted,
3814                handed_to_mesh,
3815                "Resend could not reach the recipient directly; handed it to neighbors"
3816            );
3817        }
3818        handed_to_mesh
3819    }
3820
3821    /// Whether a frame for `recipient` that a carrier did (`accepted`) or did
3822    /// not take must also be handed to the neighbours.
3823    ///
3824    /// The one rule for a resend and a handshake frame: yes when no carrier
3825    /// took it, and yes when one took it for a recipient that
3826    /// [`Self::can_reach_recipient`] says no carrier reaches, because a
3827    /// carrier's `Ok` is not delivery. Two copies of it drifting apart would
3828    /// leave one path swallowing frames the other carries.
3829    fn needs_mesh_copy(&self, recipient: &str, accepted: bool) -> bool {
3830        !accepted || !self.can_reach_recipient(recipient)
3831    }
3832
3833    /// Hands a locally-originated frame to nearby devices so it can travel
3834    /// toward a recipient we cannot reach ourselves.
3835    ///
3836    /// Returns how many neighbors took a copy. Zero means we are alone, or
3837    /// every neighbor refused the link — in both cases the retry ladder and the
3838    /// outbox remain the recovery path, exactly as before.
3839    ///
3840    /// Unlike a forwarded frame, this one is not held back: the delay before
3841    /// forwarding exists so neighbors holding the *same* frame do not all
3842    /// transmit at once, and nobody else is holding this one yet.
3843    pub(super) fn offer_to_mesh(&mut self, message: &Message) -> usize {
3844        // Deliberately not gated on `allow_relay`. That setting is about what
3845        // this device does for *other people* — whether it spends its battery
3846        // carrying their traffic. This is the device's own message, and the
3847        // ones it must be able to send include the acknowledgement for a
3848        // message it just received. Gating it here would leave a
3849        // relay-declining device unable to answer anything that reached it
3850        // across the mesh, so its sender would retransmit to exhaustion and
3851        // report a failure for a message that was delivered and read.
3852
3853        // Never hand a self-addressed frame to the mesh. Those are the relay
3854        // hint frames (`__GRP_RELAY_REG__`, `__GRP_RELAY_BCAST__`), which mean
3855        // something only to our own bridge: a neighbor receiving one would see
3856        // a frame addressed to somebody else and carry it further, spending
3857        // airtime to deliver a control op nobody can act on. They are pinned to
3858        // Internet for the same reason.
3859        if message.recipient.as_str() == self.local_id {
3860            return 0;
3861        }
3862
3863        let neighbors = self.transport_manager.mesh_neighbors();
3864        if neighbors.is_empty() {
3865            return 0;
3866        }
3867
3868        // Ours to originate: record it as handled so a copy coming back to us
3869        // through the mesh is recognized and not forwarded again.
3870        self.mesh_relay.mark_handled(&message.id.as_str());
3871
3872        // If the recipient is standing right there, hand it to them. Reached
3873        // here it means the ordinary send could not use that link, but trying
3874        // it costs one call and beats routing around the destination.
3875        let recipient = message.recipient.as_str();
3876        let targets = if neighbors.iter().any(|n| n.peer_id == recipient) {
3877            vec![recipient.to_string()]
3878        } else {
3879            // Full width regardless of this device's battery. Capability bias
3880            // narrows the fan-out for *forwarded* frames, where neighbors hold
3881            // copies and the sender is still retrying so the cost is
3882            // redundancy. This frame is ours and nobody else is holding it, so
3883            // the fan-out is the delivery attempt itself — and since targets
3884            // are chosen stably per message id, a narrowed set would be the
3885            // same narrowed set on every retransmission.
3886            self.mesh_relay.select_origination_targets(
3887                neighbors
3888                    .iter()
3889                    .map(|n| (n.peer_id.as_str(), n.link_quality())),
3890                &[],
3891                &message.id.as_str(),
3892            )
3893        };
3894
3895        // The deposit request (`docs/spec/custody.md`, "What the depositor
3896        // writes"): a class token on this device's own sealed replication
3897        // frame, judged from the plaintext retained for re-sealing, written
3898        // only here, where the frame is handed to proven mesh neighbours
3899        // because the recipient is out of reach. Never toward a custodian
3900        // whose receipt for this frame is still live.
3901        let custody_class = self.custody_class_for(message);
3902
3903        let mut handed_to = 0usize;
3904        for target in targets {
3905            // Metered against the same ceiling as carrying other people's
3906            // traffic: it is the same radio. Without this a large transfer,
3907            // which offers every chunk separately, would put an unbounded burst
3908            // on the air in one call.
3909            //
3910            // Own traffic spends from the reserve that forwarding leaves alone,
3911            // so a device carrying the neighborhood at its ceiling can still
3912            // get its own frames out — an acknowledgement above all, whose
3913            // absence makes a delivered message look lost to its sender.
3914            if !self.mesh_relay.take_own_send_token() {
3915                debug!(
3916                    message_id = %message.id,
3917                    "At the forwarding limit; not handing this to further neighbors"
3918                );
3919                break;
3920            }
3921
3922            let deposit;
3923            let frame: &Message = match custody_class {
3924                Some(class) if !self.custody_suppressed_toward(&message.id, &target) => {
3925                    let mut stamped = message.clone();
3926                    stamped
3927                        .metadata
3928                        .insert(CUSTODY_META_KEY.to_string(), class.to_string());
3929                    deposit = stamped;
3930                    &deposit
3931                }
3932                _ => message,
3933            };
3934
3935            match self.transport_manager.send_to_neighbor(&target, frame) {
3936                Ok(transport) => {
3937                    handed_to += 1;
3938                    debug!(
3939                        message_id = %message.id,
3940                        recipient = %message.recipient,
3941                        next_hop = %target,
3942                        transport = ?transport,
3943                        deposit = frame.metadata.contains_key(CUSTODY_META_KEY),
3944                        "Handed message to a neighbor to carry"
3945                    );
3946                }
3947                Err(err) => {
3948                    debug!(
3949                        message_id = %message.id,
3950                        next_hop = %target,
3951                        error = %err,
3952                        "Neighbor could not take the message"
3953                    );
3954                }
3955            }
3956        }
3957
3958        handed_to
3959    }
3960
3961    /// Puts a session-handshake frame (a key package or a Welcome) on its
3962    /// way, returning how many neighbours took a copy.
3963    ///
3964    /// Invariant: a handshake frame for a peer that no carrier is known to
3965    /// reach is handed to the mesh, whatever the direct send returned. A
3966    /// carrier's `Ok` is not that knowledge. The internet transport enqueues
3967    /// for any recipient, the one the relay has already said it cannot reach
3968    /// included, and Reticulum accepts for routing. Offering only when the
3969    /// direct send failed left a device with either carrier up unable to start
3970    /// a session with a peer only the mesh reaches: the carrier took every key
3971    /// package and Welcome and delivered none, and the message waiting on the
3972    /// session never left. This is the question
3973    /// [`Self::handle_send_success`] asks of every ordinary message, asked of
3974    /// the two frames that do not go through it.
3975    ///
3976    /// Additive, as there: a copy that also arrives through the carrier is a
3977    /// duplicate the receiver already absorbs. `Err` only when no carrier
3978    /// accepted the frame and no neighbour took it.
3979    pub(super) fn send_or_carry_handshake(&mut self, message: &Message) -> Result<usize> {
3980        let direct = self.transport_manager.send(message);
3981        let handed_to_mesh = if self.needs_mesh_copy(message.recipient.as_str(), direct.is_ok()) {
3982            self.offer_to_mesh(message)
3983        } else {
3984            0
3985        };
3986        if handed_to_mesh > 0 {
3987            debug!(
3988                recipient = %message.recipient,
3989                message_id = %message.id,
3990                handed_to_mesh,
3991                carrier_accepted = direct.is_ok(),
3992                "Handshake frame handed to neighbours to carry"
3993            );
3994        }
3995        match direct {
3996            Ok(()) => Ok(handed_to_mesh),
3997            Err(_) if handed_to_mesh > 0 => Ok(handed_to_mesh),
3998            Err(err) => Err(err),
3999        }
4000    }
4001
4002    /// Confirms that a message was successfully sent by the transport layer.
4003    pub fn on_transport_send_confirmed(&mut self, message_id: &str) -> Result<()> {
4004        let Some(peer_id) = self.find_welcome_peer_by_message_id(message_id) else {
4005            return Ok(());
4006        };
4007
4008        let updated = self
4009            .welcome_lifecycles
4010            .get(&peer_id)
4011            .cloned()
4012            .ok_or_else(|| Error::Other(format!("Missing welcome lifecycle for {}", peer_id)))?;
4013
4014        if matches!(
4015            updated.state,
4016            WelcomeDeliveryState::Sent | WelcomeDeliveryState::Expired
4017        ) {
4018            return Ok(());
4019        }
4020
4021        // A relay DeliveryError verdict outranks a late wire confirm: the
4022        // socket write succeeded, but the relay saw the frame and dropped it
4023        // (recipient offline). When the verdict raced ahead of this confirm,
4024        // the record is already parked `Failed`/`PeerUnreachable`; letting
4025        // the confirm proceed would resurrect the false `Sent` the verdict
4026        // just corrected — and emit welcome_send_succeeded *after* the
4027        // corrective welcome_send_failed. Any legitimate new send attempt
4028        // moves the record to `SendAttempted` first, so `Failed` here means
4029        // this confirm belongs to the very send the relay already failed.
4030        if updated.state == WelcomeDeliveryState::Failed
4031            && updated.last_reason_code == Some(crate::events::WelcomeReasonCode::PeerUnreachable)
4032        {
4033            return Ok(());
4034        }
4035
4036        // A wire confirm for a reachability probe must not upgrade the
4037        // record to `Sent`. With a live unreachable-park counter the send in
4038        // flight IS the escalating probe — every reachability edge clears the
4039        // counter before sending anything else — and the socket write
4040        // succeeding adds nothing the park didn't already know. Whenever the
4041        // relay's push fallback succeeds it returns no `DeliveryError` at
4042        // all, so a probe marked `Sent` here would go quiet forever:
4043        // `next_retry_at` cleared below, `Sent` outside every retry scan, and
4044        // `rearm_welcome_for_peer` a no-op on it — the exact edge-only dead
4045        // end the probe exists to close, reachable only through this bridge
4046        // confirm (mesh carriers never wire-confirm). Leaving the record
4047        // `SendAttempted` keeps the confirm deadline armed: a relay verdict
4048        // re-parks it, and an accepted-but-unanswered frame resolves at the
4049        // confirm timeout, which re-parks via
4050        // `welcome_probe_repark_permitted`. Genuine delivery still converges
4051        // regardless — the peer proving the session marks the welcome `Sent`
4052        // through `confirm_session_state`. This also withholds the
4053        // `welcome_send_succeeded` a probe's socket write would otherwise
4054        // emit, which the next verdict or timeout would only have to correct.
4055        if matches!(updated.state, WelcomeDeliveryState::SendAttempted)
4056            && updated.unreachable_parks > 0
4057        {
4058            return Ok(());
4059        }
4060
4061        {
4062            let record = self.welcome_lifecycles.get_mut(&peer_id).ok_or_else(|| {
4063                Error::Other(format!("Missing welcome lifecycle for {}", peer_id))
4064            })?;
4065            record.next_retry_at = None;
4066            record.last_reason_code = None;
4067            record.last_transport_error = None;
4068        }
4069        self.transition_welcome_state(&peer_id, WelcomeDeliveryState::Sent, "transport_confirmed")?;
4070
4071        let sent_snapshot = self
4072            .welcome_lifecycles
4073            .get(&peer_id)
4074            .cloned()
4075            .ok_or_else(|| Error::Other(format!("Missing welcome lifecycle for {}", peer_id)))?;
4076        if let Ok(state) = lock_shared_state(&self.shared_state) {
4077            state.emit_event(Event::welcome_send_succeeded(
4078                peer_id.clone(),
4079                sent_snapshot.welcome_message.id.as_str().to_string(),
4080                sent_snapshot.group_id,
4081                sent_snapshot.attempt,
4082            ));
4083        }
4084
4085        // The welcome is now confirmed as sent. Send a confirmation probe
4086        // immediately so the session can be confirmed without waiting for the
4087        // next process() tick — an optimistic fast-path for the common case
4088        // where the transport confirms promptly.
4089        self.send_session_confirmation_probe(&peer_id, "transport_confirmed");
4090
4091        Ok(())
4092    }
4093
4094    /// Handles asynchronous transport send failures for pending welcome
4095    /// sends and outbound connection requests.
4096    pub fn on_transport_send_failed(
4097        &mut self,
4098        message_id: &str,
4099        transport_error: Option<String>,
4100    ) -> Result<()> {
4101        self.on_transport_send_failed_via(message_id, transport_error, None)
4102    }
4103
4104    /// [`Self::on_transport_send_failed`] with the carrier that reported it.
4105    ///
4106    /// The carrier is what turns a failure into a *fact*: "the recipient is
4107    /// not reachable" is meaningless on its own, since a device may hold
4108    /// several carriers and only one of them is making the claim. Bridges that
4109    /// know which transport answered pass it; the plain form above keeps
4110    /// working for callers that do not, and records no fact rather than
4111    /// guessing one.
4112    pub fn on_transport_send_failed_via(
4113        &mut self,
4114        message_id: &str,
4115        transport_error: Option<String>,
4116        carrier: Option<TransportType>,
4117    ) -> Result<()> {
4118        // Classified at the boundary, once, and never carried past it.
4119        //
4120        // The platform bridges build this string from the relay's own
4121        // `DeliveryError.reason` (`recipient_unreachable: <relay text>`), and
4122        // the Nostr bridge from a relay's `OK` rejection text — remote-chosen,
4123        // unbounded, and downstream of here it reaches three event fields the
4124        // telemetry scrubber ships verbatim, each beside a recipient it
4125        // hashes. Even the honest relay renders the peer into its wording.
4126        //
4127        // The token itself is load-bearing and survives: the two prefix tests
4128        // below and the DM park key on `recipient_unreachable`, so classifying
4129        // to the bare token keeps every protocol decision intact and drops
4130        // only the prose. The raw string stays in this one device log.
4131        if let Some(raw) = transport_error.as_deref() {
4132            debug!(
4133                message_id = %message_id,
4134                transport_error = %raw.chars().take(256).collect::<String>(),
4135                "Platform reported a send failure"
4136            );
4137        }
4138        let transport_error = transport_error
4139            .as_deref()
4140            .map(classify_transport_send_error);
4141        // Whether the relay's mailbox holds this frame is orthogonal to why
4142        // the socket write missed, and it changes exactly one thing: a parked
4143        // plain DM needs no reachability probe, because the relay re-sends the
4144        // held copy on the recipient's next connection. So it is lifted out
4145        // here and the token normalized onto the vocabulary the rest of this
4146        // function already speaks — which is what leaves connection requests
4147        // and Welcomes on the paths they are on today.
4148        let stored = matches!(
4149            transport_error,
4150            Some(SEND_FAIL_REASON_RELAY_STORED | SEND_FAIL_REASON_RELAY_PUSHED_STORED)
4151        );
4152        let transport_error = match transport_error {
4153            Some(SEND_FAIL_REASON_RELAY_STORED) => Some(SEND_FAIL_REASON_RECIPIENT_UNREACHABLE),
4154            Some(SEND_FAIL_REASON_RELAY_PUSHED_STORED) => Some(SEND_FAIL_REASON_RELAY_PUSHED),
4155            other => other,
4156        };
4157        // A relay push is not a failure, so it is handled before either typed
4158        // branch below can read it as one: a connection request would be
4159        // fast-failed as undeliverable and a Welcome moved to `Failed`, both
4160        // for a frame the push may have delivered.
4161        if transport_error == Some(SEND_FAIL_REASON_RELAY_PUSHED) {
4162            self.park_relay_pushed_dm(message_id, carrier, stored);
4163            return Ok(());
4164        }
4165        // Connection requests first: the relay's "recipient offline"
4166        // DeliveryError is the only fast, authoritative failure signal a
4167        // request gets (there is no ACK from an offline peer — the app
4168        // would otherwise wait out the full retry budget for a generic
4169        // message_failed). Non-unreachable errors stay with the generic
4170        // retry machinery: the request is queued/retried and may still
4171        // deliver.
4172        if let Some(pending) = self.pending_connection_requests.get(message_id) {
4173            // The TTL must hold at read time too: the insert-path prune only
4174            // runs on the next `send_connection_request`, so an idle map can
4175            // hold an entry far past the correlation window — a failure
4176            // signal that stale belongs to a request the app has long
4177            // stopped waiting on and must not fire the event.
4178            if pending.sent_at.elapsed() > PENDING_CONNECTION_REQUEST_TTL {
4179                self.pending_connection_requests.remove(message_id);
4180            } else {
4181                let unreachable = transport_error == Some(SEND_FAIL_REASON_RECIPIENT_UNREACHABLE);
4182                if unreachable {
4183                    let recipient = pending.recipient.clone();
4184                    self.pending_connection_requests.remove(message_id);
4185                    warn!(
4186                        recipient = %recipient,
4187                        message_id = %message_id,
4188                        "Connection request undeliverable: recipient unreachable"
4189                    );
4190                    if let Ok(state) = lock_shared_state(&self.shared_state) {
4191                        state.emit_event(Event::connection_request_undeliverable(
4192                            recipient,
4193                            message_id.to_string(),
4194                            transport_error.unwrap_or(SEND_FAIL_REASON_TRANSPORT),
4195                        ));
4196                    }
4197                    return Ok(());
4198                }
4199            }
4200        }
4201
4202        let Some(peer_id) = self.find_welcome_peer_by_message_id(message_id) else {
4203            // Not a connection request, not a welcome: a plain DM or media
4204            // chunk. Surface the relay's verdict and park plain DMs so the
4205            // ACK retry budget stops burning against an offline peer.
4206            self.handle_recipient_unreachable_for_message(
4207                message_id,
4208                transport_error,
4209                carrier,
4210                stored,
4211            );
4212            return Ok(());
4213        };
4214        // A reason tagged "recipient_unreachable" (the internet bridge's
4215        // translation of the relay's DeliveryError) is authoritative proof
4216        // the frame was dropped: the carrier is up but *this peer* is not on
4217        // it. It must be handled even for records already wire-confirmed
4218        // (`Sent`) — the bridge confirms on socket-write success, before the
4219        // relay can answer, so the DeliveryError normally arrives when the
4220        // record is already Sent and the plain failure path below would
4221        // no-op on it, stranding a false `Sent`.
4222        let peer_unreachable = transport_error == Some(SEND_FAIL_REASON_RECIPIENT_UNREACHABLE);
4223        if peer_unreachable {
4224            if let Some(carrier) = carrier {
4225                self.reachability.record(
4226                    &peer_id,
4227                    carrier,
4228                    Claim::Unreachable,
4229                    FactSource::GatewayVerdict,
4230                    Instant::now(),
4231                );
4232            }
4233            // A held verdict that finds its Welcome already `Failed` is the
4234            // second report of one send, not a second failure. The bridges
4235            // report the id a `stored` verdict names even after that
4236            // recipient's in-flight set was drained, because for a plain DM
4237            // the second report is what retires the probe the first one
4238            // scheduled. So a Welcome anywhere but first in a burst is failed
4239            // by the drain as `recipient_unreachable`, then reported again as
4240            // `relay_stored` by its own verdict. Applied twice, the failure
4241            // climbs two rungs of the Welcome ladder for one send and tells
4242            // the app `welcome_send_failed` twice.
4243            //
4244            // `Failed` is what identifies the repeat: a resend moves the record
4245            // to `SendAttempted` before it writes, so the verdict for a fresh
4246            // send never finds it there. Holding the frame changes nothing for
4247            // a Welcome, which keeps its own ladder either way, so skipping the
4248            // repeat loses nothing. The fact above is still recorded, because
4249            // the relay did say it again.
4250            if stored
4251                && self
4252                    .welcome_lifecycles
4253                    .get(&peer_id)
4254                    .is_some_and(|record| record.state == WelcomeDeliveryState::Failed)
4255            {
4256                debug!(
4257                    peer_id = %peer_id,
4258                    message_id = %message_id,
4259                    "Held verdict for a Welcome this send already failed; not failing it twice"
4260                );
4261                return Ok(());
4262            }
4263            return self.apply_recipient_unreachable_failure(&peer_id, transport_error);
4264        }
4265        let reason = crate::events::WelcomeReasonCode::TransportUnavailable;
4266        // No raw error is available on this async path, so infer no-carrier
4267        // from live connectivity: with no transport currently available the
4268        // peer is simply unreachable and the Welcome must be kept alive, not
4269        // aged.
4270        let no_carrier = self.transport_manager.get_available_transports().is_empty();
4271        let _ = self.apply_welcome_send_failure(
4272            &peer_id,
4273            reason,
4274            transport_error,
4275            no_carrier,
4276            "transport_failed",
4277        )?;
4278        Ok(())
4279    }
4280
4281    /// Handles a relay `recipient_unreachable` verdict for a plain DM or
4282    /// media chunk: emits the non-terminal [`Event::MessageUndeliverable`],
4283    /// then *parks* plain DMs.
4284    ///
4285    /// Parking drops the pending ACK and the retry-queue entry while the
4286    /// outbox entry stays put — exactly the "stranded" state every re-drive
4287    /// edge already flushes (`flush_outbox_all` on reconnect/start,
4288    /// `flush_outbox_for_peer_via` on discovery and presence-online, the latter
4289    /// fed by [`Self::presence_watch_peers`]). Without the park, the offline
4290    /// peer's missing ACK burns the full `max_retries` budget in minutes
4291    /// (`process_timed_out_acks`) and settles a 7-day outbox message
4292    /// terminally; a re-driven send re-registers a fresh ACK, so the budget
4293    /// normally burns only against a peer believed reachable. The exception
4294    /// is the reachability probe below: its send re-enters the ACK machinery
4295    /// but may never earn a relay verdict (a mesh carrier cannot produce one
4296    /// at all), so exhaustion with a live park counter *re-parks* instead of
4297    /// settling ([`Self::try_repark_exhausted_dm`]). Settlement is reserved
4298    /// for delivery or outbox-lifetime expiry.
4299    ///
4300    /// The park always keeps a timed reachability probe whose interval
4301    /// escalates per consecutive park (15s → 600s cap, per-peer counter reset
4302    /// on any reachability edge). It is deliberately carrier-agnostic: with a
4303    /// local mesh carrier the peer may be a room away — possibly already a
4304    /// discovered neighbor, so no future edge would fire for it — and on an
4305    /// internet-only device the external edges (presence rotation, reconnect)
4306    /// are the *only* other recovery, which leaves delivery hostage to a
4307    /// bridge's polling cadence. See [`Self::park_unreachable_dm`] for why
4308    /// probing over the relay is self-limiting.
4309    ///
4310    /// Media chunks are never parked: their offline story is retry
4311    /// exhaustion → transfer abort → persisted descriptor →
4312    /// `MediaResendRequired` with app-resupplied bytes. Messages without an
4313    /// outbox entry (nothing to attribute a recipient from) are skipped.
4314    fn handle_recipient_unreachable_for_message(
4315        &mut self,
4316        message_id: &str,
4317        transport_error: Option<&'static str>,
4318        carrier: Option<TransportType>,
4319        stored: bool,
4320    ) {
4321        let Some(reason) = transport_error.filter(|r| *r == SEND_FAIL_REASON_RECIPIENT_UNREACHABLE)
4322        else {
4323            return;
4324        };
4325        let Ok(parsed_id) = MessageId::from_str(message_id) else {
4326            return;
4327        };
4328        let (entry, is_media) = match self.outbox.get(&parsed_id) {
4329            Some(entry) => (entry, false),
4330            None => match self.media_outbox.get(&parsed_id) {
4331                Some(entry) => (entry, true),
4332                None => {
4333                    if !self.note_superseded_probe_verdict(&parsed_id, carrier) {
4334                        debug!(
4335                            message_id = %message_id,
4336                            "Unreachable verdict for message without outbox entry, dropping"
4337                        );
4338                    }
4339                    return;
4340                }
4341            },
4342        };
4343        let recipient = entry.message.recipient.as_str().to_string();
4344        let attempt_count = entry.attempt_count;
4345        // A verdict for a frame the relay has already pushed is the answer to
4346        // our own probe (`OutboxEntry::relay_pushed`): the relay refuses to
4347        // notify twice, so it says `DeliveryError` for a message that may be
4348        // sitting delivered on the recipient's device. Everything below still
4349        // applies (the recipient is not on the relay, so record it, back the
4350        // probe off, and re-park), except the app-facing event: telling the
4351        // app the message is undeliverable would be exactly the claim
4352        // `park_relay_pushed_dm` exists not to make. Media is never pushed
4353        // and never parked, so the flag is only ever read off the main outbox.
4354        let relay_pushed = !is_media && entry.relay_pushed;
4355        // The verdict, recorded as a fact before anything acts on it. Parking
4356        // below reacts to *this* message; the fact is what lets the next send
4357        // to the same recipient skip a carrier that has already said no.
4358        if let Some(carrier) = carrier {
4359            self.reachability.record(
4360                &recipient,
4361                carrier,
4362                Claim::Unreachable,
4363                FactSource::GatewayVerdict,
4364                Instant::now(),
4365            );
4366        }
4367        // Media never parks, so the mesh offer the park performs for a DM has
4368        // to happen here instead. Cloned while the entry is still borrowed;
4369        // only on a verdict, so not a per-chunk cost.
4370        let media_frame = is_media.then(|| entry.message.clone());
4371        let file_id = self
4372            .outbound_media_chunks
4373            .get(&parsed_id)
4374            .map(|(file_id, _)| file_id.clone());
4375        // If this peer is also an unconfirmed-session probe target, fold the
4376        // verdict into the probe schedule so it backs off (15s -> 600s) instead
4377        // of re-probing every 5s forever. No-op for peers that are not probe
4378        // targets (e.g. an ordinary DM to a confirmed peer). Done here, after
4379        // the `entry` borrow of `self.outbox` has ended.
4380        self.note_confirmation_probe_unreachable(&recipient);
4381        if relay_pushed {
4382            debug!(
4383                message_id = %message_id,
4384                "Recipient unreachable for a relay-pushed DM: re-parking without notifying the app"
4385            );
4386        } else {
4387            warn!(
4388                message_id = %message_id,
4389                file_id = ?file_id,
4390                parked = !is_media,
4391                "Recipient unreachable for in-flight message (non-terminal)"
4392            );
4393            self.emit_event(Event::message_undeliverable(
4394                parsed_id.clone(),
4395                recipient.clone(),
4396                reason,
4397                file_id,
4398            ));
4399        }
4400        if let Some(frame) = media_frame {
4401            // The recipient is not on the relay, but they may be a few devices
4402            // away. A chunk keeps its pending ACK (media is never parked), so
4403            // an answer coming back over the mesh settles it through the
4404            // ordinary path — nothing else is needed here.
4405            let handed_to_mesh = self.offer_to_mesh(&frame);
4406            if handed_to_mesh > 0 {
4407                debug!(
4408                    message_id = %message_id,
4409                    recipient = %recipient,
4410                    handed_to_mesh,
4411                    "Relay cannot reach this chunk's recipient; handed it to neighbors"
4412                );
4413            }
4414            return;
4415        }
4416
4417        self.park_unreachable_dm(&parsed_id, &recipient, attempt_count, stored);
4418    }
4419
4420    /// Whether a platform send report also belongs in the carrier's delivery
4421    /// metrics (`Transport::report_send_failure`).
4422    ///
4423    /// Every report does except the two push tokens. The bridges hand the
4424    /// relay's `MessageSent { pushed: true }` to
4425    /// [`Self::on_transport_send_failed_via`] because that is their one call
4426    /// for "the relay answered about this id", but the relay took the frame
4427    /// into a device push, which is not a failed send. A wrapper that scores
4428    /// the report without asking here records a failure whenever the relay's
4429    /// answer beats the bridge's own write confirmation, against the carrier
4430    /// the router ranks by that ratio.
4431    ///
4432    /// `relay_stored` is not in that company: it answers a `DeliveryError`,
4433    /// where the write really did miss. That the mailbox kept a copy says
4434    /// nothing about the carrier.
4435    pub fn send_report_is_carrier_failure(reason: Option<&str>) -> bool {
4436        !matches!(
4437            reason.map(classify_transport_send_error),
4438            Some(SEND_FAIL_REASON_RELAY_PUSHED | SEND_FAIL_REASON_RELAY_PUSHED_STORED)
4439        )
4440    }
4441
4442    /// Handles the relay's `MessageSent { pushed: true }` answer: the recipient
4443    /// has no live socket, and the frame went out in a push notification.
4444    ///
4445    /// A plain DM is parked exactly as for a `DeliveryError`: without the park
4446    /// the missing ACK burns the retry budget to a terminal `message_failed`.
4447    /// If the push did deliver, the recipient's acknowledgement settles the
4448    /// parked entry (`settle_dm_without_pending_ack`).
4449    ///
4450    /// `stored` is the relay saying its mailbox also holds the frame
4451    /// (`MessageSent { pushed: true, stored: true }`, reported as
4452    /// [`SEND_FAIL_REASON_RELAY_PUSHED_STORED`]). It reaches
4453    /// [`Self::park_unreachable_dm`] and nothing else: the park is identical
4454    /// but for its recovery, which becomes the relay's own redelivery instead
4455    /// of a timed probe. Without it — an older relay, a mailbox turned off, a
4456    /// store that failed — the probe stands, because then a lost push really
4457    /// does mean nothing re-delivers the frame when the recipient reconnects.
4458    ///
4459    /// Two differences from the `DeliveryError` path, both because the push
4460    /// may have delivered the frame:
4461    ///
4462    /// - **Plain DMs only.** A connection request keeps its typed tracking and
4463    ///   a Welcome its lifecycle, both awaiting the acknowledgement or session
4464    ///   confirmation a delivered push produces. `is_parkable_plain_dm`
4465    ///   excludes both, and media chunks are never in `outbox`.
4466    /// - **No `MessageUndeliverable`, now or on any later probe.** The app
4467    ///   would be told a message is undeliverable when it may already have
4468    ///   arrived. The park's timed probe resends the same id, and the relay
4469    ///   answers a retry of a pushed message with `DeliveryError` rather than
4470    ///   a second notification, which the bridges can only report as
4471    ///   `recipient_unreachable`. So the entry is flagged
4472    ///   (`OutboxEntry::relay_pushed`, persisted) and
4473    ///   [`Self::handle_recipient_unreachable_for_message`] re-parks a flagged
4474    ///   entry silently. Without the flag the guarantee would last exactly
4475    ///   one probe interval.
4476    ///
4477    /// The flag also bounds the entry: it is dropped, silently and for the
4478    /// same reason, once [`offline_protocol_reliability::constants::REDELIVERY_WINDOW_MS`]
4479    /// has passed since `first_sent_at`, in process (`cleanup_outbox`) and at
4480    /// restore alike. Neither the sliding lifetime nor the restore refresh
4481    /// extends it — a re-send past the relay's push-claim TTL is a second
4482    /// visible notification, and past the recipient's dedup window it is a
4483    /// replay that fails to decrypt and is never acknowledged.
4484    fn park_relay_pushed_dm(
4485        &mut self,
4486        message_id: &str,
4487        carrier: Option<TransportType>,
4488        stored: bool,
4489    ) {
4490        let Ok(parsed_id) = MessageId::from_str(message_id) else {
4491            return;
4492        };
4493        if !self.is_parkable_plain_dm(&parsed_id) {
4494            self.note_superseded_probe_verdict(&parsed_id, carrier);
4495            return;
4496        }
4497        let Some(entry) = self.outbox.get_mut(&parsed_id) else {
4498            return;
4499        };
4500        let recipient = entry.message.recipient.as_str().to_string();
4501        let attempt_count = entry.attempt_count;
4502        let newly_pushed = !entry.relay_pushed;
4503        entry.relay_pushed = true;
4504        if newly_pushed {
4505            // Re-persisted on the flip only: the entry was written when it
4506            // was created, and the flag has to survive a restart for the
4507            // silent re-park to hold on the first probe after one.
4508            if let Some(entry) = self.outbox.get(&parsed_id) {
4509                self.persist_outbox_entry(entry);
4510            }
4511        }
4512        // The relay has said this recipient is not on it, which is the same
4513        // fact a `DeliveryError` records, and the same thing the probe backoff
4514        // keys on.
4515        if let Some(carrier) = carrier {
4516            self.reachability.record(
4517                &recipient,
4518                carrier,
4519                Claim::Unreachable,
4520                FactSource::GatewayVerdict,
4521                Instant::now(),
4522            );
4523        }
4524        self.note_confirmation_probe_unreachable(&recipient);
4525        debug!(
4526            message_id = %message_id,
4527            stored = stored,
4528            "Relay pushed a DM to an offline recipient; parking it"
4529        );
4530        self.park_unreachable_dm(&parsed_id, &recipient, attempt_count, stored);
4531    }
4532
4533    /// The park action shared by the relay-verdict path
4534    /// ([`Self::handle_recipient_unreachable_for_message`]) and the
4535    /// exhausted-probe path ([`Self::try_repark_exhausted_dm`]): drops the
4536    /// pending ACK and any scheduled retry so nothing burns budget against
4537    /// the offline peer, offers the frame to the mesh, then schedules the
4538    /// escalating reachability probe.
4539    ///
4540    /// `stored` drops that last step. The relay's mailbox is holding the
4541    /// frame and will deliver it on the recipient's next connection, so the
4542    /// park keeps everything that stops budget burning and adds no traffic of
4543    /// its own; recovery is the relay's drain, with the reachability edge
4544    /// behind it as for every parked frame. Everything the probe rationale
4545    /// below argues for still holds for an unheld frame.
4546    ///
4547    /// Two things that rationale does *not* cover, both deliberate:
4548    ///
4549    /// - **What is actually saved is the rungs, not the re-drive.** The edge
4550    ///   fires on any reconnect, not only on an eviction, and
4551    ///   [`Self::flush_outbox_for_peer_via`] selects on "in the outbox, not
4552    ///   awaiting an ACK" — so a returning peer re-drives a held frame
4553    ///   alongside the relay's own drain, exactly as it would an unheld one.
4554    ///   Dedup collapses the pair. What the flag removes is the 15s→600s
4555    ///   ladder *while the peer is away*, which is the traffic the relay asks
4556    ///   senders not to generate. It follows that past the mailbox's own
4557    ///   retention the delivery guarantee is that edge and the outbox
4558    ///   lifetime — the same trade `edge_driven_unreachable_dm` opts into for
4559    ///   every parked frame, taken unconditionally here because the mailbox is
4560    ///   already committed to the redelivery.
4561    /// - **`stored` is not persisted on the entry, unlike
4562    ///   `OutboxEntry::relay_pushed`.** Nothing needs it to be: a restart
4563    ///   re-drives the outbox, the relay answers `stored` again for a frame it
4564    ///   still holds, and the park is re-derived from that answer — whereas
4565    ///   `relay_pushed` has to survive, because it suppresses an app-facing
4566    ///   event no later answer would restate. Persisting this one would also
4567    ///   be wrong on its own terms: it records what the relay's mailbox held
4568    ///   at park time, and the mailbox can evict between then and the next
4569    ///   launch, so a restored flag would suppress the probe on the strength
4570    ///   of a copy that may be gone.
4571    ///
4572    /// The mesh offer is here rather than at either caller so both parks make
4573    /// it, and because this is where the frame is already at hand. Its rationale
4574    /// is inline below; the short version is that a relay verdict is the only
4575    /// per-peer reachability fact this device ever receives, and it says the
4576    /// recipient is somewhere the relay cannot go.
4577    ///
4578    /// The probe is carrier-agnostic — DORS picks its transport like any
4579    /// other send — because an internet-only device is the common
4580    /// configuration, not an exotic one, and parking it edge-only leaves the
4581    /// message with *no* self-recovery at all: no pending ACK to time out, no
4582    /// retry entry, nothing on a timer. Recovery then depends entirely on an
4583    /// external reachability edge (`flush_outbox_all` on reconnect/start, or
4584    /// a presence-online answer), i.e. on a bridge's presence-polling cadence
4585    /// — tens of seconds to minutes, and nothing at all for a headless
4586    /// consumer that never polls presence.
4587    ///
4588    /// Probing over the relay is self-limiting in every outcome, which is why
4589    /// no carrier guard is needed:
4590    /// - the peer is still offline and the relay's push fallback fails → a
4591    ///   fresh `DeliveryError` re-enters this park, escalating the interval
4592    ///   (15s → 600s cap) and re-emitting the non-terminal
4593    ///   [`Event::MessageUndeliverable`];
4594    /// - the relay's push fallback succeeds → the relay answers
4595    ///   `MessageSent { pushed: true }`, which parks the message again
4596    ///   ([`Self::park_relay_pushed_dm`]) and escalates the interval. That
4597    ///   happens once per push: the relay remembers which
4598    ///   `(sender, recipient, message_id)` triples it has pushed, for 7 days
4599    ///   (one day on older relays), and answers a repeat inside that window
4600    ///   with `DeliveryError`
4601    ///   (`already_pushed`) instead of a second notification. The outbox id
4602    ///   is stable across every probe, so each later rung earns that
4603    ///   `DeliveryError`, which re-parks the entry silently
4604    ///   (`OutboxEntry::relay_pushed`) rather than re-emitting
4605    ///   [`Event::MessageUndeliverable`] for a frame the push may have
4606    ///   delivered;
4607    /// - the peer is back → the probe *is* the delivery, which beats waiting
4608    ///   for any presence edge.
4609    ///
4610    /// The counter is per-*peer* while the probes are per-*message*: a burst
4611    /// of N DMs to one offline peer escalates the shared ladder once per
4612    /// park, so no individual message climbs 15s → 600s — the Nth already
4613    /// starts high on it. The compensating edge is delivery: an ACK for any
4614    /// one of the peer's DMs re-drives the rest immediately
4615    /// ([`Self::handle_ack_message`]) instead of leaving them on their
4616    /// escalated timers.
4617    ///
4618    /// Relay traffic is bounded differently in each of those branches, and
4619    /// the difference matters for capacity planning now that the whole
4620    /// internet-only fleet is on this path:
4621    /// - verdict branch: the escalation is the bound — one frame per interval
4622    ///   per parked *message*, settling at one per 600s;
4623    /// - accepted branch: the probe registers a fresh ACK, and
4624    ///   [`crate::protocol::OfflineProtocol::process_retry_queue`] seeds that
4625    ///   ACK with the retry-queue entry's carried count (`retry_count + 1`)
4626    ///   rather than letting it restart at 0. A probe whose entry is fresh (a
4627    ///   reachability edge re-drove it with a new budget) still walks the full
4628    ///   ladder — up to `max_retries` sends, 1s → 300s backoff, ~800s
4629    ///   cumulative on the defaults — but a probe whose entry already carries
4630    ///   backoff resumes near its current position and re-parks after roughly
4631    ///   one more timeout instead of replaying the 1s floor. Plan relay
4632    ///   capacity against the fresh-entry number; the carried case only sends
4633    ///   less. Either way `try_repark_exhausted_dm` re-parks it at the
4634    ///   escalated interval. The carry-forward is load-bearing: without it a
4635    ///   never-ACKing (offline) recipient's probe pins `delay_for_retry` at
4636    ///   its 1s floor and floods the relay ~once per second.
4637    ///
4638    /// The outbox lifetime bounds the entry itself — and note the probe
4639    /// refreshes `last_sent_at` on every send, so the sliding 7-day window
4640    /// stops binding and settlement moves out to the absolute cap
4641    /// ([`crate::constants::OUTBOX_ABSOLUTE_LIFETIME_FACTOR`] × the lifetime),
4642    /// or, for a relay-pushed entry, to its fixed redelivery window (see
4643    /// [`Self::park_relay_pushed_dm`]).
4644    /// Note also that a probe can be routed onto the relay when a mesh
4645    /// carrier is up — DORS has always been free to choose it — so this path
4646    /// is not new behavior for the relay, only newly reachable on
4647    /// internet-only devices.
4648    ///
4649    /// The counter increment is unconditional for the same reason: it is what
4650    /// arms [`Self::try_repark_exhausted_dm`], so a probe that exhausts its
4651    /// ACK budget re-parks instead of settling terminally.
4652    fn park_unreachable_dm(
4653        &mut self,
4654        message_id: &MessageId,
4655        recipient: &str,
4656        attempt_count: u32,
4657        stored: bool,
4658    ) {
4659        // Park: no pending ACK to time out, no backoff-scheduled resend.
4660        self.ack_manager.remove_ack(message_id);
4661        self.retry_queue.remove(&message_id.as_str());
4662
4663        let parks = {
4664            let count = self
4665                .dm_unreachable_parks
4666                .entry(recipient.to_string())
4667                .or_insert(0);
4668            *count = count.saturating_add(1);
4669            *count
4670        };
4671        // parks >= 1 here; shift clamped so 15 << 6 = 960 is the largest
4672        // pre-cap value — no overflow risk.
4673        let retry_in_secs = (WELCOME_NO_CARRIER_RETRY_SECS << (parks - 1).min(6))
4674            .min(WELCOME_UNREACHABLE_RETRY_CAP_SECS);
4675        let mut handed_to_mesh = 0usize;
4676        if let Some(entry) = self.outbox.get(message_id) {
4677            let message = entry.message.clone();
4678            // Ask the neighbors. The relay has just told us something no local
4679            // transport status can: that *this peer* is not on it. That is the
4680            // peer-specific fact `can_reach_without_carrying` cannot supply —
4681            // it answers yes for every recipient while an infrastructure
4682            // carrier is up, which is why the send-time offer never fires on an
4683            // online device and why the whole mixed-neighborhood case (an
4684            // online sender, a recipient reachable only across the mesh) used
4685            // to wait here for a peer that was never going to come online.
4686            //
4687            // Additive, exactly like the send-time offer: the outbox entry and
4688            // the probe below stand either way, since a neighbor taking a copy
4689            // is not proof of arrival. What settles it is the acknowledgement
4690            // coming back — over the mesh, for a message the relay could not
4691            // deliver, which is why parked DMs must be settleable without a
4692            // pending ACK (`settle_dm_without_pending_ack`). Without that arm this
4693            // offer would deliver messages the sender never learns about.
4694            //
4695            // Re-offering on every park is bounded by construction: the
4696            // escalation (15s → 600s) bounds how often we ask, each offer
4697            // spends the same own-send tokens as any other frame of ours, a
4698            // neighbor that already carried the id takes our re-send again
4699            // only within its per-neighbor allowance for us, and every device
4700            // further on suppresses the id for the whole `RelaySeenCache`
4701            // retention window.
4702            handed_to_mesh = self.offer_to_mesh(&message);
4703            // Default: keep the documented perpetual timed probe (15s->600s cap
4704            // on every carrier). Opt-in (`edge_driven_unreachable_dm`): after a
4705            // bounded number of probes, stop the timer and leave the message in
4706            // the outbox edge-driven — re-driven only when the peer next proves
4707            // reachable (inbound frame / presence-online -> on_neighbor_discovered
4708            // -> flush_outbox_for_peer_via). This zeroes steady-state relay
4709            // traffic to a durably-gone peer, at the cost of the timed-probe
4710            // self-recovery guarantee for a silent returning peer.
4711            // `stored`: the relay's mailbox holds this frame and re-sends it
4712            // on the recipient's next connection, so the probe has nothing
4713            // left to achieve — it would re-store the same row, once per rung,
4714            // for as long as the peer stays away. Edge-driven unconditionally
4715            // here, not behind `edge_driven_unreachable_dm`: that opt-in
4716            // trades away a delivery guarantee, while this is the one case
4717            // where something else already carries it.
4718            let edge_driven = stored
4719                || (self.config.reliability.retry.edge_driven_unreachable_dm
4720                    && parks > crate::protocol::types::DM_UNREACHABLE_PROBE_LIMIT);
4721            if !edge_driven {
4722                let _ = self.retry_queue.enqueue_with_delay(
4723                    message,
4724                    attempt_count,
4725                    (retry_in_secs * 1000) as u64,
4726                );
4727            }
4728        }
4729        debug!(
4730            message_id = %message_id,
4731            recipient = %recipient,
4732            retry_in_secs = retry_in_secs,
4733            parks = parks,
4734            handed_to_mesh,
4735            stored = stored,
4736            "Parked unreachable DM"
4737        );
4738    }
4739
4740    /// Re-parks a plain DM whose ACK retry budget just exhausted while its
4741    /// recipient still holds a live unreachable-park counter, returning
4742    /// `true` when the caller (`handle_max_retries_exceeded`) must skip
4743    /// terminal settlement.
4744    ///
4745    /// A live counter means the relay declared the peer offline and no
4746    /// reachability edge has fired since (every edge clears it), so the
4747    /// exhausted budget was burnt by reachability probes — sends that succeed
4748    /// locally without proving the peer is back — not by a peer believed
4749    /// reachable. Over a mesh carrier such a probe can never earn a relay
4750    /// verdict at all; over the relay a verdict normally re-parks the message
4751    /// (resetting its ACK budget) well before exhaustion, so reaching here on
4752    /// an internet-only device means the probes were accepted for delivery
4753    /// but never answered. Settling terminally here would
4754    /// reintroduce the ~15-minute `message_failed` the park exists to
4755    /// prevent; re-parking keeps settlement reserved for delivery or
4756    /// outbox-lifetime expiry (the escalation cap bounds probe traffic, the
4757    /// outbox lifetime + absolute cap bound the entry itself).
4758    ///
4759    /// Deliberately narrow: pending connection requests keep their typed
4760    /// terminal path, welcomes keep their own lifecycle, and media chunks
4761    /// (never in `outbox`, and never counted) keep retry exhaustion →
4762    /// transfer abort → `MediaResendRequired`.
4763    pub(super) fn try_repark_exhausted_dm(&mut self, message_id: &MessageId) -> bool {
4764        if !self.is_parkable_plain_dm(message_id) {
4765            return false;
4766        }
4767        let Some(entry) = self.outbox.get(message_id) else {
4768            return false;
4769        };
4770        let recipient = entry.message.recipient.as_str();
4771        if !self.dm_unreachable_parks.contains_key(recipient) {
4772            return false;
4773        }
4774        let recipient = recipient.to_string();
4775        let attempt_count = entry.attempt_count;
4776        debug!(
4777            message_id = %message_id,
4778            recipient = %recipient,
4779            "ACK budget exhausted by reachability probes; re-parking instead of settling"
4780        );
4781        // Not `stored`: a held DM is never probed, so it does not burn an ACK
4782        // budget and does not arrive here. Anything that does got here by
4783        // probing, and keeps the probe it was re-parked from.
4784        self.park_unreachable_dm(message_id, &recipient, attempt_count, false);
4785        true
4786    }
4787
4788    /// True when `message_id` is a plain DM eligible for the
4789    /// unreachable-park machinery: not a pending connection request (typed
4790    /// terminal path), not an MLS welcome (own lifecycle), and still present
4791    /// in the outbox. Media chunks live in `media_outbox`, so they never
4792    /// pass the outbox check.
4793    pub(super) fn is_parkable_plain_dm(&self, message_id: &MessageId) -> bool {
4794        if self
4795            .pending_connection_requests
4796            .contains_key(&message_id.as_str())
4797        {
4798            return false;
4799        }
4800        if self
4801            .find_welcome_peer_by_message_id(&message_id.as_str())
4802            .is_some()
4803        {
4804            return false;
4805        }
4806        self.outbox.contains_key(message_id)
4807    }
4808
4809    /// Cancels the in-flight probe state (pending ACK + scheduled retry) of
4810    /// every parkable plain DM addressed to `peer_id`, so a
4811    /// reachability-edge flush can re-drive them.
4812    ///
4813    /// Only acts when the peer holds a live unreachable-park counter: a
4814    /// live counter means the relay declared the peer offline and no edge
4815    /// has fired since, so a pending ACK on the peer's DMs belongs to a
4816    /// reachability probe — a send that locally succeeded but has not been
4817    /// answered — not to a delivery in good standing.
4818    /// Without this cancel, the flush edges' awaiting-ACK filters skip
4819    /// exactly the probing messages while still clearing the counter,
4820    /// stranding them on minutes-scale ACK backoff after the relay already
4821    /// reported the peer online — and, with the counter gone, exposing them
4822    /// to terminal settlement on exhaustion (`try_repark_exhausted_dm`
4823    /// finds nothing). Canceling an ACK that was genuinely about to arrive
4824    /// is safe: receivers dedupe and re-ACK duplicates.
4825    ///
4826    /// Does NOT clear the counter itself — the flush edge owns that reset,
4827    /// after this runs.
4828    pub(super) fn cancel_probe_state_for_parked_peer(&mut self, peer_id: &str) {
4829        if !self.dm_unreachable_parks.contains_key(peer_id) {
4830            return;
4831        }
4832        let probing: Vec<MessageId> = self
4833            .outbox
4834            .values()
4835            .filter(|entry| entry.message.recipient.as_str() == peer_id)
4836            .map(|entry| entry.message.id.clone())
4837            .collect();
4838        for message_id in probing {
4839            if !self.is_parkable_plain_dm(&message_id) {
4840                continue;
4841            }
4842            self.ack_manager.remove_ack(&message_id);
4843            self.retry_queue.remove(&message_id.as_str());
4844        }
4845    }
4846
4847    /// Classifies an outbound internet frame as a server-plane control op the
4848    /// platform bridge should translate to (or mirror as) a relay-native
4849    /// message instead of an opaque `SendMessage`.
4850    ///
4851    /// Returns `(op, payload)` where `payload` is the JSON after the prefix
4852    /// (empty string for payload-less ops). `None` means normal traffic —
4853    /// send verbatim.
4854    ///
4855    /// Ops and their bridge semantics:
4856    /// - `group_relay_register` / `group_relay_broadcast` — self-addressed
4857    ///   relay hints (`send_group_message` optimization); REPLACE with
4858    ///   relay-native `CreateGroup`+member deltas / `SendGroupMessage`. The
4859    ///   relay does not intercept content prefixes, so untranslated frames
4860    ///   would be echoed back uselessly.
4861    /// - `group_mls_leave` — TAP, don't replace: the per-member leave
4862    ///   notification must still be delivered verbatim, but the bridge also
4863    ///   sends one relay-native `LeaveGroup` so the relay's group registry
4864    ///   (which feeds invite links and broadcast fan-out) doesn't go stale.
4865    ///
4866    /// Connection ops (`__CONN_REQ__`/`__CONN_ACC__`/`__CONN_REJ__`/
4867    /// `__CONN_CAN__`) deliberately do NOT classify: they ship verbatim as
4868    /// opaque `SendMessage` frames so the Ed25519 control signature in the
4869    /// message metadata survives to the receiver's security gate. The former
4870    /// relay-native translation (`SendConnectionRequest` & co.) rebuilt them
4871    /// unsigned on the receiving bridge, which the gate now rejects
4872    /// unconditionally — control traffic must be signed. Verbatim
4873    /// also gains the relay's push-notification fallback for offline
4874    /// recipients, which the relay-native connection frames never had. The
4875    /// cost is that pre-SDK relay clients (which only speak the relay-native
4876    /// frames) no longer interoperate with connection ops — an accepted
4877    /// trade: the SDK's internet transport is the only supported relay
4878    /// client.
4879    ///
4880    /// Only frames *originated by this device* classify. The mesh relays
4881    /// third-party messages verbatim (`try_relay_message`). The relay-hint
4882    /// ops additionally require the self-addressed recipient (that is how
4883    /// the core marks them as hints rather than traffic), so they classify
4884    /// only when sender AND recipient are both this device.
4885    ///
4886    /// Self-origination is proven by `hop_count == 0`, not by the `sender`
4887    /// field: `sender` is an unauthenticated wire field, so a mesh peer
4888    /// could forge `sender == self` on a frame we then relay — but
4889    /// `try_relay_message` increments the hop before re-sending, while every
4890    /// locally-originated frame leaves `send_internal_message` at hop 0.
4891    /// (Defense in depth: the receive loop also drops inbound frames
4892    /// claiming our own origin before they can reach the relay path.)
4893    pub fn internet_control_op(&self, message: &Message) -> Option<(&'static str, String)> {
4894        if message.hop_count.value() != 0 {
4895            return None;
4896        }
4897        let content = message.content.as_str();
4898        if message.sender.as_str() == self.local_id {
4899            if let Some(payload) = content.strip_prefix(internal_prefixes::GROUP_MLS_LEAVE) {
4900                return Some(("group_mls_leave", payload.to_string()));
4901            }
4902        }
4903        if message.sender.as_str() == self.local_id && message.recipient.as_str() == self.local_id {
4904            if let Some(payload) = content.strip_prefix(internal_prefixes::GROUP_RELAY_REGISTER) {
4905                return Some(("group_relay_register", payload.to_string()));
4906            }
4907            if let Some(payload) = content.strip_prefix(internal_prefixes::GROUP_RELAY_BROADCAST) {
4908                return Some(("group_relay_broadcast", payload.to_string()));
4909            }
4910        }
4911        None
4912    }
4913
4914    pub(super) fn handle_outbound_media_chunk_delivered(&mut self, message_id: &MessageId) {
4915        let Some((file_id, chunk_index)) = self.outbound_media_chunks.remove(message_id) else {
4916            return;
4917        };
4918
4919        if let Some(window) = self.outbound_media_windows.get_mut(&file_id) {
4920            window.on_chunk_ack(chunk_index);
4921        }
4922
4923        let Some(transfer) = self.outbound_media_transfers.get_mut(&file_id) else {
4924            return;
4925        };
4926
4927        transfer.delivered_chunks.insert(chunk_index);
4928        transfer.last_updated_at = Instant::now();
4929
4930        let delivered_chunks = transfer.delivered_chunks.len() as u32;
4931        let total_chunks = transfer.total_chunks;
4932        let content_type = transfer.content_type;
4933        let recipient = transfer.recipient.clone();
4934        let completed = delivered_chunks == total_chunks;
4935        let carries_data = transfer.data_purpose.is_some();
4936
4937        if !carries_data {
4938            if let Ok(state) = lock_shared_state(&self.shared_state) {
4939                state.emit_event(Event::file_progress(
4940                    file_id.clone(),
4941                    delivered_chunks,
4942                    total_chunks,
4943                ));
4944                if completed {
4945                    state.emit_event(Event::media_sent(file_id.clone(), content_type, recipient));
4946                }
4947            }
4948        }
4949
4950        if completed {
4951            self.outbound_media_transfers.remove(&file_id);
4952            self.outbound_media_windows.remove(&file_id);
4953            self.remove_media_descriptor(&file_id);
4954        }
4955    }
4956
4957    pub(super) fn handle_outbound_media_chunk_failed(
4958        &mut self,
4959        message_id: &MessageId,
4960        reason: &str,
4961    ) {
4962        let Some((file_id, _chunk_index)) = self.outbound_media_chunks.remove(message_id) else {
4963            return;
4964        };
4965        self.abort_outbound_media_transfer(&file_id, reason);
4966    }
4967
4968    /// Aborts an active outbound media transfer: removes all transfer
4969    /// tracking (freeing its per-peer transfer slot) and emits
4970    /// [`Event::MediaSendFailed`] so the app learns the transfer will never
4971    /// complete. Idempotent — a second call for the same `file_id` is a no-op.
4972    ///
4973    /// A data-purposed transfer is the one exception to the event: it has no
4974    /// application-facing identity, so the report is withheld rather than
4975    /// naming a `file_id` the app never saw.
4976    pub(super) fn abort_outbound_media_transfer(&mut self, file_id: &str, reason: &str) {
4977        let Some(transfer) = self.outbound_media_transfers.remove(file_id) else {
4978            return;
4979        };
4980        self.outbound_media_chunks
4981            .retain(|_, (candidate_file_id, _)| candidate_file_id.as_str() != file_id);
4982        self.outbound_media_windows.remove(file_id);
4983        // A terminal abort is settled state — the app hears MediaSendFailed
4984        // now, so a MediaResendRequired for it after a restart would be
4985        // stale noise.
4986        self.remove_media_descriptor(file_id);
4987        warn!(
4988            file_id = %file_id,
4989            reason = %reason,
4990            "Aborting outbound media transfer"
4991        );
4992        // A data-purposed transfer has no application-facing identity to
4993        // fail, so reporting one would name a file_id the app has never
4994        // seen. The requester's own fetch expires and can be asked again,
4995        // and a snapshot is re-offered by the next anti-entropy exchange, so
4996        // nothing is stranded by staying quiet here.
4997        if transfer.data_purpose.is_some() {
4998            debug!(
4999                file_id = %file_id,
5000                reason = %reason,
5001                "A data-layer transfer was aborted; the asking side will retry"
5002            );
5003            return;
5004        }
5005        if let Ok(state) = lock_shared_state(&self.shared_state) {
5006            state.emit_event(Event::media_send_failed(
5007                file_id.to_string(),
5008                transfer.recipient,
5009                reason.to_string(),
5010            ));
5011        }
5012    }
5013
5014    pub(super) fn cleanup_stale_media_state(&mut self, max_age: StdDuration) {
5015        let now = Instant::now();
5016
5017        self.pending_media_metadata
5018            .retain(|_, metadata| now.duration_since(metadata.last_updated_at) <= max_age);
5019
5020        let stale_outbound_file_ids: HashSet<String> = self
5021            .outbound_media_transfers
5022            .iter()
5023            .filter_map(|(file_id, transfer)| {
5024                if now.duration_since(transfer.last_updated_at) > max_age {
5025                    return Some(file_id.clone());
5026                }
5027                None
5028            })
5029            .collect();
5030
5031        if stale_outbound_file_ids.is_empty() {
5032            return;
5033        }
5034
5035        self.outbound_media_transfers
5036            .retain(|file_id, _| !stale_outbound_file_ids.contains(file_id));
5037        self.outbound_media_chunks
5038            .retain(|_, (file_id, _)| !stale_outbound_file_ids.contains(file_id));
5039        self.outbound_media_windows
5040            .retain(|file_id, _| !stale_outbound_file_ids.contains(file_id));
5041        for file_id in &stale_outbound_file_ids {
5042            self.remove_media_descriptor(file_id);
5043        }
5044    }
5045
5046    // ========================================================================
5047    // ACK
5048    // ========================================================================
5049
5050    pub(super) fn send_delivery_ack(
5051        &mut self,
5052        message: &Message,
5053        inbound_transport: TransportType,
5054    ) -> Result<()> {
5055        self.send_ack_with_status(message, inbound_transport, None)
5056    }
5057
5058    /// Answers a frame that can never decrypt: a delivery ACK in shape (so a
5059    /// relay holding the frame drops it) carrying
5060    /// `ack_status = "undecryptable"`, which the sender settles without
5061    /// `message_delivered` (see [`Self::handle_undecryptable_ack`]). Only for
5062    /// a frame that asked for an answer; the plain ACK's rules otherwise apply.
5063    ///
5064    /// The one gate for every path that gives up on a frame (receive loop,
5065    /// queue drain, queue eviction): only a sender that advertised
5066    /// [`ACK_UNDECRYPTABLE_V1`](offline_protocol_sealed::ACK_UNDECRYPTABLE_V1)
5067    /// is answered. An older SDK reads any ACK as a delivery, so it gets what
5068    /// it always got (nothing) and keeps its retries.
5069    ///
5070    /// And only a frame that arrived over the internet relay. Every frame this
5071    /// answers failed before anything authenticated it: garbage ciphertext in
5072    /// a derivable session slot, or a parked frame whose sender is only a
5073    /// claim. Answering on a mesh or peer-stream link would tell an injector
5074    /// in radio range that the target is live and processing, which the
5075    /// threat model's acknowledgement rule withholds. The relay already knows
5076    /// the device is connected, and its mailbox is the one thing that
5077    /// redelivers a dead frame forever, so it is the one carrier answered.
5078    pub(super) fn send_undecryptable_ack(
5079        &mut self,
5080        message: &Message,
5081        inbound_transport: TransportType,
5082    ) {
5083        if inbound_transport != TransportType::Internet
5084            || !message.requires_ack
5085            || !self
5086                .peer_undecryptable_ack
5087                .contains(message.sender.as_str())
5088        {
5089            return;
5090        }
5091        let ack = match self.build_ack(message, inbound_transport, Some(ACK_STATUS_UNDECRYPTABLE)) {
5092            Ok(ack) => ack,
5093            Err(err) => {
5094                error!(
5095                    message_id = %message.id,
5096                    error = %err,
5097                    "Failed to build undecryptable ACK"
5098                );
5099                return;
5100            }
5101        };
5102        // Pinned to the relay, never the ACK ladder: `route_ack` falls back to
5103        // the mesh and to whatever DORS picks when the relay is down, and a
5104        // held ACK re-runs that same ladder later. Either would put this
5105        // answer on a radio link, where R25 says it never goes. Dropped
5106        // instead: the relay mailbox redelivers the frame on the next
5107        // connect, and the live path answers it then.
5108        let sent = self
5109            .transport_manager
5110            .can_address_via(TransportType::Internet, message.sender.as_str())
5111            && self
5112                .transport_manager
5113                .send_via_transport(&ack, TransportType::Internet)
5114                .is_ok();
5115        if !sent {
5116            debug!(
5117                message_id = %message.id,
5118                "Relay unavailable; undecryptable ACK dropped until the frame is redelivered"
5119            );
5120        }
5121    }
5122
5123    fn send_ack_with_status(
5124        &mut self,
5125        message: &Message,
5126        inbound_transport: TransportType,
5127        status: Option<&str>,
5128    ) -> Result<()> {
5129        let ack = self.build_ack(message, inbound_transport, status)?;
5130        self.route_ack(&ack, inbound_transport)
5131    }
5132
5133    /// Builds the acknowledgement for `message`, optionally carrying
5134    /// [`ACK_STATUS_KEY`]. Routing is the caller's.
5135    fn build_ack(
5136        &self,
5137        message: &Message,
5138        inbound_transport: TransportType,
5139        status: Option<&str>,
5140    ) -> Result<Message> {
5141        let sender = UserId::new(&self.local_id)?;
5142        let recipient = message.sender.clone();
5143        let app_id = AppId::new(&self.config.app_id)?;
5144        let ttl = TTL::new(self.config.initial_ttl).unwrap_or_else(|_| TTL::default());
5145
5146        let mut builder = Message::builder(sender, recipient, app_id)
5147            .content(String::new())
5148            .priority(MessagePriority::Low)
5149            .ttl(ttl)
5150            .requires_ack(false)
5151            .metadata(ACK_FOR_KEY, message.id.as_str())
5152            .metadata(ACK_HOP_COUNT_KEY, message.hop_count.value().to_string())
5153            .metadata(ACK_TRANSPORT_KEY, Self::transport_label(inbound_transport));
5154        if let Some(status) = status {
5155            builder = builder.metadata(ACK_STATUS_KEY, status);
5156        }
5157
5158        Ok(builder.build())
5159    }
5160
5161    /// Gets an acknowledgement back to whoever sent us the message it answers.
5162    ///
5163    /// The single route-selection point for every delivery ACK, direct or
5164    /// group. Keeping one ladder is deliberate: it has four callers across two
5165    /// builders, and the failure this ordering exists to prevent is silent —
5166    /// a copy of the ladder that drifted would look fine and lose answers.
5167    ///
5168    /// Three routes, in order:
5169    ///
5170    /// 1. **The link it arrived on**, when that carrier can actually address
5171    ///    the sender. Preferred because it is known to work for this peer.
5172    /// 2. **The mesh**, when the sender is not a direct mesh neighbor and
5173    ///    either nothing we hold reaches them directly *or* the message itself
5174    ///    arrived over a mesh carrier: the answer travels back the way the
5175    ///    message came, carried by the devices in between. Without this,
5176    ///    multi-hop delivery looks like failure to a sender who retransmits a
5177    ///    message we already have and read.
5178    /// 3. **DORS**, which picks among whatever else is available.
5179    ///
5180    /// The arrival-over-mesh half of step 2 is what makes this work in a mixed
5181    /// neighborhood. A frame that reached us across the mesh came from someone
5182    /// the mesh can reach and — having failed step 1 — someone we cannot hand it
5183    /// straight back to, which says nothing about whether *our* internet is up.
5184    /// Deciding from local carrier status alone, an online recipient answers a
5185    /// mesh-delivered message over the relay, where an offline sender cannot see
5186    /// it: delivered and read, reported failed. In that case the answer goes to
5187    /// the mesh **and** on to step 3, since an online sender is equally
5188    /// plausible and a duplicate acknowledgement costs one frame the sender's
5189    /// ack handling already absorbs. When nothing reaches the sender directly
5190    /// the mesh remains the whole answer and step 3 is skipped, as before.
5191    ///
5192    /// Step 1 is *gated on addressability* as well as on the send result. A
5193    /// transport returning `Ok` is not evidence the frame can arrive: a mesh
5194    /// carrier that enqueued for any recipient (Wi-Fi Direct did, before it
5195    /// refused a recipient no stream had proved) would, on the last hop of a
5196    /// forwarded frame, report the answer as sent toward a sender it holds no
5197    /// link to, and step 2 would never run. The sender's retransmissions would
5198    /// take the same path every time, ending in a failure report for a
5199    /// delivered message.
5200    ///
5201    /// An acknowledgement no route takes is held, not dropped
5202    /// ([`Self::hold_unrouted_ack`]): it was owed the moment the message was
5203    /// accepted, and the common miss is a carrier that is merely not up yet —
5204    /// a push-injected frame acknowledged before the relay socket has
5205    /// authenticated. Held is reported as `Ok`, like any carrier queue.
5206    fn route_ack(&mut self, ack_message: &Message, inbound_transport: TransportType) -> Result<()> {
5207        if self.try_route_ack(ack_message, inbound_transport) {
5208            return Ok(());
5209        }
5210        debug!(
5211            ack_to = %ack_message.recipient,
5212            "No route back for a delivery acknowledgement; holding it for the next carrier edge"
5213        );
5214        self.hold_unrouted_ack(ack_message.clone(), inbound_transport, Instant::now());
5215        Ok(())
5216    }
5217
5218    /// Holds an acknowledgement until [`Self::flush_unrouted_acks`]. Bounded
5219    /// both ways: [`MAX_UNROUTED_ACKS`] (oldest dropped first) and
5220    /// [`UNROUTED_ACK_TTL`]. Nothing past either is lost for good — a sender
5221    /// that never hears back re-sends, and the duplicate is re-ACKed.
5222    fn hold_unrouted_ack(&mut self, ack: Message, inbound: TransportType, held_at: Instant) {
5223        // One ACK answers every copy of a message, and a peer replaying it
5224        // while no carrier is up would otherwise spend a slot per copy.
5225        let acked = ack.metadata.get(ACK_FOR_KEY).cloned();
5226        self.unrouted_acks.retain(|(held, _, _)| {
5227            held.recipient != ack.recipient || held.metadata.get(ACK_FOR_KEY).cloned() != acked
5228        });
5229        if self.unrouted_acks.len() >= MAX_UNROUTED_ACKS {
5230            self.unrouted_acks.pop_front();
5231            warn!("Unrouted acknowledgement queue full; dropped the oldest");
5232        }
5233        self.unrouted_acks.push_back((ack, inbound, held_at));
5234    }
5235
5236    /// Re-routes every held acknowledgement through the same ladder. Run on
5237    /// the carrier-level edge (`flush_outbox_all`: start, and every carrier's
5238    /// false→true), ahead of the outbox, because an ACK is what lets the
5239    /// peer's outbox stop re-sending to us. Still unroutable stays held, on
5240    /// its original clock.
5241    ///
5242    /// A peer blocked since its ACK was held gets nothing: the receive path
5243    /// never answers a blocked sender, so its presence is not leaked, and a
5244    /// held ACK must not reverse that decision late.
5245    pub(super) fn flush_unrouted_acks(&mut self) {
5246        for (ack, inbound, held_at) in std::mem::take(&mut self.unrouted_acks) {
5247            if held_at.elapsed() > UNROUTED_ACK_TTL
5248                || self.is_user_blocked(ack.recipient.as_str())
5249                || self.try_route_ack(&ack, inbound)
5250            {
5251                continue;
5252            }
5253            self.hold_unrouted_ack(ack, inbound, held_at);
5254        }
5255    }
5256
5257    /// The ladder itself; `true` when some route took the acknowledgement.
5258    fn try_route_ack(&mut self, ack_message: &Message, inbound_transport: TransportType) -> bool {
5259        let ack_to = ack_message.recipient.as_str().to_string();
5260
5261        if self
5262            .transport_manager
5263            .can_address_via(inbound_transport, &ack_to)
5264            && self
5265                .transport_manager
5266                .send_via_transport(ack_message, inbound_transport)
5267                .is_ok()
5268        {
5269            return true;
5270        }
5271
5272        debug!(
5273            ack_to = %ack_to,
5274            inbound_transport = ?inbound_transport,
5275            "Inbound transport cannot answer this sender; looking for another route"
5276        );
5277
5278        // No route at all without help: the mesh is the entire answer.
5279        let no_direct_route = !self.can_reach_recipient(&ack_to);
5280        // Reached us through other devices, and we cannot hand it straight
5281        // back: the way it came is a route we know exists, whatever our own
5282        // carriers claim.
5283        let carried_to_us = TransportManager::is_mesh_transport(inbound_transport)
5284            && !self.transport_manager.mesh_can_address(&ack_to);
5285
5286        let mut handed_to_mesh = 0usize;
5287        if no_direct_route || carried_to_us {
5288            handed_to_mesh = self.offer_to_mesh(ack_message);
5289            if handed_to_mesh > 0 {
5290                debug!(
5291                    ack_to = %ack_to,
5292                    handed_to_mesh,
5293                    "Handed the acknowledgement to the mesh"
5294                );
5295                if no_direct_route {
5296                    return true;
5297                }
5298            }
5299        }
5300
5301        // Neighbors took it, so the answer is on its way even though nothing
5302        // else could carry it.
5303        self.transport_manager.send(ack_message).is_ok() || handed_to_mesh > 0
5304    }
5305
5306    /// Builds and sends a delivery ACK from the group-message drain, which
5307    /// holds only the acked id and the original wire sender (not the full
5308    /// inbound [`Message`]). Mirrors [`Self::send_delivery_ack`]; the hop count
5309    /// is unknown at drain time and reported as 0 (sender-side metrics only).
5310    pub(crate) fn send_group_delivery_ack(
5311        &mut self,
5312        ack_to: &str,
5313        acked_message_id: &str,
5314        inbound_transport: TransportType,
5315    ) -> Result<()> {
5316        let sender = UserId::new(&self.local_id)?;
5317        let recipient = UserId::new(ack_to)?;
5318        let app_id = AppId::new(&self.config.app_id)?;
5319        let ttl = TTL::new(self.config.initial_ttl).unwrap_or_else(|_| TTL::default());
5320
5321        let ack_message = Message::builder(sender, recipient, app_id)
5322            .content(String::new())
5323            .priority(MessagePriority::Low)
5324            .ttl(ttl)
5325            .requires_ack(false)
5326            .metadata(ACK_FOR_KEY, acked_message_id)
5327            .metadata(ACK_HOP_COUNT_KEY, "0")
5328            .metadata(ACK_TRANSPORT_KEY, Self::transport_label(inbound_transport))
5329            .build();
5330
5331        // Same ladder as a direct-message ACK, mesh fallback included: a group
5332        // frame reaches its members over the same hops a DM does.
5333        self.route_ack(&ack_message, inbound_transport)
5334    }
5335
5336    pub(super) fn handle_ack_message(&mut self, message: &Message) {
5337        if let Some(ack_for) = message.metadata.get(ACK_FOR_KEY) {
5338            if let Ok(message_id) = MessageId::from_str(ack_for) {
5339                // Whoever answers has to be whoever we sent it to. An
5340                // acknowledgement names a message id and nothing else ties it
5341                // to a delivery, so without this any third party that merely
5342                // saw the frame — a mesh carrier that took a copy, the relay —
5343                // settles it: the outbox entry is dropped, the retries stop and
5344                // the app is told `message_delivered` for a message that
5345                // arrived nowhere. Refusing delivery is something those parties
5346                // could always do; a false confirmation is the strictly worse
5347                // one, because it is the version the sender never finds out
5348                // about.
5349                //
5350                // Checked before anything is removed. The settle work below
5351                // starts by taking the pending ACK, and a frame rejected after
5352                // that point would have already cost the message its retry
5353                // record — turning a refused acknowledgement into the silent
5354                // loss this exists to prevent.
5355                //
5356                // Safe on this path — which every delivery passes through —
5357                // because delivery already implies the equality: a device
5358                // processes a frame only when its own `local_id` equals the
5359                // frame's recipient (`receive_message` forwards anything else),
5360                // and the acknowledgement it builds carries that same
5361                // `local_id` as its sender. A genuine confirmation therefore
5362                // matches whatever identifier namespace the app addresses peers
5363                // in — the profile before MLS initialization, the `off1…`
5364                // address after it.
5365                //
5366                // It is **not** authentication and must not be read as one: an
5367                // acknowledgement carries no signature (empty content is not a
5368                // security-gated prefix, and the receive loop handles it before
5369                // the gate would run anyway), so a forger who saw the frame saw
5370                // its recipient too and can simply write that name. What this
5371                // removes is the unattributed forgery, and the divergence
5372                // between the two settle paths below. Binding an
5373                // acknowledgement to its sender for real needs signed acks.
5374                if !self.ack_is_from_the_message_recipient(&message_id, message) {
5375                    debug!(
5376                        message_id = %message_id,
5377                        claimed_sender = %message.sender,
5378                        "Ignoring an acknowledgement from someone this message was not sent to"
5379                    );
5380                    return;
5381                }
5382
5383                // Answered, not delivered: never `message_delivered`.
5384                if message.metadata.get(ACK_STATUS_KEY).map(String::as_str)
5385                    == Some(ACK_STATUS_UNDECRYPTABLE)
5386                {
5387                    self.handle_undecryptable_ack(&message_id);
5388                    return;
5389                }
5390
5391                // A delivery ack settles an outbound connection request:
5392                // the recipient provably received it, so a later stale
5393                // recipient_unreachable signal must not fire a false
5394                // ConnectionRequestUndeliverable. Outside the ack_manager
5395                // branch — a duplicate ack (second transport) or one
5396                // arriving after retry exhaustion is still proof.
5397                self.pending_connection_requests
5398                    .remove(&message_id.as_str());
5399                if let Some(pending) = self.ack_manager.remove_ack(&message_id) {
5400                    let latency = Utc::now()
5401                        .signed_duration_since(pending.sent_at)
5402                        .num_milliseconds()
5403                        .max(0) as u64;
5404
5405                    let hop_count = message
5406                        .metadata
5407                        .get(ACK_HOP_COUNT_KEY)
5408                        .and_then(|v| v.parse::<u8>().ok())
5409                        .unwrap_or(0);
5410
5411                    let transport = message
5412                        .metadata
5413                        .get(ACK_TRANSPORT_KEY)
5414                        .map(|label| Self::transport_from_label(label))
5415                        .unwrap_or(TransportType::BLE);
5416
5417                    // Emit event if we can lock the state, but don't fail if we can't
5418                    if let Ok(state) = lock_shared_state(&self.shared_state) {
5419                        state.emit_event(Event::message_delivered(
5420                            message_id.clone(),
5421                            latency,
5422                            hop_count,
5423                            transport,
5424                        ));
5425                        drop(state);
5426                    } else {
5427                        error!(
5428                            "Failed to lock shared state for ACK event, skipping event emission"
5429                        );
5430                    }
5431
5432                    self.transport_manager.reset_retry_count(transport);
5433                    self.transport_manager.record_delivery_success(
5434                        transport,
5435                        latency.min(u32::MAX as u64) as u32,
5436                        hop_count,
5437                    );
5438                    self.handle_outbound_media_chunk_delivered(&message_id);
5439                    self.retry_queue.remove(&message_id.as_str());
5440                    if let Some(entry) = self.remove_outbox_entry(&message_id) {
5441                        // Delivery proves reachability, so the escalating
5442                        // unreachable-probe interval starts over. Clearing the
5443                        // counter is not enough on its own: it is per-peer
5444                        // while the probes are per-message, so a burst of DMs
5445                        // to an offline peer escalates the shared ladder once
5446                        // per park and leaves the later ones scheduled minutes
5447                        // out (at the 600s cap after seven). Delivery of any
5448                        // one of them proves the rest can go now, and on a
5449                        // consumer that never polls presence this ACK is the
5450                        // only edge that will say so. Flushing owns the
5451                        // counter reset — including restoring it when every
5452                        // re-drive fails — so this must not pre-clear it.
5453                        //
5454                        // The override is our own record of the transport the
5455                        // delivered send went out on (`last_transport`), which
5456                        // this ACK just proved reaches the peer — genuine
5457                        // per-transport reachability, pinning the re-drive the
5458                        // way presence-online pins Internet. Deliberately NOT
5459                        // the ACK's `transport` above: ACK_TRANSPORT_KEY is
5460                        // peer-supplied, and `from_label` maps any absent,
5461                        // stale, or hostile label to BLE — pinning a burst of
5462                        // sibling sends to a carrier chosen by the peer.
5463                        // Filtered on local availability (the transport may
5464                        // have dropped since the send); `None` falls back to
5465                        // DORS, so a missing carrier costs routing freedom,
5466                        // never the re-drive itself.
5467                        let recipient = entry.message.recipient.as_str().to_string();
5468                        if self.dm_unreachable_parks.contains_key(&recipient) {
5469                            let redrive_via = entry.last_transport.filter(|t| {
5470                                self.transport_manager
5471                                    .get_available_transports()
5472                                    .contains_key(t)
5473                            });
5474                            self.flush_outbox_for_peer_via(&recipient, redrive_via);
5475                        }
5476                    }
5477                } else {
5478                    // No pending ACK. Ordinarily that means a duplicate answer
5479                    // or one arriving after the message already settled, but a
5480                    // parked DM has none either, nor does one the mesh carried
5481                    // after no carrier took it directly, and their delivery is
5482                    // the one this device has no other way to learn about.
5483                    self.settle_dm_without_pending_ack(&message_id, message);
5484                }
5485            }
5486        }
5487    }
5488
5489    /// Settles an `undecryptable` answer: the recipient cannot decrypt the
5490    /// copy it holds. An encrypted DM is resent once, same id: re-sealed under
5491    /// the current session when its plaintext is still in memory (the flush
5492    /// path re-seals), verbatim when it is not (an entry restored after a
5493    /// restart). Verbatim is not futile: a frame the recipient's
5494    /// pending-decryption queue gave up on (TTL or overflow) was never
5495    /// attempted, so the same ciphertext decrypts once the session is up.
5496    /// Failing it here instead would turn a recoverable drop into a terminal
5497    /// one, and since a parked frame's sender is unauthenticated, anyone
5498    /// able to fill the queue could trigger that. A second answer for an id
5499    /// already resent leaves the entry to its retry ladder, which re-seals on
5500    /// every attempt it can and ends in `message_failed` at max retries: that
5501    /// bounds two devices trading answers and resends at network speed, and
5502    /// keeps a split-brain pair's message deliverable once the session is
5503    /// rebuilt. Anything else (no entry left, plaintext, media) is ignored,
5504    /// which is exactly what the frame's failure used to cost it.
5505    fn handle_undecryptable_ack(&mut self, message_id: &MessageId) {
5506        let Some(entry) = self.outbox.get(message_id) else {
5507            return;
5508        };
5509        if !entry
5510            .message
5511            .content
5512            .starts_with(internal_prefixes::ENCRYPTED)
5513        {
5514            return;
5515        }
5516        if !self.undecryptable_resent.insert(message_id.clone()) {
5517            return;
5518        }
5519        let (message, attempt_count) = (entry.message.clone(), entry.attempt_count);
5520        debug!(
5521            message_id = %message_id,
5522            "Recipient cannot decrypt this copy; resending once"
5523        );
5524        self.retry_queue.remove(&message_id.as_str());
5525        self.ack_manager.remove_ack(message_id);
5526        self.try_flush_send(message, attempt_count);
5527    }
5528
5529    /// Whether `ack` comes from the peer the message it answers was addressed
5530    /// to — the one thing about an acknowledgement that is not simply asserted
5531    /// by whoever sent it. See [`Self::handle_ack_message`] for what that does
5532    /// and does not buy.
5533    ///
5534    /// Both outboxes are consulted because both hold ack-bearing sends: plain
5535    /// DMs and control frames in `outbox`, media chunks in `media_outbox`.
5536    ///
5537    /// Answers `true` when neither holds the id, which is the ordinary shape of
5538    /// a duplicate answer arriving after the message already settled — there is
5539    /// no record left to judge against, and inventing a verdict would start
5540    /// dropping the answers this device has always accepted. The fail-open is
5541    /// not reachable by an attacker: it needs our own outbox entry to be gone,
5542    /// which nothing a peer sends can arrange.
5543    fn ack_is_from_the_message_recipient(&self, message_id: &MessageId, ack: &Message) -> bool {
5544        let Some(entry) = self
5545            .outbox
5546            .get(message_id)
5547            .or_else(|| self.media_outbox.get(message_id))
5548        else {
5549            return true;
5550        };
5551        ack.sender.as_str() == entry.message.recipient.as_str()
5552    }
5553
5554    /// Settles a DM whose acknowledgement arrived with no pending ACK to match
5555    /// it: the delivery the sender can otherwise never learn about.
5556    ///
5557    /// Invariant: a plain DM still in the outbox is settled by its recipient's
5558    /// acknowledgement, whichever copy of it arrived. A pending ACK exists only
5559    /// for a send some carrier accepted, and two paths hand a DM to the mesh
5560    /// without one: a park, which removes it so the retry budget stops burning
5561    /// against a peer the relay says is offline, and a direct send that no
5562    /// carrier took, which never registered one. On both, a neighbour can
5563    /// carry the frame to the recipient and the answer lands here. Before this
5564    /// arm took the second path, a message to a peer two hops away was
5565    /// delivered and read, and its sender kept it in the outbox, retrying,
5566    /// until the outbox lifetime failed it seven days later.
5567    ///
5568    /// The offer and this arm are one change; neither is correct alone.
5569    ///
5570    /// Gated twice, because an acknowledgement is unauthenticated (it carries
5571    /// no internal prefix, so it never reaches the control gate, and it is
5572    /// handled before the security gate runs on the receive path):
5573    ///
5574    /// 1. the id is a plain DM ([`Self::is_parkable_plain_dm`]): welcomes keep
5575    ///    their own lifecycle, and media chunks keep their pending ACK and
5576    ///    settle on the branch above. This gate does **not** exclude connection
5577    ///    requests: the caller retires their typed tracking a few lines earlier
5578    ///    and unconditionally, so by the time it runs the map no longer holds
5579    ///    the id. A request carried to its recipient and answered was
5580    ///    delivered, and settling it is the same proof the pending-ACK branch
5581    ///    acts on;
5582    /// 2. it is still in the outbox, so a late or duplicate answer to a
5583    ///    message that already settled finds nothing to act on.
5584    ///
5585    /// Attribution, that the answer comes from the recipient rather than from
5586    /// any party that handled the frame, is applied by
5587    /// [`Self::handle_ack_message`] to every acknowledgement it accepts, so
5588    /// both settle paths judge by one rule against one record.
5589    ///
5590    /// What remains is a forgery by someone who saw the frame: a carrier or the
5591    /// relay, parties who can already deny delivery outright and who can forge
5592    /// an acknowledgement for an in-flight message on the branch above. An
5593    /// earlier third gate, a live park counter, narrowed that to parties who
5594    /// also knew the relay refused the frame; it is gone because it is what
5595    /// dropped every answer to a message the mesh carried without a park.
5596    ///
5597    /// Reports latency end to end (from the first send) rather than per attempt:
5598    /// there is no pending record to measure the last attempt against, and for a
5599    /// message that waited the honest number is how long delivery actually
5600    /// took. For the same reason no transport metric is recorded: the carrier
5601    /// label is peer-supplied and there is no attempt to attribute it to.
5602    fn settle_dm_without_pending_ack(&mut self, message_id: &MessageId, ack: &Message) {
5603        if !self.is_parkable_plain_dm(message_id) {
5604            return;
5605        }
5606        let Some(entry) = self.outbox.get(message_id) else {
5607            return;
5608        };
5609        let recipient = entry.message.recipient.as_str().to_string();
5610
5611        let latency = Utc::now()
5612            .signed_duration_since(entry.first_sent_at)
5613            .num_milliseconds()
5614            .max(0) as u64;
5615        let hop_count = ack
5616            .metadata
5617            .get(ACK_HOP_COUNT_KEY)
5618            .and_then(|v| v.parse::<u8>().ok())
5619            .unwrap_or(0);
5620        let transport = ack
5621            .metadata
5622            .get(ACK_TRANSPORT_KEY)
5623            .map(|label| Self::transport_from_label(label))
5624            .unwrap_or(TransportType::BLE);
5625
5626        debug!(
5627            message_id = %message_id,
5628            recipient = %recipient,
5629            latency_ms = latency,
5630            "DM with no pending acknowledgement was delivered; settling from its acknowledgement"
5631        );
5632
5633        self.retry_queue.remove(&message_id.as_str());
5634        let redrive_via = self.remove_outbox_entry(message_id).and_then(|entry| {
5635            // Same reasoning as the pending-ACK path: our own record of where
5636            // the delivered send went, never the peer-supplied label, and only
5637            // while that carrier is still up. The park counter stays for the
5638            // flush to reset, so a re-drive that fails everywhere restores it.
5639            entry.last_transport.filter(|t| {
5640                self.transport_manager
5641                    .get_available_transports()
5642                    .contains_key(t)
5643            })
5644        });
5645        if let Ok(state) = lock_shared_state(&self.shared_state) {
5646            state.emit_event(Event::message_delivered(
5647                message_id.clone(),
5648                latency,
5649                hop_count,
5650                transport,
5651            ));
5652            drop(state);
5653        } else {
5654            error!("Failed to lock shared state for an ACK event, skipping event emission");
5655        }
5656        // The peer answered, so the rest of their waiting traffic can go now
5657        // rather than waiting out its own backoff or escalated probe timers.
5658        self.flush_outbox_for_peer_via(&recipient, redrive_via);
5659    }
5660
5661    pub(super) fn handle_missing_outbox_entry(
5662        &mut self,
5663        message_id: &MessageId,
5664        retry_count: u32,
5665    ) -> Result<()> {
5666        let state = lock_shared_state(&self.shared_state).map_err(|e| {
5667            error!(
5668                "Failed to lock shared state for missing outbox entry event: {}",
5669                e
5670            );
5671            e
5672        })?;
5673        state.emit_event(Event::message_failed(
5674            message_id.clone(),
5675            "Message missing from outbox (cannot retry)",
5676            retry_count,
5677        ));
5678        drop(state);
5679
5680        self.retire_undeliverable_message(message_id, "missing outbox entry");
5681        Ok(())
5682    }
5683
5684    // ========================================================================
5685    // EVENTS
5686    // ========================================================================
5687
5688    /// Emits a message sent event.
5689    pub(super) fn emit_message_sent_event(&self, message: &Message) -> Result<()> {
5690        let state = lock_shared_state(&self.shared_state).map_err(|e| {
5691            error!("Failed to lock shared state for message sent event: {}", e);
5692            e
5693        })?;
5694        state.emit_event(Event::message_sent(message));
5695        drop(state);
5696        Ok(())
5697    }
5698
5699    /// Emits a transport switched event if the transport changed.
5700    pub(super) fn emit_transport_switch_event(
5701        &self,
5702        previous_transport: Option<TransportType>,
5703        current_transport: Option<TransportType>,
5704    ) -> Result<()> {
5705        if current_transport != previous_transport {
5706            if let Some(new_transport) = current_transport {
5707                let state = lock_shared_state(&self.shared_state).map_err(|e| {
5708                    error!(
5709                        "Failed to lock shared state for transport switch event: {}",
5710                        e
5711                    );
5712                    e
5713                })?;
5714                state.emit_event(Event::transport_switched(
5715                    previous_transport,
5716                    new_transport,
5717                    "DORS selected better transport".to_string(),
5718                ));
5719                drop(state);
5720            }
5721        }
5722        Ok(())
5723    }
5724
5725    // ========================================================================
5726    // SOCIAL
5727    // ========================================================================
5728
5729    /// Sends a connection request to another user via any available transport.
5730    ///
5731    /// The request is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
5732    ///
5733    /// # Arguments
5734    ///
5735    /// * `recipient` - The user ID of the recipient
5736    /// * `sender_name` - Display name of the sender
5737    /// * `key_package` - Optional MLS key package for encrypted session setup
5738    /// * `initial_message` - Optional first message shown with the request
5739    ///   (surfaced verbatim in the recipient's `ConnectionRequestReceived`
5740    ///   event; apps typically seed the conversation with it on accept).
5741    ///   At most 4096 UTF-8 bytes (`MAX_INITIAL_MESSAGE_BYTES`, crate-private)
5742    ///   — longer input is rejected with `Error::InvalidArgument`.
5743    ///
5744    /// # Encryption
5745    ///
5746    /// Connection requests are internal control messages sent in plaintext,
5747    /// exempt from `require_encryption` (same as key packages and welcome
5748    /// messages) — an `initial_message` is therefore NOT end-to-end
5749    /// encrypted and should be treated like the sender display name.
5750    pub fn send_connection_request(
5751        &mut self,
5752        recipient: &str,
5753        sender_name: &str,
5754        key_package: Option<Vec<u8>>,
5755        initial_message: Option<String>,
5756    ) -> Result<MessageId> {
5757        Self::validate_outbound_recipient(recipient)?;
5758        if let Some(msg) = initial_message.as_deref() {
5759            if msg.len() > MAX_INITIAL_MESSAGE_BYTES {
5760                return Err(Error::InvalidArgument(format!(
5761                    "initial_message exceeds {} bytes (got {})",
5762                    MAX_INITIAL_MESSAGE_BYTES,
5763                    msg.len()
5764                )));
5765            }
5766        }
5767
5768        // Connection requests are internal control messages (not user content),
5769        // so they are exempt from require_encryption — same as key packages.
5770        if self.is_user_blocked(recipient) {
5771            return Err(Error::UserBlocked(recipient.to_string()));
5772        }
5773
5774        let payload = ConnectionRequestPayload {
5775            sender_name: sender_name.to_string(),
5776            timestamp_ms: Utc::now().timestamp_millis(),
5777            key_package,
5778            initial_message,
5779        };
5780
5781        let serialized =
5782            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
5783        let content = format!("{}{}", internal_prefixes::CONN_REQUEST, serialized);
5784
5785        let message_id =
5786            self.send_internal_message_alerting(recipient, content, MessagePriority::High, true)?;
5787        self.track_pending_connection_request(&message_id, recipient);
5788        info!(recipient = %recipient, "Sent connection request");
5789        Ok(message_id)
5790    }
5791
5792    /// Records an outbound connection request so a later transport-level
5793    /// "recipient offline" verdict can be surfaced as a typed event
5794    /// (see `on_transport_send_failed`). TTL-prunes and caps the map here,
5795    /// on the write path, so it needs no periodic sweep.
5796    fn track_pending_connection_request(&mut self, message_id: &MessageId, recipient: &str) {
5797        let now = std::time::Instant::now();
5798        self.pending_connection_requests
5799            .retain(|_, p| now.duration_since(p.sent_at) <= PENDING_CONNECTION_REQUEST_TTL);
5800        if self.pending_connection_requests.len() >= MAX_PENDING_CONNECTION_REQUESTS {
5801            if let Some(oldest) = self
5802                .pending_connection_requests
5803                .iter()
5804                .min_by_key(|(_, p)| p.sent_at)
5805                .map(|(id, _)| id.clone())
5806            {
5807                self.pending_connection_requests.remove(&oldest);
5808            }
5809        }
5810        self.pending_connection_requests.insert(
5811            message_id.as_str(),
5812            PendingConnectionRequest {
5813                recipient: recipient.to_string(),
5814                sent_at: now,
5815            },
5816        );
5817    }
5818
5819    /// Accepts a connection request from another user via any available transport.
5820    ///
5821    /// The response is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
5822    ///
5823    /// # Arguments
5824    ///
5825    /// * `recipient` - The user ID of the original requester
5826    /// * `accepter_name` - Display name of the accepting party
5827    /// * `key_package` - Optional MLS key package for encrypted session setup
5828    ///
5829    /// # Encryption
5830    ///
5831    /// Connection accepts are internal control messages sent in plaintext,
5832    /// exempt from `require_encryption` (same as key packages and welcome messages).
5833    pub fn accept_connection_request(
5834        &mut self,
5835        recipient: &str,
5836        accepter_name: &str,
5837        key_package: Option<Vec<u8>>,
5838    ) -> Result<MessageId> {
5839        Self::validate_outbound_recipient(recipient)?;
5840        // Accept messages are internal control messages (not user content),
5841        // so they are exempt from require_encryption — same as key packages.
5842        if self.is_user_blocked(recipient) {
5843            return Err(Error::UserBlocked(recipient.to_string()));
5844        }
5845
5846        let payload = ConnectionAcceptedPayload {
5847            accepted_by_name: accepter_name.to_string(),
5848            timestamp_ms: Utc::now().timestamp_millis(),
5849            key_package,
5850        };
5851
5852        let serialized =
5853            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
5854        let content = format!("{}{}", internal_prefixes::CONN_ACCEPT, serialized);
5855
5856        let message_id =
5857            self.send_internal_message_alerting(recipient, content, MessagePriority::High, true)?;
5858        info!(recipient = %recipient, "Accepted connection request");
5859        Ok(message_id)
5860    }
5861
5862    /// Rejects a connection request from another user via any available transport.
5863    ///
5864    /// The response is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
5865    ///
5866    /// # Arguments
5867    ///
5868    /// * `recipient` - The user ID of the original requester
5869    ///
5870    /// # Encryption
5871    ///
5872    /// Connection rejects are internal control messages sent in plaintext,
5873    /// exempt from `require_encryption` (same as key packages and welcome messages).
5874    pub fn reject_connection_request(&mut self, recipient: &str) -> Result<MessageId> {
5875        Self::validate_outbound_recipient(recipient)?;
5876        // Reject messages are internal control messages (not user content),
5877        // so they are exempt from require_encryption — same as key packages.
5878        if self.is_user_blocked(recipient) {
5879            return Err(Error::UserBlocked(recipient.to_string()));
5880        }
5881
5882        let content = internal_prefixes::CONN_REJECT.to_string();
5883
5884        let message_id = self.send_internal_message(recipient, content, MessagePriority::High)?;
5885        info!(recipient = %recipient, "Rejected connection request");
5886        Ok(message_id)
5887    }
5888
5889    /// Cancels a previously sent connection request via any available transport.
5890    ///
5891    /// The cancellation is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
5892    ///
5893    /// # Arguments
5894    ///
5895    /// * `recipient` - The user ID of the original request recipient
5896    ///
5897    /// # Encryption
5898    ///
5899    /// Connection cancellations are internal control messages sent in plaintext,
5900    /// exempt from `require_encryption` (same as key packages and welcome messages).
5901    pub fn cancel_connection_request(&mut self, recipient: &str) -> Result<MessageId> {
5902        Self::validate_outbound_recipient(recipient)?;
5903        if self.is_user_blocked(recipient) {
5904            return Err(Error::UserBlocked(recipient.to_string()));
5905        }
5906
5907        let content = internal_prefixes::CONN_CANCEL.to_string();
5908
5909        let message_id = self.send_internal_message(recipient, content, MessagePriority::High)?;
5910        info!(recipient = %recipient, "Cancelled connection request");
5911        Ok(message_id)
5912    }
5913
5914    /// Sends a presence update to a single peer (unicast).
5915    ///
5916    /// This sends to one recipient at a time. To broadcast presence to multiple
5917    /// peers, the caller must invoke this method once per peer.
5918    ///
5919    /// # Arguments
5920    ///
5921    /// * `recipient` - The user ID of the peer to notify
5922    /// * `status` - Presence status
5923    pub fn send_presence_update(
5924        &mut self,
5925        recipient: &str,
5926        status: PresenceStatus,
5927    ) -> Result<MessageId> {
5928        Self::validate_outbound_recipient(recipient)?;
5929        if self.is_user_blocked(recipient) {
5930            return Err(Error::UserBlocked(recipient.to_string()));
5931        }
5932
5933        let payload = PresencePayload {
5934            status,
5935            timestamp_ms: Utc::now().timestamp_millis(),
5936        };
5937
5938        let serialized =
5939            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
5940        let content = format!("{}{}", internal_prefixes::PRESENCE, serialized);
5941
5942        let message_id = self.send_internal_message(recipient, content, MessagePriority::Low)?;
5943        debug!(recipient = %recipient, status = ?status, "Sent presence update");
5944        Ok(message_id)
5945    }
5946
5947    /// Sends a typing indicator to a peer.
5948    ///
5949    /// # Arguments
5950    ///
5951    /// * `recipient` - The user ID of the peer to notify
5952    /// * `conversation_id` - Opaque conversation key, carried to the peer and
5953    ///   echoed back on its event. Never parsed or routed on. Conventionally
5954    ///   the peer's address for a DM and the group id for a group; any stable
5955    ///   string works, but a display name does not (those change).
5956    /// * `is_typing` - Whether the user started or stopped typing
5957    pub fn send_typing_indicator(
5958        &mut self,
5959        recipient: &str,
5960        conversation_id: &str,
5961        is_typing: bool,
5962    ) -> Result<MessageId> {
5963        Self::validate_outbound_recipient(recipient)?;
5964        if self.is_user_blocked(recipient) {
5965            return Err(Error::UserBlocked(recipient.to_string()));
5966        }
5967        if conversation_id.is_empty() {
5968            return Err(Error::InvalidArgument(
5969                "conversation_id must not be empty".to_string(),
5970            ));
5971        }
5972
5973        let payload = TypingIndicatorPayload {
5974            conversation_id: conversation_id.to_string(),
5975            is_typing,
5976            timestamp_ms: Utc::now().timestamp_millis(),
5977        };
5978
5979        let serialized =
5980            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
5981        let content = format!("{}{}", internal_prefixes::TYPING_INDICATOR, serialized);
5982
5983        let message_id = self.send_internal_message(recipient, content, MessagePriority::Low)?;
5984        debug!(recipient = %recipient, is_typing = %is_typing, "Sent typing indicator");
5985        Ok(message_id)
5986    }
5987
5988    /// Sends a read receipt to a peer, indicating that the given messages have been read.
5989    ///
5990    /// # Arguments
5991    ///
5992    /// * `recipient` - The user ID of the peer who sent the messages
5993    /// * `message_ids` - IDs of the messages that were read (max 256)
5994    pub fn send_read_receipt(
5995        &mut self,
5996        recipient: &str,
5997        message_ids: Vec<String>,
5998    ) -> Result<MessageId> {
5999        Self::validate_outbound_recipient(recipient)?;
6000        if self.is_user_blocked(recipient) {
6001            return Err(Error::UserBlocked(recipient.to_string()));
6002        }
6003        if message_ids.is_empty() {
6004            return Err(Error::InvalidArgument(
6005                "message_ids must not be empty".to_string(),
6006            ));
6007        }
6008        if message_ids.len() > MAX_READ_RECEIPT_IDS {
6009            return Err(Error::InvalidArgument(format!(
6010                "message_ids exceeds maximum of {MAX_READ_RECEIPT_IDS}"
6011            )));
6012        }
6013
6014        let payload = ReadReceiptPayload {
6015            message_ids,
6016            timestamp_ms: Utc::now().timestamp_millis(),
6017        };
6018
6019        let serialized =
6020            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
6021        let content = format!("{}{}", internal_prefixes::READ_RECEIPT, serialized);
6022
6023        let message_id = self.send_internal_message(recipient, content, MessagePriority::Low)?;
6024        debug!(recipient = %recipient, "Sent read receipt");
6025        Ok(message_id)
6026    }
6027
6028    /// Builds the advertisement body carried by a key package, whether it is
6029    /// pushed to a peer or published for a stranger to fetch.
6030    ///
6031    /// Shared so the two paths cannot drift: a published record that advertised
6032    /// a different capability set from a pushed one would make a peer's
6033    /// behaviour depend on which way they first met us.
6034    pub(crate) fn build_key_package_payload(
6035        &self,
6036        key_pkg: &offline_protocol_mls::KeyPackageBundle,
6037        session_reset: bool,
6038    ) -> KeyPackagePayload {
6039        KeyPackagePayload {
6040            user_id: self.local_id.clone(),
6041            key_package_data: key_pkg.key_package_data.clone(),
6042            remaining_lifetime_ms: key_pkg.remaining_lifetime_ms(),
6043            timestamp_ms: Utc::now().timestamp_millis() as u64,
6044            session_reset,
6045            wire_versions: if self.config.transport.binary_wire_enabled {
6046                vec![offline_protocol_core::WIRE_VERSION_V1]
6047            } else {
6048                Vec::new()
6049            },
6050            env_versions: if self.config.encryption.compact_envelope_enabled {
6051                vec![MLS_ENVELOPE_COMPACT_V1]
6052            } else {
6053                Vec::new()
6054            },
6055            rich_versions: if self.config.encryption.rich_payload_enabled {
6056                vec![RICH_PAYLOAD_V1]
6057            } else {
6058                Vec::new()
6059            },
6060            data_versions: self.advertised_data_versions(),
6061            // Advertised unconditionally, including with freshness
6062            // enforcement switched off. The two are different questions: this
6063            // field says which signatures we can *read*, which is always both
6064            // of them, while the kill switch decides whether a readable
6065            // signature is also refused for being stale. Withholding it under
6066            // the switch would make peers sign the older payload at us, and
6067            // the peer-capability record that closes the downgrade could then
6068            // never be written.
6069            ctrl_versions: vec![offline_protocol_sealed::CTRL_SIGN_V2],
6070            // Advertised under the switch only: a peer told we route the
6071            // sealed form seals toward us, and with the switch off we still
6072            // route it (parsing is unconditional), so this gates only whether
6073            // peers are invited to.
6074            svc_versions: if self.config.encryption.encrypt_service_messages {
6075                vec![offline_protocol_sealed::SVC_SEALED_V1]
6076            } else {
6077                Vec::new()
6078            },
6079            // Present only when the Nostr transport is installed. Advertised
6080            // regardless of the sealing kill switch, which gates what *we*
6081            // publish, not what a peer may seal to us: withholding it would
6082            // downgrade the peer's traffic to the bootstrap key as a side
6083            // effect of a local setting.
6084            // Read unconditionally: an `undecryptable` ACK is settled as "not
6085            // delivered" whatever our config, so a peer may always send us one.
6086            ack_versions: vec![offline_protocol_sealed::ACK_UNDECRYPTABLE_V1],
6087            nostr_pubkey: self.transport_manager.nostr_public_key(),
6088        }
6089    }
6090
6091    /// Advertises this device's key package to `peer_id`.
6092    ///
6093    /// The package comes from the peer-keyed pool
6094    /// ([`MlsManager::take_push_key_package`]): `peer_id` gets its own init key,
6095    /// re-handed on every push until a Welcome consumes it. Before that, this
6096    /// path handed the *same* package to every peer until somebody used it,
6097    /// which both weakened forward secrecy at session establishment (one
6098    /// compromised init key opens every Welcome built against it) and made the
6099    /// second peer's Welcome permanently unprocessable.
6100    ///
6101    /// A peer no carrier is known to reach is reached through the mesh: the
6102    /// package is handed to neighbours to carry, as an ordinary message is
6103    /// (see [`Self::send_or_carry_handshake`]). Without that a pair whose only path
6104    /// runs through a third device could never start a session, because the
6105    /// package and the Welcome were the two frames that never left the
6106    /// device by any other route, and every frame after them is sealed.
6107    pub(crate) fn send_key_package_to(&mut self, peer_id: &str, session_reset: bool) -> Result<()> {
6108        let mls = self.mls_manager.as_ref().ok_or(Error::MlsNotInitialized)?;
6109
6110        let push = {
6111            let manager = mls
6112                .read()
6113                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
6114            manager.take_push_key_package(peer_id)?
6115        };
6116
6117        if push.pool_exhausted {
6118            self.warn_push_key_package_pool_exhausted(peer_id);
6119        }
6120
6121        let key_pkg = push.bundle;
6122        let payload = self.build_key_package_payload(&key_pkg, session_reset);
6123
6124        let serialized =
6125            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
6126        let content = format!("{}{}", internal_prefixes::KEY_PACKAGE, serialized);
6127
6128        let mut message =
6129            self.create_message(peer_id, content, Some(MessagePriority::Low), None)?;
6130        self.sign_control_message(&mut message)?;
6131
6132        // A package no carrier is known to reach the peer with may still
6133        // reach it through a neighbour. Handing it over counts as sending it:
6134        // the stamp below is what starts the re-push ladder, which is the only
6135        // repeat a package carried this way gets, since a peer we never hear
6136        // directly produces no discovery to push again on.
6137        match self.send_or_carry_handshake(&message) {
6138            Ok(_) => {
6139                // SECURITY (resource exhaustion): `key_package_sent_to` is keyed
6140                // by the wire-claimed peer id, so a forged-sender key-package
6141                // flood (each reply routes through here) would grow it without
6142                // bound. Reset at capacity like `plaintext_receive_warned` — the
6143                // only cost of forgetting a peer is one idempotent re-send.
6144                if !self.key_package_sent_to.contains_key(peer_id)
6145                    && self.key_package_sent_to.len() >= MAX_KEY_PACKAGE_SENT_TO
6146                {
6147                    self.key_package_sent_to.clear();
6148                }
6149                // Re-stamped, and the repeat count (the re-arm's backoff) is
6150                // kept: only a reset push, which opens a new exchange, or a
6151                // removal (session reset, neighbour lost) starts it over.
6152                let now = Instant::now();
6153                let entry = self
6154                    .key_package_sent_to
6155                    .entry(peer_id.to_string())
6156                    .or_insert((now, 0));
6157                entry.0 = now;
6158                if session_reset {
6159                    entry.1 = 0;
6160                }
6161                debug!(peer_id = %peer_id, message_id = %message.id, "Sent key package");
6162                Ok(())
6163            }
6164            Err(err) => {
6165                // Don't mark as sent and don't enqueue for retry -- if the peer is
6166                // unreachable now, on_neighbor_discovered will fire again when they
6167                // reconnect, generating a fresh exchange, and a peer only the mesh
6168                // reaches is pushed to from the reconciliation tick while a
6169                // message waits for it.
6170                debug!(peer_id = %peer_id, error = %err, "Key package send deferred");
6171                Err(err)
6172            }
6173        }
6174    }
6175
6176    // ========================================================================
6177    // TRANSPORT HELPERS
6178    // ========================================================================
6179
6180    pub(super) fn transport_from_label(label: &str) -> TransportType {
6181        TransportType::from_label(label)
6182    }
6183
6184    pub(super) fn transport_label(transport: TransportType) -> &'static str {
6185        transport.label()
6186    }
6187
6188    /// The carrier a media transfer to `recipient` is pinned to, for every
6189    /// chunk and every retry.
6190    ///
6191    /// The peer stream is eligible only while a stream has proved `recipient`
6192    /// itself. The stream carries one peer, so pinned toward anyone else it
6193    /// refuses every chunk; the refused chunks, sized and windowed for a
6194    /// stream, are then offered to the mesh and reach a Bluetooth LE
6195    /// neighbour with no window at all, and the ones past the own-send budget
6196    /// retry against the pin until the transfer fails.
6197    pub(super) fn select_media_transport(&self, recipient: &str) -> Result<TransportType> {
6198        let available = self.transport_manager.get_available_transports();
6199        let eligible = |transport: TransportType| {
6200            available.contains_key(&transport)
6201                && (transport != TransportType::WiFiDirect
6202                    || self
6203                        .transport_manager
6204                        .holds_mesh_link(TransportType::WiFiDirect, recipient))
6205        };
6206
6207        if let Some(current) = self.transport_manager.current_transport() {
6208            if eligible(current) {
6209                return Ok(current);
6210            }
6211        }
6212
6213        // The daemon carrier is left out of the fallback order: its reference
6214        // backbone is scarce-class (gateway-contract.md, rate class).
6215        for preferred in [
6216            TransportType::Internet,
6217            TransportType::WiFiDirect,
6218            TransportType::BLE,
6219        ] {
6220            if eligible(preferred) {
6221                return Ok(preferred);
6222            }
6223        }
6224
6225        Err(Error::Other(
6226            "No available transport for media transfer".to_string(),
6227        ))
6228    }
6229
6230    pub(super) fn pinned_media_transport_for_message(
6231        &self,
6232        message_id: &MessageId,
6233    ) -> Option<TransportType> {
6234        let (file_id, _) = self.outbound_media_chunks.get(message_id)?;
6235        self.outbound_media_transfers
6236            .get(file_id)
6237            .map(|transfer| transfer.pinned_transport)
6238    }
6239
6240    pub(super) fn decryption_failure_code_from_kind(
6241        kind: DecryptionFailureKind,
6242    ) -> DecryptionFailureCode {
6243        match kind {
6244            DecryptionFailureKind::NotInitialized => DecryptionFailureCode::NotInitialized,
6245            DecryptionFailureKind::InvalidCiphertext => DecryptionFailureCode::InvalidCiphertext,
6246            DecryptionFailureKind::IdentityMismatch => DecryptionFailureCode::IdentityMismatch,
6247            DecryptionFailureKind::CryptoFailure => DecryptionFailureCode::CryptoFailure,
6248            DecryptionFailureKind::SessionNotFound | DecryptionFailureKind::Unknown => {
6249                DecryptionFailureCode::Unknown
6250            }
6251        }
6252    }
6253}