You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Outgoing iMessages are marked as "failed to send" in the UI when one of the sender's own other Apple devices rejects the reflected copy with ec-com.apple.messageprotection-6. The message itself is successfully delivered to the chat recipient — the failure is purely in the self-reflection path that mirrors the conversation to the user's other devices. The UI surfaces this as a send failure, which it isn't.
This appears closely related to (and may explain) closed issue #205 and open issues #170 / #61.
Symptoms
User sends an iMessage from OpenBubbles on Android.
Chat recipient receives the message normally.
After SendFinished fires in the Dart layer (~1–2s after send), one or more of the user's own registered devices respond with APN command 120 / ec-com.apple.messageprotection-6 for the reflected copy.
Dart receives this as a Message::Error and marks the local row failed.
Reinstalling OpenBubbles, switching the relay Mac, and re-registering the device do not help — because the problem isn't in the sender's identity, it's in the stale-key state of other devices on the same Apple ID that Apple IDS keeps reflecting to.
Reproduction shape
Hard to reproduce on demand because it depends on having one or more devices on the user's Apple ID whose iMessage NGM session keys have drifted out of sync with what rustpush's identity cache thinks they are (e.g. retired/sold devices still listed in appleid.apple.com, devices whose keystore was wiped, devices after a major iOS update, etc.).
When it does occur, every outbound message produces a flurry of MP-6 errors from the same set of stuck device-tokens.
Evidence
Over a ~9-hour window in one user's rustpush log:
81 ec-com.apple.messageprotection-6 errors, all with command: 120.
100% of them are self-loops: the sP (sender person) field equals the tP (target person) field — always the user's own phone-number handle or their iCloud email handle.
23 distinct failing device-tokens (fU field), most failures concentrated on the top 5–6 tokens (one specific device produced 18 failures alone).
Each failure has fR: 200 — confirming Apple's IDS accepted the send and forwarded it; the per-recipient-device decryption is what failed.
Cross-referenced with the Flutter-side log, a single sent message shows:
T+0.0s Dart sending <msg-uuid>
T+1.5sSendFinished (Apple ack — delivery to intended chat recipient succeeded)
T+3.2s Self-loop MP-6 from one of the user's devices arrives → UI flips message to failed
For every other MP-* code (including -6), the error is passed through to the Dart layer verbatim with no auto-recovery. The Dart UI then renders it as a send failure.
There are also two structural issues compounding this:
MP-6 means "signature/decrypt verification failed on the recipient device", which on a self-loop is the same recoverable condition that MP-802 represents — a stale per-device session key. It should invalidate the cache for at least the failing fU and ideally trigger a refresh.
Self-loop errors (sP == tP) are cosmetic: the message was already delivered to the actual chat recipient (Apple acked it; otherwise we'd see a different error). The UI shouldn't treat a self-reflection failure as a send failure regardless of MP code.
Suggested fix
Either / both of:
(a) Broaden the auto-invalidate branch to cover all messageprotection-* codes (or at least -6):
if error_string.starts_with("ec-com.apple.messageprotection-"){letmut cache_lock = self.identity.cache.lock().await;
cache_lock.invalidate(&target,&sender);}
This is the same logic that already works for -802. It would cause the next send to re-fetch IDS keys for the affected pair, which empirically resolves the failure (Apple returns a fresh device-session list that excludes the stuck devices, or includes their rotated keys).
(b) In process_msg, detect sP == sender_handle_of_this_device (i.e. self-loop) and don't surface Message::Error to Dart for those cases. They're delivery feedback for the user's own devices, not chat-send feedback.
Approach (a) is safer and lower-impact; (b) is the more semantically correct fix.
Why current workarounds don't help
Reinstall / new relay Mac / clear app data: regenerates the sender's identity, but does nothing about the other devices Apple IDS still reflects to.
Re-register from Developer Tools: same as above. Logs show multiple successful re-registrations on the same day with no change in MP-6 rate.
Toggle Text Message Forwarding off/on (workaround for Messages fail to send, IDS key missing #205): incidentally re-registers the Mac's IDS identity, which can clear some stale session keys, but isn't a real fix and only works for the specific Mac being toggled.
Clear Identity Cache from Developer Tools: this is the manual version of the -802 auto-invalidate — it works as a one-off but the issue recurs once a device drifts again.
Environment
Platform: Android, OpenBubbles self-hosted
iMessage account: Apple ID with multiple registered devices (the more devices, the higher the chance of one drifting and triggering this)
rustpush version: current main as of mid-May 2026 (also seen with a vendored "openbubbles-app-old" copy of rustpush that appears alongside the current path in recent APKs)
Logs
Happy to share a sanitized rustpush log on request. The relevant log lines look like:
[INFO] [/.../rustpush/src/imessage/aps_client.rs:205] recieved [<own handle>] 'failed to receive our message'
with the corresponding raw APN dict containing c: 120, fRM: "ec-com.apple.messageprotection-6", fR: 200, sP == tP, and varying fU tokens corresponding to the user's own devices.
Summary
Outgoing iMessages are marked as "failed to send" in the UI when one of the sender's own other Apple devices rejects the reflected copy with
ec-com.apple.messageprotection-6. The message itself is successfully delivered to the chat recipient — the failure is purely in the self-reflection path that mirrors the conversation to the user's other devices. The UI surfaces this as a send failure, which it isn't.This appears closely related to (and may explain) closed issue #205 and open issues #170 / #61.
Symptoms
SendFinishedfires in the Dart layer (~1–2s after send), one or more of the user's own registered devices respond with APN command 120 /ec-com.apple.messageprotection-6for the reflected copy.Message::Errorand marks the local row failed.Reproduction shape
Hard to reproduce on demand because it depends on having one or more devices on the user's Apple ID whose iMessage NGM session keys have drifted out of sync with what rustpush's identity cache thinks they are (e.g. retired/sold devices still listed in
appleid.apple.com, devices whose keystore was wiped, devices after a major iOS update, etc.).When it does occur, every outbound message produces a flurry of MP-6 errors from the same set of stuck device-tokens.
Evidence
Over a ~9-hour window in one user's rustpush log:
ec-com.apple.messageprotection-6errors, all withcommand: 120.sP(sender person) field equals thetP(target person) field — always the user's own phone-number handle or their iCloud email handle.fUfield), most failures concentrated on the top 5–6 tokens (one specific device produced 18 failures alone).fR: 200— confirming Apple's IDS accepted the send and forwarded it; the per-recipient-device decryption is what failed.T+0.0sDartsending <msg-uuid>T+1.5sSendFinished(Apple ack — delivery to intended chat recipient succeeded)T+3.2sSelf-loop MP-6 from one of the user's devices arrives → UI flips message to failedRoot cause
In
rustpush/src/imessage/aps_client.rs(~line 245–263), onlyec-com.apple.messageprotection-802triggers identity-cache invalidation:For every other MP-* code (including
-6), the error is passed through to the Dart layer verbatim with no auto-recovery. The Dart UI then renders it as a send failure.There are also two structural issues compounding this:
MP-6 means "signature/decrypt verification failed on the recipient device", which on a self-loop is the same recoverable condition that MP-802 represents — a stale per-device session key. It should invalidate the cache for at least the failing
fUand ideally trigger a refresh.Self-loop errors (
sP == tP) are cosmetic: the message was already delivered to the actual chat recipient (Apple acked it; otherwise we'd see a different error). The UI shouldn't treat a self-reflection failure as a send failure regardless of MP code.Suggested fix
Either / both of:
(a) Broaden the auto-invalidate branch to cover all
messageprotection-*codes (or at least-6):This is the same logic that already works for
-802. It would cause the next send to re-fetch IDS keys for the affected pair, which empirically resolves the failure (Apple returns a fresh device-session list that excludes the stuck devices, or includes their rotated keys).(b) In
process_msg, detectsP == sender_handle_of_this_device(i.e. self-loop) and don't surfaceMessage::Errorto Dart for those cases. They're delivery feedback for the user's own devices, not chat-send feedback.Approach (a) is safer and lower-impact; (b) is the more semantically correct fix.
Why current workarounds don't help
-802auto-invalidate — it works as a one-off but the issue recurs once a device drifts again.Environment
mainas of mid-May 2026 (also seen with a vendored "openbubbles-app-old" copy of rustpush that appears alongside the current path in recent APKs)Logs
Happy to share a sanitized rustpush log on request. The relevant log lines look like:
with the corresponding raw APN dict containing
c: 120,fRM: "ec-com.apple.messageprotection-6",fR: 200,sP == tP, and varyingfUtokens corresponding to the user's own devices.Related issues
Message::Errorsurfaced as send failure