retrieveMessages method

Future<List<SFMessage>> retrieveMessages({
  1. PeerId? targetPeerId,
  2. PeerId? fromServer,
  3. String? folderPath,
  4. int? fromSequence,
  5. int? maxMessages,
  6. MessagePriority? minPriority,
  7. bool throwOnFailure = false,
})

Retrieve pending messages from S&F server

targetPeerId Optional: retrieve from another peer's public mailbox (default: own mailbox) fromServer Optional: specify which server to retrieve from folderPath Which folder to retrieve from (default: 'inbox') fromSequence Cursor position (for multi-reader mailboxes) maxMessages Maximum number of messages to retrieve minPriority Minimum priority level to retrieve throwOnFailure When true, a retrieval that fails (no server, a timeout, a broken connection) throws RetrieveFailedException instead of returning an empty list, so the caller can tell "the mailbox is empty" from "the mailbox could not be read" and try again.

Implementation

Future<List<SFMessage>> retrieveMessages({
  PeerId? targetPeerId,  // NEW: retrieve from another peer's public mailbox
  PeerId? fromServer,
  String? folderPath,
  int? fromSequence,
  int? maxMessages,
  MessagePriority? minPriority,
  bool throwOnFailure = false,
}) async {
  final effectivePeerId = targetPeerId ?? host.id;
  final isOwnMailbox = effectivePeerId == host.id;

  _logger.info('📥 [SFClient] retrieveMessages called');
  _logger.info('📥 [SFClient] Target peer: ${effectivePeerId.toBase58()}');
  _logger.info('📥 [SFClient] Folder: ${folderPath ?? 'inbox'}');
  _logger.info('📥 [SFClient] Own mailbox: $isOwnMailbox');
  _logger.info('📥 [SFClient] FromSeq: $fromSequence, MaxMsgs: $maxMessages');

  // Determine which server to query
  _logger.info('📥 [SFClient] Selecting server...');
  final serverId = fromServer ??
      await _serverSelector.selectServer(config.preferredServers);

  if (serverId == null) {
    _logger.warning('❌ [SFClient] No S&F server available for retrieval');
    if (throwOnFailure) throw const RetrieveFailedException('No S&F server available');
    return [];
  }

  _logger.info('✅ [SFClient] Server selected: ${serverId.toBase58()}');

  P2PStream? stream;  // Declare outside try
  try {
    // Open stream to server using MAA (Mail Access Agent) protocol
    _logger.info('🌐 [SFClient] Opening stream to server (MAA protocol)...');
    final context = Context();
    stream = await host.newStream(
      serverId,
      [AccessHandler.protocolId],
      context,
    ).timeout(config.connectionTimeout);

    _logger.info('✅ [SFClient] Stream opened successfully');
    _logger.info('📤 [SFClient] Sending retrieve request to server...');

    // Retrieve messages via MAA (pass targetPeerId to protocol handler)
    final response = await AccessHandler.retrieveMessages(
      stream,
      effectivePeerId,  // Request messages for this peer (could be us or another peer)
      folderPath: folderPath,
      fromSequence: fromSequence,
      maxMessages: maxMessages,
      minPriority: minPriority,
    ).timeout(config.messageTimeout);

    _logger.info('✅ [SFClient] Retrieved ${response.messages.length} messages from ${folderPath ?? 'inbox'}');

    // Protect the S&F server connection so it won't be closed by cleanup
    host.connManager.protect(serverId, 'ricochet-sf-server');
    _logger.fine('Protected connection to S&F server ${serverId.toBase58().substring(0, 12)}...');

    for (int i = 0; i < response.messages.length; i++) {
      final msg = response.messages[i];
      _logger.info('✅ [SFClient] Message $i: ID=${msg.messageId.substring(0, 8)}, Folder=${msg.folderPath ?? "null"}');
    }

    // Open sealed payloads against the envelope each message arrived in,
    // so a ciphertext the server moved or relabelled is refused.
    final messages = <SFMessage>[];
    for (final message in response.messages) {
      try {
        messages.add(await openIfEncrypted(message, encryptor));
      } catch (e) {
        // A payload that does not open is returned sealed rather than
        // dropped: the caller still sees the message, and can delete it or
        // pass it on. Failing the loop instead would cost the caller every
        // other message in the batch, so one unopenable ciphertext (from a
        // sender using the wrong key, say) would stall the mailbox.
        _logger.warning(
            'message ${message.messageId} did not open, returning it sealed: $e');
        messages.add(message);
      }
    }

    // Emit to incoming messages stream
    for (final message in messages) {
      _incomingMessagesController.add(message);
    }

    return messages;

  } on TimeoutException {
    _logger.warning('Retrieval timed out');
    if (throwOnFailure) throw const RetrieveFailedException('Retrieval timed out');
    return [];
  } on RetrieveRefusedException catch (e) {
    // Not an empty mailbox: the caller is told so, rather than finding
    // nothing and concluding nothing was ever published there.
    _logger.warning('Retrieve refused: $e');
    rethrow;
  } catch (e) {
    _logger.warning('Failed to retrieve messages: $e');
    if (throwOnFailure) throw RetrieveFailedException('$e');
    return [];
  } finally {
    // Always close the stream after use
    if (stream != null && !stream.isClosed) {
      try {
        await stream.close();
      } catch (e) {
        _logger.fine('Error closing stream: $e');
      }
    }
  }
}