diff --git a/@packages/imessage/Sources/IMessageSync/Sender.swift b/@packages/imessage/Sources/IMessageSync/Sender.swift index 8da4736..4083f13 100644 --- a/@packages/imessage/Sources/IMessageSync/Sender.swift +++ b/@packages/imessage/Sources/IMessageSync/Sender.swift @@ -88,9 +88,23 @@ class SendService { .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") + // Resolve the iMessage service by iterating with a per-service `try`. + // A plain `1st service whose service type = iMessage` throws (-1728) + // when any stale service errors on `service type`, and it would also + // pick a *disabled* service. Require an enabled iMessage service; if + // none, fail with a clear reason instead of a cryptic delivery error. let script = """ tell application "Messages" - set targetService to 1st service whose service type = iMessage + set targetService to missing value + repeat with s in services + try + if (service type of s) is iMessage and (enabled of s) then + set targetService to s + exit repeat + end if + end try + end repeat + if targetService is missing value then error "no enabled iMessage service" set targetBuddy to buddy "\(sanitizedBuddy)" of targetService send "\(sanitizedBody)" to targetBuddy end tell @@ -143,6 +157,17 @@ class SendService { return (false, nil, errorMessage) } + // osascript exiting 0 only means Messages accepted the command — NOT + // that the message transmitted. Verify against chat.db: a disabled + // iMessage service still creates a message row, but with error != 0 + // (e.g. error 33). Never report success the ground truth contradicts. + Thread.sleep(forTimeInterval: 4) + if let sendError = Self.chatDBSendError(buddyId: buddyId), sendError != 0 { + osLog.error("send: chat.db reports error=\(sendError, privacy: .public) for \(logPrefix, privacy: .public) — NOT delivered") + activityLog.error("iMessage send failed to \(logPrefix)...: Messages error \(sendError)") + return (false, nil, "Messages reported error \(sendError) — not delivered (iMessage service may be disabled)") + } + dailySendCount += 1 hourlySendCount += 1 @@ -192,6 +217,27 @@ class SendService { } } + /// Most-recent outbound `message.error` for `buddyId` from chat.db, or nil + /// if no row is found. A non-zero value means Messages failed the send + /// even when `osascript` exited 0 (e.g. disabled iMessage service → 33). + nonisolated static func chatDBSendError(buddyId: String) -> Int? { + guard let dbQueue = iMessageReader.shared.getDatabaseQueue() else { return nil } + let suffix = String(buddyId.suffix(7)) + let value: Int? = (try? dbQueue.read { db -> Int? in + guard let row = try Row.fetchOne(db, sql: """ + SELECT m.error AS err + FROM message m + JOIN handle h ON h.ROWID = m.handle_id + WHERE m.is_from_me = 1 AND h.id LIKE ? + ORDER BY m.date DESC + LIMIT 1 + """, arguments: ["%\(suffix)"]) + else { return nil } + return (row["err"] as? Int64).map(Int.init) + }) ?? nil + return value + } + func getRateLimitStatus() -> (dailyRemaining: Int, hourlyRemaining: Int) { resetCountersIfNeeded() return (max(0, maxDailyMessages - dailySendCount), max(0, maxHourlyMessages - hourlySendCount))