Skip to content

Sync Inbox preferences offline - #367

Merged
rajzik merged 6 commits into
mainfrom
codex/issue-117-sync-inbox-preferences
Aug 8, 2026
Merged

rajzik merged 6 commits into
mainfrom
codex/issue-117-sync-inbox-preferences

Conversation

@gipity-bot

@gipity-bot gipity-bot Bot commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #117

Summary

  • add an Inbox Settings destination for density, preview length, contact images, Category badges, and attachment indicators
  • persist offline edits locally, queue encrypted Product Sync writes, merge non-overlapping fields, and preserve same-field conflicts for explicit resolution
  • apply the preferences to thread rows and make Unified Inbox the transient new-session selection
  • add migration-safe defaults, account-cleanup wiring, focused tests, ADR updates, and a patch changeset

The fixed Important Mail View remains owned by related issue #159; this change establishes the non-persisted Unified Inbox launch selection that view will use.

Validation

  • mise exec -- zsh scripts/check-apple-lint.zsh — passed; SwiftLint found 0 violations in 99 files
  • git diff --check — passed
  • plutil -lint apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj — passed
  • focused xcodebuild test for InboxPreferenceSyncServiceTests and SettingsDestinationRegistryTests — passed, 59 tests
  • required Debug Apple test pass excluding the separately run mixed/performance fixtures — passed, 1,640 tests in 35 suites with the repository's one expected known issue
  • isolated mixed-connection scenario — passed, 1 test
  • Release-only cached-presentation performance fixture with CI budgets — passed, 1 test

All Simulator validation used iPhone 17 / iOS 26.5 UDID D6D43A14-3B1A-41B6-B25A-0DFCA1BFB089 after explicit boot readiness checks. No Busy recovery was required.

Remaining human actions

None before merge or issue completion.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 040f1fab-e562-4890-a7e2-09c582a04dc1

📥 Commits

Reviewing files that changed from the base of the PR and between d30bc9b and 07ea0fb.

📒 Files selected for processing (2)
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
📝 Walkthrough

Summary

  • Added Inbox Settings for density, preview length, contact images, Category badges, and attachment indicators.
  • Persisted offline edits locally and retried encrypted Product Sync writes.
  • Merged non-overlapping changes and preserved same-field conflicts for explicit resolution.
  • Started new sessions in Unified Inbox → Important.
  • Kept launch mailbox and current Mail View as transient device-local state.
  • Added migration-safe defaults and account-cleanup handling.
  • Added attachment detection for Gmail, EWS, IMAP, and Microsoft Graph metadata.
  • Updated ADRs and added a patch changeset.

Privacy and security

  • Synced preferences through the versioned mail-workflow-preferences:inbox record.
  • Sent only encrypted preference payloads to Product Sync.
  • Kept effective values, pending edits, and conflicts in trusted-device local storage.
  • Kept plaintext preferences hidden from the backend.

Affected areas

  • Apple: SwiftUI account and settings views, Product Sync services, mailbox metadata and attachment detection, product-account cleanup, Xcode targets, and Apple tests.
  • TypeScript: No TypeScript files are included in the reported changes.

Validation

  • Linting and project-file checks passed.
  • Focused and required Apple tests passed.
  • Mixed-connection testing passed.
  • The Release performance fixture passed.

Walkthrough

The PR adds offline Inbox preference synchronization, encrypted Product Sync storage, explicit field conflicts, Inbox settings controls, preference-driven thread rendering, and attachment metadata across mailbox providers. New sessions start in Unified Inbox.

Changes

Inbox preference synchronization

Layer / File(s) Summary
Preference contract and Product Sync service
apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift, docs/adr/*
Defines versioned preference values, conditional synchronization results, encrypted Product Sync operations, and offline conflict rules.
Local persistence and conflict reconciliation
apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift, apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift, apps/unwired-mail/unwired-mail/Services/ProductAccount/ProductAccountSession.swift
Persists account-scoped edits, retries synchronization, merges non-overlapping fields, records same-field conflicts, and clears stored state during account cleanup.
Settings and Inbox presentation integration
apps/unwired-mail/unwired-mail/Features/Settings/*, apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift, apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj
Adds Inbox settings and routing, synchronizes on account and foreground events, applies preferences to thread rows, and makes Unified Inbox the initial selection.
Attachment metadata propagation
apps/unwired-mail/unwired-mail/Services/Gmail/*, apps/unwired-mail/unwired-mail/Services/Mailbox/*, apps/unwired-mail/unwired-mailTests/*
Detects and propagates attachment state through Gmail, EWS, IMAP, and Microsoft Graph metadata flows.

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

Possibly related issues

  • unwired-dev/product#164 — Covers Inbox workflow preferences, offline synchronization, and conflict resolution.
  • unwired-dev/product#159 — Covers synchronized Inbox presentation preferences and defaults.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.17% 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
Title check ✅ Passed The title clearly summarizes the primary change: offline synchronization of Inbox preferences.
Description check ✅ Passed The description directly explains the Inbox preference synchronization, UI, conflict handling, tests, and scope.
Linked Issues check ✅ Passed The changes satisfy the coding objectives for synchronized Inbox preferences, offline persistence, conflict resolution, launch behavior, encryption, migrations, and tests [#117].
Out of Scope Changes check ✅ Passed The changes remain related to Inbox preferences, attachment indicators, synchronization, account cleanup, documentation, project wiring, and tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-117-sync-inbox-preferences

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

@rajzik
rajzik marked this pull request as ready for review August 8, 2026 14:14
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

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 Aug 8, 2026

Copy link
Copy Markdown
Contributor

@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: c71f36693a

ℹ️ 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/Services/ProductSync/InboxPreferenceStore.swift Outdated

@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: 6

🤖 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/Features/Account/AccountView.swift`:
- Around line 1542-1546: Update the accountSettings routing for
openSettings(.inbox) to include InboxSettingsView, passing the existing
inboxPreferenceStore and navigationRequest values. Ensure the inbox case is
exposed through accountSettings so release builds can open and edit inbox
preferences.

In `@apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift`:
- Around line 843-844: Populate hasAttachments during every mailbox metadata
conversion using each provider’s attachment metadata, and preserve the value
when reconstructing mailbox metadata so AccountView receives the correct state.
Update the relevant conversion and reconstruction methods around bccRecipients,
and add coverage for both attached and unattached messages.

In
`@apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift`:
- Around line 93-101: Update the restoration logic in the ProductSync preference
state-loading flow to use migration-aware decoding for older or partially
decodable local payloads, preserving recoverable stored preferences and
conflicts. If decoding cannot safely recover the stored state, mark restoration
as failed and prevent subsequent synchronization or lifecycle transitions from
persisting .empty or .defaults over it; add coverage for an older and partially
decodable payload.
- Around line 141-199: Bind synchronize() to the account generation captured at
startup, and after every await in its load/save retry flow exit if
updateSession() has advanced that generation. Ensure only the task belonging to
the current generation clears syncTask, while preserving cancellation handling
and actor isolation across account transitions. Add a blocking synchronization
test that switches accounts while loading and verifies no old-account state is
reconciled or saved into the new account.

In
`@apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift`:
- Around line 72-90: Update InboxPreferenceSyncService’s init(from:) to validate
the decoded schemaVersion against the client’s supported schema version and
reject newer versions before applying or reconciling any fields. Add a test
covering a future schema payload and verify decoding fails without allowing a
subsequent local write to discard unknown values.

In `@docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md`:
- Around line 4-5: Update the ADR statement about transient navigation state to
explicitly identify the Mail View and selected mailbox as device-local state,
and state that every new session starts in Unified Inbox with Important
selected. Preserve the existing synchronization scope and use the precise
navigation terms from CONTEXT.md.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9ecc6cee-6b66-46f5-8dbb-75d45004262b

📥 Commits

Reviewing files that changed from the base of the PR and between 1e76216 and ac47715.

📒 Files selected for processing (14)
  • .changeset/sync-inbox-preferences.md
  • apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mail/Features/Settings/InboxSettingsView.swift
  • apps/unwired-mail/unwired-mail/Features/Settings/SettingsScene.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mail/Services/ProductAccount/ProductAccountSession.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/SettingsDestinationRegistryTests.swift
  • docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md
  • docs/adr/0022-save-workflow-preferences-offline-with-explicit-conflicts.md
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Apple Debug · Unwired Mail
  • GitHub Check: Apple performance · Unwired Mail
🧰 Additional context used
📓 Path-based instructions (6)
.changeset/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Create a Changeset for runtime behavior changes or exported type/API changes; tests-only, internal refactors, documentation-only changes, and JSDoc-only maintenance may omit one at maintainer discretion.

Files:

  • .changeset/sync-inbox-preferences.md
docs/adr/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Record durable architecture decisions and privacy-boundary decisions in docs/adr/.

Files:

  • docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md
  • docs/adr/0022-save-workflow-preferences-offline-with-explicit-conflicts.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: Treat docs as product truth, not marketing. Prefer precise domain language
from CONTEXT.md; flag undefined terms, synonym drift, and conflicts with
existing ADRs under docs/adr/. For ADR changes, require a clear decision,
consequences, and privacy or security boundary impact; flag silent
reversals of prior ADRs without an explicit supersession note. For
docs/agents/ and bootstrap docs, verify agent workflow, validation, and
triage guidance stay aligned with AGENTS.md. Flag broken cross-links,
speculative future claims presented as shipped behavior, and docs that
omit the Apple vs TypeScript ownership of a boundary they describe.

Files:

  • docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md
  • docs/adr/0022-save-workflow-preferences-offline-with-explicit-conflicts.md
apps/unwired-mail/**/*.{swift,m,mm}

📄 CodeRabbit inference engine (AGENTS.md)

The Apple app must provide formatter, linter, and test commands. Run zsh scripts/check-apple-lint.zsh for format and lint, and use the documented xcodebuild test command for tests; SwiftLint runs in strict mode.

Files:

  • apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/ProductAccount/ProductAccountSession.swift
  • apps/unwired-mail/unwired-mail/Features/Settings/InboxSettingsView.swift
  • apps/unwired-mail/unwired-mailTests/SettingsDestinationRegistryTests.swift
  • apps/unwired-mail/unwired-mail/Features/Settings/SettingsScene.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
apps/unwired-mail/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Apple validation must use SwiftLint strict mode and Swift Format strict linting; if Apple tooling is unavailable, report that clearly in the final handoff.

Files:

  • apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/ProductAccount/ProductAccountSession.swift
  • apps/unwired-mail/unwired-mail/Features/Settings/InboxSettingsView.swift
  • apps/unwired-mail/unwired-mailTests/SettingsDestinationRegistryTests.swift
  • apps/unwired-mail/unwired-mail/Features/Settings/SettingsScene.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift

⚙️ CodeRabbit configuration file

apps/unwired-mail/**/*.swift: Prioritize functional correctness, data protection, and Apple platform
behavior over style already enforced by swift-format and SwiftLint.
Check actor isolation, cancellation, lifecycle transitions, persistence,
retry and offline behavior, and Keychain accessibility. Flag credentials,
tokens, mail content, or decrypted Product Sync data that can be logged,
persisted outside approved device-local storage, or cross a documented
privacy boundary. Require focused tests for changed behavior and failure
paths.

Files:

  • apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/ProductAccount/ProductAccountSession.swift
  • apps/unwired-mail/unwired-mail/Features/Settings/InboxSettingsView.swift
  • apps/unwired-mail/unwired-mailTests/SettingsDestinationRegistryTests.swift
  • apps/unwired-mail/unwired-mail/Features/Settings/SettingsScene.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
**/*Tests/**/*.swift

⚙️ CodeRabbit configuration file

**/*Tests/**/*.swift: Verify externally observable behavior, privacy invariants, error paths,
cancellation or restart behavior where relevant, and deterministic
isolation. Flag fragile sleeps, ordering dependencies, and tests that
duplicate the implementation instead of exercising it.

Files:

  • apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/SettingsDestinationRegistryTests.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
🪛 markdownlint-cli2 (0.23.2)
.changeset/sync-inbox-preferences.md

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🔇 Additional comments (12)
apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift (1)

3-61: LGTM!

Also applies to: 93-283

apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift (1)

4-59: LGTM!

Also applies to: 104-139, 202-262

apps/unwired-mail/unwired-mail/Services/ProductAccount/ProductAccountSession.swift (1)

138-142: LGTM!

apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift (1)

16-175: LGTM!

Also applies to: 178-268

apps/unwired-mail/unwired-mailTests/SettingsDestinationRegistryTests.swift (1)

212-214: LGTM!

Also applies to: 235-235, 938-942, 955-955

.changeset/sync-inbox-preferences.md (1)

1-5: LGTM!

docs/adr/0022-save-workflow-preferences-offline-with-explicit-conflicts.md (1)

4-5: LGTM!

apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift (1)

1071-1121: LGTM!

Also applies to: 1259-1259, 1448-1448, 1593-1603, 1652-1652, 2517-2517, 3224-3224, 3277-3279, 3522-3524, 3690-3813

apps/unwired-mail/unwired-mail/Features/Settings/InboxSettingsView.swift (1)

3-181: LGTM!

apps/unwired-mail/unwired-mail/Features/Settings/SettingsScene.swift (1)

260-275: LGTM!

Also applies to: 557-557, 1763-1821, 1894-1917

apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj (1)

84-87: LGTM!

Also applies to: 203-206, 274-274, 322-323, 346-346, 500-500, 548-549, 570-570

apps/unwired-mail/unwired-mailTests/MailboxConnectionAdapterTests.swift (1)

4254-4256: LGTM!

Comment thread apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
Comment thread docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md Outdated

@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: 1

🤖 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-mailTests/InboxPreferenceSyncServiceTests.swift`:
- Around line 157-162: Replace the fixed 1,000-iteration Task.yield polling
around the second synchronization with an explicit completion signal, such as a
continuation tied to completion of the rescheduled save/synchronization. Await
that signal before the final assertions, while preserving the existing saveCount
and store-state expectations.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 89cdceb6-cb89-4db0-8feb-393f65beece2

📥 Commits

Reviewing files that changed from the base of the PR and between ac47715 and c2f1fe8.

📒 Files selected for processing (14)
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mail/Services/Gmail/GmailMessageMetadataService.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/IMAPMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MicrosoftGraphMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/SystemEWSClient.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/SystemIMAPMailboxClient.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/GmailMessageMetadataServiceTests.swift
  • apps/unwired-mail/unwired-mailTests/IMAPMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mailTests/MicrosoftGraphMailboxConnectionAdapterTests.swift
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Apple performance · Unwired Mail
  • GitHub Check: Apple Debug · Unwired Mail
🧰 Additional context used
📓 Path-based instructions (3)
apps/unwired-mail/**/*.{swift,m,mm}

📄 CodeRabbit inference engine (AGENTS.md)

The Apple app must provide formatter, linter, and test commands. Run zsh scripts/check-apple-lint.zsh for format and lint, and use the documented xcodebuild test command for tests; SwiftLint runs in strict mode.

Files:

  • apps/unwired-mail/unwired-mail/Services/Mailbox/IMAPMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mailTests/IMAPMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/SystemEWSClient.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mailTests/MicrosoftGraphMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MicrosoftGraphMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/SystemIMAPMailboxClient.swift
  • apps/unwired-mail/unwired-mail/Services/Gmail/GmailMessageMetadataService.swift
  • apps/unwired-mail/unwired-mailTests/GmailMessageMetadataServiceTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift
apps/unwired-mail/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Apple validation must use SwiftLint strict mode and Swift Format strict linting; if Apple tooling is unavailable, report that clearly in the final handoff.

Files:

  • apps/unwired-mail/unwired-mail/Services/Mailbox/IMAPMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mailTests/IMAPMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/SystemEWSClient.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mailTests/MicrosoftGraphMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MicrosoftGraphMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/SystemIMAPMailboxClient.swift
  • apps/unwired-mail/unwired-mail/Services/Gmail/GmailMessageMetadataService.swift
  • apps/unwired-mail/unwired-mailTests/GmailMessageMetadataServiceTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift

⚙️ CodeRabbit configuration file

apps/unwired-mail/**/*.swift: Prioritize functional correctness, data protection, and Apple platform
behavior over style already enforced by swift-format and SwiftLint.
Check actor isolation, cancellation, lifecycle transitions, persistence,
retry and offline behavior, and Keychain accessibility. Flag credentials,
tokens, mail content, or decrypted Product Sync data that can be logged,
persisted outside approved device-local storage, or cross a documented
privacy boundary. Require focused tests for changed behavior and failure
paths.

Files:

  • apps/unwired-mail/unwired-mail/Services/Mailbox/IMAPMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mailTests/IMAPMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/SystemEWSClient.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift
  • apps/unwired-mail/unwired-mailTests/MicrosoftGraphMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/MicrosoftGraphMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/SystemIMAPMailboxClient.swift
  • apps/unwired-mail/unwired-mail/Services/Gmail/GmailMessageMetadataService.swift
  • apps/unwired-mail/unwired-mailTests/GmailMessageMetadataServiceTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift
**/*Tests/**/*.swift

⚙️ CodeRabbit configuration file

**/*Tests/**/*.swift: Verify externally observable behavior, privacy invariants, error paths,
cancellation or restart behavior where relevant, and deterministic
isolation. Flag fragile sleeps, ordering dependencies, and tests that
duplicate the implementation instead of exercising it.

Files:

  • apps/unwired-mail/unwired-mailTests/IMAPMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/MicrosoftGraphMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • apps/unwired-mail/unwired-mailTests/GmailMessageMetadataServiceTests.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
🔇 Additional comments (21)
apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift (2)

268-275: Keep the replacement synchronization task registered.

Line 272 clears syncTask after an await without checking task or session ownership. If an account switch cancels this task and starts a replacement task, the obsolete task can clear the replacement handle. A later edit can then start concurrent synchronization.

Bind the task to its session generation. Clear syncTask only when that generation is still current.


73-73: LGTM!

Also applies to: 204-204

apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift (1)

225-225: LGTM!

Also applies to: 242-242, 258-283

apps/unwired-mail/unwired-mail/Features/Account/AccountView.swift (1)

2034-2038: LGTM!

apps/unwired-mail/unwired-mail/Services/Mailbox/MailboxConnection.swift (1)

844-844: hasAttachments propagation gap from the earlier review is resolved.
A prior review flagged that hasAttachments was left at its default false during every provider-to-MailboxMessageMetadata conversion and during the reverse conversion back to provider metadata. This round adds hasAttachments: hasAttachments ?? false at Line 1088 (Gmail to MailboxMessageMetadata) and hasAttachments: hasAttachments ? true : nil at Line 1098 (MailboxMessageMetadata back to Gmail), and the equivalent EWS, IMAP, and Microsoft Graph adapters in this PR each populate hasAttachments in their own mailboxMetadata() conversions. The gap is closed.

Also applies to: 1071-1114

apps/unwired-mail/unwired-mail/Services/Gmail/GmailMessageMetadataService.swift (4)

13-13: hasAttachments default is consistent with the fetch logic.
This field stays nil until Gmail explicitly reports an attachment, matching the ternary at Line 2667 that never assigns false. No issue here.


2631-2646: 🎯 Functional Correctness | 💤 Low value

Verify attachment detection at deep MIME nesting levels.
The fields selector expands parts to a fixed depth of four levels (payload(filename,headers,parts(filename,headers,parts(filename,headers,parts(filename,headers))))). Gmail message trees that nest multipart containers deeper than four levels (for example, forwarded messages wrapped in another multipart container, or S/MIME-wrapped messages) return a payload where deeper attachments are outside the requested field mask. GmailMessagePayload.hasAttachments at Line 3304 only sees the parts included in the response, so it reports no attachment even when one exists deeper in the tree.

Confirm whether this depth is sufficient for the mailbox population you support, or add a fallback (for example, detect truncation and re-fetch with a wider field mask) for messages with deeper nesting.


3299-3306: 🎯 Functional Correctness | ⚡ Quick win

Verify that filename does not also appear on inline (non-attachment) parts.
hasAttachments treats any part with a non-empty filename as an attachment. Gmail API documentation states that filename is present only for parts that represent an attachment, but some inline, cid:-referenced images observed in practice also carry a filename value. If Gmail populates filename for inline images in your account population, this logic reports hasAttachments == true for messages that only contain inline images, showing an incorrect attachment indicator to the user.

Compare this against IMAPBodyStructure.isAttachment in SystemIMAPMailboxClient.swift, which additionally checks the Content-Disposition value. Confirm whether Gmail requires a similar check, for example by also inspecting the part's mimeType or a Content-Disposition header, before relying on filename alone.


2662-2686: LGTM!

apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift (2)

1571-1633: LGTM!


1682-1684: hasAttachments is now populated for EWS mailbox metadata.
This addresses the earlier finding that hasAttachments was not populated during mailbox metadata conversion. EWS reports a definitive HasAttachments value in SystemEWSClient.swift (Line 956), so hasAttachments ?? false here correctly reflects the provider signal.

apps/unwired-mail/unwired-mail/Services/Mailbox/IMAPMailboxConnectionAdapter.swift (1)

31-107: LGTM!

apps/unwired-mail/unwired-mail/Services/Mailbox/MicrosoftGraphMailboxConnectionAdapter.swift (2)

367-481: LGTM!


1370-1424: LGTM!

apps/unwired-mail/unwired-mail/Services/Mailbox/SystemEWSClient.swift (1)

413-413: LGTM!

Also applies to: 956-956

apps/unwired-mail/unwired-mail/Services/Mailbox/SystemIMAPMailboxClient.swift (1)

778-839: Attachment detection follows the same disposition-aware heuristic as preferredTextPart.
containsAttachment reuses isAttachment, which checks the Content-Disposition list and the NAME/FILENAME parameters. This is more precise than the Gmail path's filename-only check (see the corresponding comment on GmailMessageMetadataService.swift), and correctly excludes inline text parts that only carry a CHARSET parameter.

apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift (1)

2895-2921: LGTM!

Also applies to: 6119-6138

apps/unwired-mail/unwired-mailTests/GmailMessageMetadataServiceTests.swift (2)

943-985: LGTM!


6388-6656: LGTM!

apps/unwired-mail/unwired-mailTests/IMAPMailboxConnectionAdapterTests.swift (1)

1119-1160: LGTM!

apps/unwired-mail/unwired-mailTests/MicrosoftGraphMailboxConnectionAdapterTests.swift (1)

1738-1738: LGTM!

Also applies to: 2082-2082, 2104-2104

@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: 1

🤖 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 `@docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md`:
- Line 5: Replace the term “Inbox presentation preferences” in the ADR
description with the canonical “Mail Workflow Preferences” terminology defined
by CONTEXT.md, while preserving the rest of the synchronization behavior and
details unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3a4b4269-5aa0-4ef7-ab21-dfd0de808868

📥 Commits

Reviewing files that changed from the base of the PR and between c2f1fe8 and b2d17c6.

📒 Files selected for processing (4)
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
  • docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Apple performance · Unwired Mail
  • GitHub Check: Apple Debug · Unwired Mail
🧰 Additional context used
📓 Path-based instructions (5)
docs/adr/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Record durable architecture decisions and privacy-boundary decisions in docs/adr/.

Files:

  • docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: Treat docs as product truth, not marketing. Prefer precise domain language
from CONTEXT.md; flag undefined terms, synonym drift, and conflicts with
existing ADRs under docs/adr/. For ADR changes, require a clear decision,
consequences, and privacy or security boundary impact; flag silent
reversals of prior ADRs without an explicit supersession note. For
docs/agents/ and bootstrap docs, verify agent workflow, validation, and
triage guidance stay aligned with AGENTS.md. Flag broken cross-links,
speculative future claims presented as shipped behavior, and docs that
omit the Apple vs TypeScript ownership of a boundary they describe.

Files:

  • docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md
apps/unwired-mail/**/*.{swift,m,mm}

📄 CodeRabbit inference engine (AGENTS.md)

The Apple app must provide formatter, linter, and test commands. Run zsh scripts/check-apple-lint.zsh for format and lint, and use the documented xcodebuild test command for tests; SwiftLint runs in strict mode.

Files:

  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
apps/unwired-mail/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Apple validation must use SwiftLint strict mode and Swift Format strict linting; if Apple tooling is unavailable, report that clearly in the final handoff.

Files:

  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift

⚙️ CodeRabbit configuration file

apps/unwired-mail/**/*.swift: Prioritize functional correctness, data protection, and Apple platform
behavior over style already enforced by swift-format and SwiftLint.
Check actor isolation, cancellation, lifecycle transitions, persistence,
retry and offline behavior, and Keychain accessibility. Flag credentials,
tokens, mail content, or decrypted Product Sync data that can be logged,
persisted outside approved device-local storage, or cross a documented
privacy boundary. Require focused tests for changed behavior and failure
paths.

Files:

  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift
  • apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift
  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
**/*Tests/**/*.swift

⚙️ CodeRabbit configuration file

**/*Tests/**/*.swift: Verify externally observable behavior, privacy invariants, error paths,
cancellation or restart behavior where relevant, and deterministic
isolation. Flag fragile sleeps, ordering dependencies, and tests that
duplicate the implementation instead of exercising it.

Files:

  • apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift
🔇 Additional comments (3)
apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceSyncService.swift (1)

40-40: LGTM!

Also applies to: 61-61, 75-83, 99-99

apps/unwired-mail/unwired-mail/Services/ProductSync/InboxPreferenceStore.swift (1)

28-58: LGTM!

Also applies to: 106-108, 136-136, 185-267, 286-288, 327-327, 336-347

apps/unwired-mail/unwired-mailTests/InboxPreferenceSyncServiceTests.swift (1)

42-82: LGTM!

Also applies to: 214-256, 298-313, 316-333, 356-382

Comment thread docs/adr/0019-sync-mail-workflow-preferences-not-device-state.md Outdated
@rajzik

rajzik commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

@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: d30bc9b377

ℹ️ 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/ProductSync/InboxPreferenceStore.swift Outdated
@rajzik
rajzik merged commit 9135198 into main Aug 8, 2026
7 checks passed
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.

Sync Inbox preferences offline

1 participant