From 980506fadfa91e8d27c50ff2b059e1d59b54afbe Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:35:53 +0200 Subject: [PATCH] feat: log why the iOS notification extension falls back to the placeholder --- plugins/ios-push/NotificationService.swift | 16 +++++++++++++--- plugins/ios-push/SharedKeychain.swift | 6 +++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/ios-push/NotificationService.swift b/plugins/ios-push/NotificationService.swift index 1b93d075..beed8009 100644 --- a/plugins/ios-push/NotificationService.swift +++ b/plugins/ios-push/NotificationService.swift @@ -1,6 +1,10 @@ import Foundation +import os import UserNotifications +/// Ids and steps only, never credentials or message content. +let pushLog = Logger(subsystem: "org.bulwarkmail.mobile.NotificationService", category: "push") + /// Turns the relay's content-blind push ("New message" plus ids) into a real /// notification: sender, subject and preview, fetched over JMAP on this /// device with the account's own credentials. Nothing about the message ever @@ -50,12 +54,16 @@ final class NotificationService: UNNotificationServiceExtension { } static func enrich(_ content: UNMutableNotificationContent, push: [String: Any]?) async -> UNMutableNotificationContent { - guard let push, - let accountId = push["accountId"] as? String, !accountId.isEmpty, - let account = SharedKeychain.readJSON("bulwark_push_account__\(sanitize(accountId))"), + guard let push, let accountId = push["accountId"] as? String, !accountId.isEmpty else { + pushLog.error("push without an account id") + return content + } + pushLog.info("push for account \(accountId, privacy: .public), group \(SharedKeychain.group ?? "none", privacy: .public)") + guard let account = SharedKeychain.readJSON("bulwark_push_account__\(sanitize(accountId))"), let credentialsKey = account["credentialsKey"] as? String, let apiString = account["apiUrl"] as? String, let apiUrl = URL(string: apiString) else { + pushLog.error("no shared account entry for \(accountId, privacy: .public)") return content } let emailIds = push["emailIds"] as? [String] ?? [] @@ -69,6 +77,7 @@ final class NotificationService: UNNotificationServiceExtension { do { let authorization = try await Jmap.authorization(credentialsKey: credentialsKey) let result = try await Jmap.fetch(apiUrl: apiUrl, accountId: accountId, emailIds: emailIds, authorization: authorization) + pushLog.info("fetched \(result.emails.count, privacy: .public) of \(emailIds.count, privacy: .public) messages") if let first = result.emails.first { content.title = first.sender @@ -91,6 +100,7 @@ final class NotificationService: UNNotificationServiceExtension { } } catch { // Keep the placeholder. + pushLog.error("fetch failed: \(String(describing: error), privacy: .public)") } var userInfo = content.userInfo userInfo["body"] = body diff --git a/plugins/ios-push/SharedKeychain.swift b/plugins/ios-push/SharedKeychain.swift index 6eeaeb31..77668029 100644 --- a/plugins/ios-push/SharedKeychain.swift +++ b/plugins/ios-push/SharedKeychain.swift @@ -23,7 +23,11 @@ enum SharedKeychain { kSecReturnData as String: true, ] var item: CFTypeRef? - guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess else { return nil } + let status = SecItemCopyMatching(query as CFDictionary, &item) + guard status == errSecSuccess else { + pushLog.error("keychain read of \(key, privacy: .public) failed: \(status, privacy: .public)") + return nil + } return item as? Data }