Skip to content

Synchronize Mailbox Connection definitions across devices - #91

Merged
rajzik merged 17 commits into
mainfrom
codex/issue-52-synchronize-mailbox-connections
Jul 22, 2026
Merged

rajzik merged 17 commits into
mainfrom
codex/issue-52-synchronize-mailbox-connections

Conversation

@rajzik

@rajzik rajzik commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #52

Summary

  • synchronize encrypted, non-secret Mailbox Connection definitions and the Default Sending Connection through Product Sync
  • keep provider authorization device-local, expose authorization-required connections, and bind reauthorization to the selected provider identity
  • support device-only authorization removal and durable global removal tombstones without touching provider mail
  • fence provider access after synchronized removal and preserve the synced default without silently substituting a local sender
  • document the privacy, authorization, and deletion-convergence behavior

Validation

  • mise exec -- pnpm install --frozen-lockfile
  • mise exec -- pnpm lint
  • mise exec -- pnpm format
  • mise exec -- pnpm turbo run check-types
  • mise exec -- pnpm test
  • mise exec -- pnpm fallow
  • mise exec -- zsh scripts/check-apple-lint.zsh
  • mise exec -- xcodebuild test -quiet -project apps/unwired-mail/unwired-mail.xcodeproj -scheme unwired-mail -destination 'platform=iOS Simulator,name=iPhone 17'

Two-axis Standards and Spec review completed with no remaining findings.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features
    • Encrypted mailbox connection definitions now synchronize across trusted devices, including synchronized default-sending.
    • Account and generic mail setup now display synced connections with per-connection status and a Manage menu (set default sender, remove local authorization, remove everywhere).
  • Bug Fixes
    • Inbox, actions, and sending are gated to authorized connections; default-sending no longer substitutes silently.
    • “Remove everywhere” publishes encrypted tombstones to prevent offline resurrection; push sync stops when a connection is removed.
  • Documentation
    • Expanded local development and mail connectivity guidance, including OAuth and removal semantics.
  • Tests
    • Added/expanded multi-device sync, rollback/error paths, and encrypted-payload opacity coverage.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds encrypted cross-device synchronization for mailbox definitions and Default Sending connections while keeping credentials device-local. It introduces authorization-required states, tombstone removal fencing, provider-access checks, account UI controls, documentation, and synchronization tests.

Changes

Mailbox Connection synchronization

Layer / File(s) Summary
Sync contracts and local authorization
apps/unwired-mail/unwired-mail/Services/ProductSync/*, apps/unwired-mail/unwired-mail/Services/Mailbox/*
Defines synchronized mailbox identities, authorization states, snapshots, connection-scoped authorization, sync protocols, and encrypted cache storage.
Encrypted snapshot synchronization
apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncService.swift, apps/unwired-mail/unwired-mailTests/MailboxConnectionSyncServiceTests.swift
Loads, encrypts, caches, reconciles, conditionally updates, and tombstones mailbox connection snapshots, with cached provider-access fallback and cross-device tests.
Adapter reconciliation and provider access
apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift, apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swift, apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift
Integrates synchronized definitions with local authorization, expected-account validation, default-sender management, removal cleanup, and provider-access fencing.
Account behavior, UI, wiring, and documentation
apps/unwired-mail/unwired-mail/Features/Account/*, apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj, apps/unwired-mail/unwired-mailTests/*, README.md, docs/adr/*, .changeset/*
Updates account and generic-mail management UI, wires sources and tests into Xcode, validates synchronization and removal flows, and documents the resulting behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AccountView
  participant GmailMailboxConnectionAdapter
  participant MailboxConnectionSyncService
  participant ProductSyncPayloadTransport
  AccountView->>GmailMailboxConnectionAdapter: load or authorize mailbox connection
  GmailMailboxConnectionAdapter->>MailboxConnectionSyncService: reconcile or save definition
  MailboxConnectionSyncService->>ProductSyncPayloadTransport: read or conditionally write encrypted snapshot
  ProductSyncPayloadTransport-->>MailboxConnectionSyncService: synchronized snapshot
  MailboxConnectionSyncService-->>GmailMailboxConnectionAdapter: definitions, defaults, and removals
  GmailMailboxConnectionAdapter-->>AccountView: authorized or authorization-required connections
Loading

Possibly related PRs

  • unwired-dev/product#81: Extends the provider-neutral mailbox connection seam used by this synchronization flow.
  • unwired-dev/product#82: Provides related multi-mailbox Gmail account and default-sender behavior.
  • unwired-dev/product#86: Provides the generic-mail setup flow extended with connection-scoped authorization and definition synchronization.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: synchronizing mailbox connection definitions across devices.
Linked Issues check ✅ Passed The changes satisfy #52 by syncing definitions/default sender, keeping auth device-local, supporting both removal scopes, and adding cross-device/deletion-convergence tests.
Out of Scope Changes check ✅ Passed The PR stays focused on mailbox-connection sync, with docs, tests, and project wiring all supporting the stated behavior.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-52-synchronize-mailbox-connections

Comment @coderabbitai help to get the list of available commands.

@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@rajzik
rajzik force-pushed the codex/issue-52-synchronize-mailbox-connections branch from 1f25a35 to 6009f47 Compare July 21, 2026 14:14

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6009f4788e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift Outdated
Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
Comment thread apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift Outdated
@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift (1)

805-978: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Every provider-access path now performs a fresh remote/cache round trip before the actual Gmail call.

loadInbox, syncInbox, syncRecentInbox, searchProvider, registerOrRenewPush, perform, and send all funnel through gmailConnectionForProviderAccess → ensureConnectionIsActive, which calls definitionSyncService.loadSnapshotForProviderAccess (a Product Sync network fetch, falling back to Keychain cache) on every single invocation. For high-frequency paths like loadMessageBody/message-list refreshes, this adds a serialized network round trip before each mail action, which can noticeably increase latency and load on the Product Sync backend.

Consider a short-lived in-memory TTL cache (or event-driven invalidation on known removal) for the "is this connection still active" check, rather than re-fetching before every call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift`
around lines 805 - 978, The gmailConnectionForProviderAccess path performs a
Product Sync/cache lookup on every provider call through
ensureConnectionIsActive, adding unnecessary latency. Add a short-lived
in-memory TTL cache for successful active-connection checks, keyed by connection
ID, and reuse it in gmailConnectionForProviderAccess while preserving validation
after expiry. Invalidate the cached status when a connection is removed or
otherwise known to become inactive.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swift`:
- Around line 488-505: Update the catch block around
definitionSyncService.saveDefinition so rollback failures cannot replace the
original sync error. Make authorizationStore.save/remove best-effort, then
always rethrow the caught error while preserving the existing
restore-versus-remove behavior.

In `@apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift`:
- Around line 699-748: Update the localStatusesById construction in
loadConnections to use a duplicate-tolerant dictionary initializer instead of
Dictionary(uniqueKeysWithValues:). Ensure duplicate MailboxConnection IDs do not
trap and retain the intended status value while preserving the existing lookup
and reconciliation behavior.

In
`@apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionDefinition.swift`:
- Around line 4-10: Document stableProviderConnectionKey with a concise comment
explaining that it is a versioned derived hash of provider and
providerAccountIdentifier and is retained for external persistence or migration;
if no such contract exists, remove the property and its assignment from
MailboxConnectionDefinition.

In
`@apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncService.swift`:
- Around line 209-245: Update update() so failed CAS attempts, identified by
writtenPayload.encryptedPayload differing from encryptedPayload, wait before the
next loop iteration. Add a small jittered backoff between retries, while
avoiding a delay after the final attempt and preserving the existing
maximumWriteAttempts and concurrentModification behavior.

---

Outside diff comments:
In `@apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift`:
- Around line 805-978: The gmailConnectionForProviderAccess path performs a
Product Sync/cache lookup on every provider call through
ensureConnectionIsActive, adding unnecessary latency. Add a short-lived
in-memory TTL cache for successful active-connection checks, keyed by connection
ID, and reuse it in gmailConnectionForProviderAccess while preserving validation
after expiry. Invalidate the cached status when a connection is removed or
otherwise known to become inactive.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0e89eafe-6386-48e6-8620-9dda775a1e53

📥 Commits

Reviewing files that changed from the base of the PR and between 9f21c68 and 009ff6b.

📒 Files selected for processing (16)
  • .changeset/synchronize-mailbox-connections.md
  • README.md
  • apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailProviderCatalog.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionDefinition.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncCacheStore.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncService.swift
  • apps/unwired-mail/unwired-mailTests/GenericMailSetupServiceTests.swift
  • apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/MailboxConnectionSyncServiceTests.swift
  • docs/adr/0001-end-to-end-encrypted-product-sync.md
  • docs/adr/0010-device-local-mailbox-authorization.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 009ff6b7d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift Outdated
Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift Outdated
Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
Comment thread apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift Outdated
@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 4 file(s) based on 4 unresolved review comments.

Files modified:

  • apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionDefinition.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncService.swift

Commit: 13437bad10b4f22ea8f638f5d474884cf3414886

The changes have been pushed to the codex/issue-52-synchronize-mailbox-connections branch.

Time taken: 3m 31s

coderabbitai Bot and others added 2 commits July 21, 2026 15:02
Fixed 4 file(s) based on 4 unresolved review comments.

Co-authored-by: CodeRabbit <[email protected]>
@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift (1)

1007-1035: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Every provider-access call now performs a full remote sync fetch.

gmailConnectionForProviderAccess → ensureConnectionIsActive calls definitionSyncService.loadSnapshotForProviderAccess, which starts with a network round-trip (loadRemotePayload) plus decrypt, on every invocation. This helper is now invoked before loadInbox, syncInbox, syncRecentInbox, searchProvider, registerOrRenewPush, perform, send, and via ensureConnectionIsActive directly for overrideCategory and loadMessageBody — i.e. before essentially every message read/action. This adds a network+decrypt round-trip to the hot path for reading/sending mail, increasing latency and backend load proportional to mail usage, not to connection-management actions.

Consider caching the provider-access snapshot for a short TTL (or per-session) rather than fetching remotely on every call, while still refreshing periodically enough to honor the removal-fencing guarantee.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift`
around lines 1007 - 1035, Update ensureConnectionIsActive and the
provider-access flow to reuse a cached snapshot for a short TTL or session scope
instead of calling definitionSyncService.loadSnapshotForProviderAccess on every
invocation. Preserve periodic refreshes so removed connections are still
detected and connectionRemoved fencing remains enforced, including for direct
ensureConnectionIsActive callers.
apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swift (1)

96-122: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

username isn't case-normalized when deriving connectionId, risking cross-device identity mismatches.

hostname is explicitly .lowercased() before hashing, but username is hashed as-is. If the same mailbox is set up on two devices with the username entered in different casing (e.g. copy/paste vs. manual typing), the derived MailboxConnectionId differs between devices, breaking the dedup/sync-convergence contract this PR relies on (connections created on one device should appear as the same connection on others). Most IMAP/POP3/SMTP providers treat mailbox usernames case-insensitively in practice, and validatedDefinition (line ~446) only trims whitespace, not case.

🐛 Proposed fix
     let identityInput = [
-      username,
+      username.lowercased(),
       incomingEndpoint.mailProtocol.rawValue,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swift`
around lines 96 - 122, Normalize username casing before hashing in the
connectionId property, matching the existing hostname normalization, so
equivalent mailbox configurations produce the same MailboxConnectionId across
devices. Update the username component of identityInput while preserving the
remaining identity fields and hashing flow.
apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift (1)

269-308: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Same double snapshot-fetch pattern as AccountView.refreshConnections().

service.loadSyncedDefinitions(session:) internally calls definitionSyncService.loadSnapshot, and the very next line calls service.loadDefaultSendingConnectionId(session:), which performs a second, independent loadSnapshot (network fetch + decrypt) for a value the first call's snapshot already contained. This doubles the network/decrypt cost of every loadSyncedDefinitions() call (initial load, scene-active, after every connect/remove).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift`
around lines 269 - 308, Update GenericMailSetupViewModel.loadSyncedDefinitions
to load the synced snapshot once and derive both syncedDefinitions and
defaultSendingConnectionId from that result, rather than calling
service.loadDefaultSendingConnectionId separately after
service.loadSyncedDefinitions. Reuse the existing snapshot-backed service/API
symbols and preserve the current sorting, selection, authorization, and
error-handling behavior.
apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift (1)

883-891: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

refreshConnections() performs two independent full snapshot network fetches.

service.loadConnections(session:) internally reconciles/fetches a MailboxConnectionSyncSnapshot (which already carries defaultSendingConnectionId), then service.loadDefaultSendingConnectionId(session:) performs a second, entirely separate loadSnapshot call (network fetch + decrypt) purely to read a field the first call already resolved internally. This doubles latency/backend load every time refreshConnections() runs — on initial load, after connect(), removeLocalAuthorization(), and removeEverywhere(). The same pattern recurs in GenericMailSetupPanel.loadSyncedDefinitions().

Consider exposing the already-computed snapshot (or a combined connections+default-id result) from loadConnections/loadSyncedDefinitions instead of a second independent fetch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift` around
lines 883 - 891, The refreshConnections flow performs two independent snapshot
fetches; update the service API so loadConnections returns the already-resolved
connections together with defaultSendingConnectionId, then assign both values
from that single result and remove the separate loadDefaultSendingConnectionId
call. Apply the same combined-result or snapshot-reuse approach in
GenericMailSetupPanel.loadSyncedDefinitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift`:
- Around line 883-891: The refreshConnections flow performs two independent
snapshot fetches; update the service API so loadConnections returns the
already-resolved connections together with defaultSendingConnectionId, then
assign both values from that single result and remove the separate
loadDefaultSendingConnectionId call. Apply the same combined-result or
snapshot-reuse approach in GenericMailSetupPanel.loadSyncedDefinitions.

In `@apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift`:
- Around line 269-308: Update GenericMailSetupViewModel.loadSyncedDefinitions to
load the synced snapshot once and derive both syncedDefinitions and
defaultSendingConnectionId from that result, rather than calling
service.loadDefaultSendingConnectionId separately after
service.loadSyncedDefinitions. Reuse the existing snapshot-backed service/API
symbols and preserve the current sorting, selection, authorization, and
error-handling behavior.

In
`@apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swift`:
- Around line 96-122: Normalize username casing before hashing in the
connectionId property, matching the existing hostname normalization, so
equivalent mailbox configurations produce the same MailboxConnectionId across
devices. Update the username component of identityInput while preserving the
remaining identity fields and hashing flow.

In `@apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift`:
- Around line 1007-1035: Update ensureConnectionIsActive and the provider-access
flow to reuse a cached snapshot for a short TTL or session scope instead of
calling definitionSyncService.loadSnapshotForProviderAccess on every invocation.
Preserve periodic refreshes so removed connections are still detected and
connectionRemoved fencing remains enforced, including for direct
ensureConnectionIsActive callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 741c967a-ce0d-491e-b1fa-c4c1475be764

📥 Commits

Reviewing files that changed from the base of the PR and between 009ff6b and 280c35a.

📒 Files selected for processing (6)
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionDefinition.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncService.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 280c35a12d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift Outdated
Comment thread apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift Outdated
@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 149bc0e687

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift Outdated

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

Please review the latest review-thread fixes in 752e11e.

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review the latest review-thread fixes in 752e11e.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

@rajzik: Reviewing the latest review-thread fixes in 752e11e.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift (1)

868-878: 🗄️ Data Integrity & Integration | 🟠 Major

Preserve the synchronized default after global removal.

If the removed connection is selected while another synchronized default remains, this unconditionally selects connections.first, silently replacing the default-aware selection. Reconcile with defaultSendingConnectionId: select its matching connection, select the first only when the default is nil, otherwise leave selection unset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift` around
lines 868 - 878, Update removeEverywhere so that when the removed connection was
selected, it preserves the synchronized default selection: choose the connection
matching defaultSendingConnectionId when present, choose connections.first only
when the default ID is nil, and otherwise leave selectedConnectionId unset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift`:
- Around line 868-878: Update removeEverywhere so that when the removed
connection was selected, it preserves the synchronized default selection: choose
the connection matching defaultSendingConnectionId when present, choose
connections.first only when the default ID is nil, and otherwise leave
selectedConnectionId unset.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b574e70c-25b1-41e1-84e6-424a352dfda3

📥 Commits

Reviewing files that changed from the base of the PR and between 149bc0e and 752e11e.

📒 Files selected for processing (2)
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mail/Services/Gmail/GmailPushRelayService.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 752e11eb49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift Outdated
Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift Outdated
Comment thread apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift Outdated
Comment thread apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift Outdated
@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@rajzik

rajzik commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift (2)

898-905: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reorder refreshConnections() to assign only after both reads succeed.

connections is assigned immediately after the first await, then defaultSendingConnectionId is fetched with a second await that can throw. If it throws, connections is left updated while defaultSendingConnectionId remains stale — the exact partial-update hazard this contract was previously fixed for (see past review on try? conversion). Compute both locally before assigning either property.

🐛 Proposed fix
 private func refreshConnections() async throws {
   let loadedConnections = try await service.loadConnections(session: session)
     .sorted {
       $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending
     }
-    connections = loadedConnections
-    defaultSendingConnectionId = try await service.loadDefaultSendingConnectionId(session: session)
+  let loadedDefaultSendingConnectionId = try await service.loadDefaultSendingConnectionId(
+    session: session
+  )
+  connections = loadedConnections
+  defaultSendingConnectionId = loadedDefaultSendingConnectionId
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift` around
lines 898 - 905, Update refreshConnections() to load and sort the connections
and fetch defaultSendingConnectionId into local values first; assign both
properties only after both awaited service calls succeed, preserving the
existing sort and error propagation.

852-864: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse refreshConnections() in the error path instead of duplicating it.

This catch block re-implements a connections-only reload (sort + push-status filter) instead of calling try? await refreshConnections() as removeEverywhere does. It duplicates logic and never refreshes defaultSendingConnectionId, so a failed local-authorization removal can leave the default stale until the next full load().

♻️ Proposed fix
     } catch {
-      if let refreshedConnections = try? await service.loadConnections(session: session) {
-        connections = refreshedConnections.sorted {
-          $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending
-        }
-        pushStatusMessages = pushStatusMessages.filter { connectionId, _ in
-          connections.contains { $0.id == connectionId }
-        }
-        if selectedConnectionId == connection.id {
-          selectedConnectionId = connections.first?.id
-        }
-      }
+      try? await refreshConnections()
+      pushStatusMessages = pushStatusMessages.filter { connectionId, _ in
+        connections.contains { $0.id == connectionId }
+      }
+      if selectedConnectionId == connection.id {
+        selectedConnectionId = connections.first?.id
+      }
       errorMessage = error.localizedDescription
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift` around
lines 852 - 864, In the catch block handling the removal failure, replace the
duplicated loadConnections sorting, push-status filtering, and selection update
with a best-effort call to refreshConnections(), matching the removeEverywhere
error path. Preserve assigning error.localizedDescription to errorMessage after
the refresh attempt so defaultSendingConnectionId and all connection-related
state are refreshed consistently.
apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift (1)

281-318: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Compute authorizedSyncedConnectionIds before mutating other state.

syncedDefinitions and defaultSendingConnectionId are assigned before the throwing try Set(...) computation over hasLocalAuthorization. If that throws for any definition, this catch aborts, but syncedDefinitions/defaultSendingConnectionId are already updated while authorizedSyncedConnectionIds stays stale relative to the new definitions list — the same partial-state-update pattern flagged (and fixed) in AccountView's refreshConnections(). It fails safe (unmatched IDs read as unauthorized) but is still an avoidable inconsistency.

🐛 Proposed fix
     do {
       let definitions = try await service.loadSyncedDefinitions(session: syncSession)
         .sorted { $0.emailAddress < $1.emailAddress }
-      defaultSendingConnectionId = try await service.loadDefaultSendingConnectionId(
+      let loadedDefaultSendingConnectionId = try await service.loadDefaultSendingConnectionId(
         session: syncSession
       )
-      syncedDefinitions = definitions
-      authorizedSyncedConnectionIds = try Set(
+      let loadedAuthorizedIds = try Set(
         definitions.compactMap { definition in
           try service.hasLocalAuthorization(
             definition,
             productAccountId: productAccountId
           ) ? definition.connectionId : nil
         }
       )
+      defaultSendingConnectionId = loadedDefaultSendingConnectionId
+      syncedDefinitions = definitions
+      authorizedSyncedConnectionIds = loadedAuthorizedIds
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift`
around lines 281 - 318, In loadSyncedDefinitions, compute the throwing
authorizedSyncedConnectionIds Set from definitions before assigning
syncedDefinitions or defaultSendingConnectionId. Only mutate those state
properties after all service calls and authorization checks succeed, preserving
the existing catch behavior without leaving partially updated state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift`:
- Around line 898-905: Update refreshConnections() to load and sort the
connections and fetch defaultSendingConnectionId into local values first; assign
both properties only after both awaited service calls succeed, preserving the
existing sort and error propagation.
- Around line 852-864: In the catch block handling the removal failure, replace
the duplicated loadConnections sorting, push-status filtering, and selection
update with a best-effort call to refreshConnections(), matching the
removeEverywhere error path. Preserve assigning error.localizedDescription to
errorMessage after the refresh attempt so defaultSendingConnectionId and all
connection-related state are refreshed consistently.

In `@apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift`:
- Around line 281-318: In loadSyncedDefinitions, compute the throwing
authorizedSyncedConnectionIds Set from definitions before assigning
syncedDefinitions or defaultSendingConnectionId. Only mutate those state
properties after all service calls and authorization checks succeed, preserving
the existing catch behavior without leaving partially updated state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a9d3ee16-d85c-4470-9dec-5268d4d779c0

📥 Commits

Reviewing files that changed from the base of the PR and between 752e11e and 3b75bff.

📒 Files selected for processing (3)
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swift
  • apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b75bffed9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +788 to +792
if defaultSendingConnectionId?.providerId == .gmail {
selectedConnectionId = connections.first { $0.id == defaultSendingConnectionId }?.id
} else {
selectedConnectionId = connections.first?.id
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve non-Gmail default instead of auto-selecting Gmail

When the synced Default Sending Connection belongs to another provider (for example an IMAP/SMTP connection) and the device also has Gmail connected, this branch silently selects the first Gmail account during load. That re-enables the Gmail compose/send UI with a different sender even though the product rules say a new message defaults to the Default Sending Connection and should require authorization or an explicit sender choice rather than substituting another identity. Leave the selection empty/unchanged for non-Gmail defaults, or require the user to explicitly choose Gmail before sending.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left unresolved: this conflicts with the existing adapter test and earlier accepted PR decision to fall back to an available Gmail mailbox when the synchronized default belongs to another provider. Resolving the conflict requires product direction on Gmail UI selection versus default-sender semantics.

Comment on lines +190 to +192
payload.connections.removeAll { $0.id == definition.id }
payload.connections.append(definition)
payload.removals.removeAll { $0.connectionId == definition.id }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require explicit recreation before clearing tombstones

When another trusted device removes a connection everywhere while this device still has a stale synced definition on screen, using the existing “Authorize on This Device” flow saves that definition and this line removes the tombstone from the latest Product Sync payload. That resurrects a globally removed Mailbox Connection even though the user only reauthorized the stale local row, so removed connections can come back across devices instead of converging to deletion; keep the tombstone unless the caller is explicitly creating a new connection after learning the removal state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to #97: distinguishing device reauthorization from deliberate recreation needs an explicit product flow. The issue records the tombstone-preservation behavior and regression coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Synchronize Mailbox Connection definitions across devices

1 participant