Repository navigation
Synchronize Mailbox Connection definitions across devices - #91
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesMailbox Connection synchronization
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@codex review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
1f25a35 to
6009f47
Compare
|
@codex review |
There was a problem hiding this comment.
💡 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".
|
@codex review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 liftEvery provider-access path now performs a fresh remote/cache round trip before the actual Gmail call.
loadInbox,syncInbox,syncRecentInbox,searchProvider,registerOrRenewPush,perform, andsendall funnel throughgmailConnectionForProviderAccess→ensureConnectionIsActive, which callsdefinitionSyncService.loadSnapshotForProviderAccess(a Product Sync network fetch, falling back to Keychain cache) on every single invocation. For high-frequency paths likeloadMessageBody/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
📒 Files selected for processing (16)
.changeset/synchronize-mailbox-connections.mdREADME.mdapps/unwired-mail/unwired-mail.xcodeproj/project.pbxprojapps/unwired-mail/unwired-mail/Features/Account/AccountView.swiftapps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swiftapps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailProviderCatalog.swiftapps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swiftapps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swiftapps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionDefinition.swiftapps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncCacheStore.swiftapps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncService.swiftapps/unwired-mail/unwired-mailTests/GenericMailSetupServiceTests.swiftapps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swiftapps/unwired-mail/unwired-mailTests/MailboxConnectionSyncServiceTests.swiftdocs/adr/0001-end-to-end-encrypted-product-sync.mddocs/adr/0010-device-local-mailbox-authorization.md
There was a problem hiding this comment.
💡 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".
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 4 file(s) based on 4 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 4 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit <[email protected]>
|
@codex review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@codex review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 liftEvery provider-access call now performs a full remote sync fetch.
gmailConnectionForProviderAccess→ensureConnectionIsActivecallsdefinitionSyncService.loadSnapshotForProviderAccess, which starts with a network round-trip (loadRemotePayload) plus decrypt, on every invocation. This helper is now invoked beforeloadInbox,syncInbox,syncRecentInbox,searchProvider,registerOrRenewPush,perform,send, and viaensureConnectionIsActivedirectly foroverrideCategoryandloadMessageBody— 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
usernameisn't case-normalized when derivingconnectionId, risking cross-device identity mismatches.
hostnameis explicitly.lowercased()before hashing, butusernameis 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 derivedMailboxConnectionIddiffers 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, andvalidatedDefinition(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 liftSame double snapshot-fetch pattern as
AccountView.refreshConnections().
service.loadSyncedDefinitions(session:)internally callsdefinitionSyncService.loadSnapshot, and the very next line callsservice.loadDefaultSendingConnectionId(session:), which performs a second, independentloadSnapshot(network fetch + decrypt) for a value the first call's snapshot already contained. This doubles the network/decrypt cost of everyloadSyncedDefinitions()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 aMailboxConnectionSyncSnapshot(which already carriesdefaultSendingConnectionId), thenservice.loadDefaultSendingConnectionId(session:)performs a second, entirely separateloadSnapshotcall (network fetch + decrypt) purely to read a field the first call already resolved internally. This doubles latency/backend load every timerefreshConnections()runs — on initial load, afterconnect(),removeLocalAuthorization(), andremoveEverywhere(). The same pattern recurs inGenericMailSetupPanel.loadSyncedDefinitions().Consider exposing the already-computed snapshot (or a combined connections+default-id result) from
loadConnections/loadSyncedDefinitionsinstead 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
📒 Files selected for processing (6)
apps/unwired-mail/unwired-mail/Features/Account/AccountView.swiftapps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swiftapps/unwired-mail/unwired-mail/Services/Mailbox/GenericMailSetupService.swiftapps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swiftapps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionDefinition.swiftapps/unwired-mail/unwired-mail/Services/ProductSync/MailboxConnectionSyncService.swift
There was a problem hiding this comment.
💡 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".
|
@codex review |
There was a problem hiding this comment.
💡 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".
|
@coderabbitai review Please review the latest review-thread fixes in 752e11e. |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 | 🟠 MajorPreserve 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 withdefaultSendingConnectionId: select its matching connection, select the first only when the default isnil, 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
📒 Files selected for processing (2)
apps/unwired-mail/unwired-mail/Features/Account/AccountView.swiftapps/unwired-mail/unwired-mail/Services/Gmail/GmailPushRelayService.swift
There was a problem hiding this comment.
💡 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".
|
@codex review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 winReorder
refreshConnections()to assign only after both reads succeed.
connectionsis assigned immediately after the firstawait, thendefaultSendingConnectionIdis fetched with a secondawaitthat can throw. If it throws,connectionsis left updated whiledefaultSendingConnectionIdremains stale — the exact partial-update hazard this contract was previously fixed for (see past review ontry?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 winReuse
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()asremoveEverywheredoes. It duplicates logic and never refreshesdefaultSendingConnectionId, so a failed local-authorization removal can leave the default stale until the next fullload().♻️ 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 winCompute
authorizedSyncedConnectionIdsbefore mutating other state.
syncedDefinitionsanddefaultSendingConnectionIdare assigned before the throwingtry Set(...)computation overhasLocalAuthorization. If that throws for any definition, this catch aborts, butsyncedDefinitions/defaultSendingConnectionIdare already updated whileauthorizedSyncedConnectionIdsstays stale relative to the new definitions list — the same partial-state-update pattern flagged (and fixed) inAccountView'srefreshConnections(). 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
📒 Files selected for processing (3)
apps/unwired-mail/unwired-mail/Features/Account/AccountView.swiftapps/unwired-mail/unwired-mail/Features/Account/GenericMailSetupPanel.swiftapps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift
There was a problem hiding this comment.
💡 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".
| if defaultSendingConnectionId?.providerId == .gmail { | ||
| selectedConnectionId = connections.first { $0.id == defaultSendingConnectionId }?.id | ||
| } else { | ||
| selectedConnectionId = connections.first?.id | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| payload.connections.removeAll { $0.id == definition.id } | ||
| payload.connections.append(definition) | ||
| payload.removals.removeAll { $0.connectionId == definition.id } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Deferred to #97: distinguishing device reauthorization from deliberate recreation needs an explicit product flow. The issue records the tombstone-preservation behavior and regression coverage.
Closes #52
Summary
Validation
mise exec -- pnpm install --frozen-lockfilemise exec -- pnpm lintmise exec -- pnpm formatmise exec -- pnpm turbo run check-typesmise exec -- pnpm testmise exec -- pnpm fallowmise exec -- zsh scripts/check-apple-lint.zshmise 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.
Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.Summary by CodeRabbit