Skip to main content

offline_protocol/protocol/
send.rs

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