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