Skip to content

Add durable OAuth token refresh for EWS connections - #281

Merged
rajzik merged 6 commits into
mainfrom
codex/issue-203-durable-ews-oauth
Aug 5, 2026
Merged

rajzik merged 6 commits into
mainfrom
codex/issue-203-durable-ews-oauth

Conversation

@gipity-bot

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

Copy link
Copy Markdown
Contributor

Summary

  • add system-browser EWS OAuth authorization with authorization code and PKCE
  • persist refresh credentials device-locally and refresh access tokens before provider use
  • serialize concurrent refreshes, persist refresh-token rotation, and require reauthorization after invalid_grant
  • document and configure the provider authorization and token endpoints

Why

EWS OAuth connections previously stored only an access token. Once that token expired, the account could no longer authenticate without manually replacing the credential. This change gives supported on-premises EWS connections a durable, device-bound refresh lifecycle while failing closed for legacy token-only records.

Validation

  • mise exec -- zsh scripts/check-apple-lint.zsh — passed with 0 violations
  • focused EWS OAuth XCTest suite on iPhone 17 (D6D43A14-3B1A-41B6-B25A-0DFCA1BFB089) — 8 tests passed
  • required full Apple XCTest command — several hundred tests passed, then the run hung in unchanged ProductAccountSessionTests.testProductAccountDeletionBlocksMailActionsWhileBackendDeletionRuns; an isolated rerun reproduced the same async XCTest hang

Published at the user's explicit direction despite the full-suite hang above.

Closes #203


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 4, 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: 21 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: a4761877-e2ff-44fb-8b31-915bf26b722a

📥 Commits

Reviewing files that changed from the base of the PR and between d64c99d and 9896e2c.

📒 Files selected for processing (1)
  • apps/unwired-mail/unwired-mailTests/PendingProviderActionServiceTests.swift
📝 Walkthrough

Summary

  • Added system-browser EWS OAuth authorization code flow with PKCE for supported on-premises EWS connections in the Apple client.
  • Added device-local OAuth credential handling for EWS, including token exchange, token refresh, and rotated refresh-token persistence.
  • Added expiration-based token refresh before authorized EWS requests and serialized concurrent refreshes.
  • Required reauthorization after invalid_grant and cleared rejected OAuth authorizations.
  • Kept OAuth tokens out of synchronized EWS definitions.
  • Updated the EWS setup UI to label OAuth as “OAuth (system browser)”, remove the credential entry field for OAuth, and rename the OAuth save action to “Sign In and Save on This Device”.
  • Documented configurable provider authorization and token endpoints and added environment/configuration support for EWS OAuth settings in README and app configuration.
  • Updated mailbox request retry behavior so EWSOAuthError.tokenExchangeFailed with HTTP 408, 409, 425, 429, or 5xx is treated as transient, and a transient 503 preserves the archived provider action for retry.
  • Added a changeset documenting browser-based on-device authorization and device-local OAuth refresh.

Affected areas

  • Apple: apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.swift, apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift, apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift, apps/unwired-mail/unwired-mail/Services/Mailbox/PendingProviderActionService.swift, plus Info.plist, project.pbxproj, and DotEnvFile.
  • TypeScript: No TypeScript code changed.

Privacy and security

  • Stored OAuth tokens, refresh credentials, and token expiry on the device only using device-local Keychain-backed storage.
  • Bound OAuth usage to device-local stored credentials and failed closed for incomplete legacy records.
  • Validated provider/redirect inputs, including HTTPS endpoint validation, callback scheme validation, redirect/state validation, and PKCE-related authorization-code exchange.
  • Persisted rotated refresh tokens safely and serialized refresh work to prevent concurrent credential rotation conflicts.
  • Represented rejected grants as a reauthorization state and cleared rejected credentials so the client does not reuse invalid refresh state.
  • Ensured device-local OAuth credentials do not enter synchronized EWS configuration.

Configuration and migrations

  • Added EWS OAuth configuration keys to apps/unwired-mail/unwired-mail/Info.plist: EWSOAuthAuthorizationEndpoint, EWSOAuthCallbackScheme, EWSOAuthClientId, EWSOAuthScope, and EWSOAuthTokenEndpoint.
  • Added $(EWS_OAUTH_CALLBACK_SCHEME) to CFBundleURLTypes.
  • Added EWS OAuth environment settings to README and added the same keys to the Apple client key allowlist in DotEnvFile.swift.
  • Added Xcode build setting entries for Debug/Release so the app can supply the authorization/token endpoints and callback scheme.

Validation

  • Passed Apple lint.
  • Passed focused EWS OAuth tests covering authorization URL/state and callback validation, token refresh/rotation, expiry-based refresh, rejection handling for invalid_grant, credential cleanup, redirect protection, refresh deduplication, and persistence behavior.
  • Observed that a full test run hung in an unchanged account deletion test. An isolated rerun reproduced the hang.
  • Added unit tests confirming transient classification for EWSOAuthError.tokenExchangeFailed (for example HTTP 429) and that transient 503 leaves the archived action persisted for retry.

Walkthrough

This change adds EWS OAuth setup, browser-based authorization, device-local token storage, refresh coordination, retry classification, tests, and documentation.

Changes

EWS OAuth support

Layer / File(s) Summary
OAuth configuration and setup UX
apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj, apps/unwired-mail/unwired-mail/Info.plist, apps/unwired-mail/unwired-mail/Services/DotEnvFile.swift, apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.swift, README.md, .changeset/refresh-ews-oauth.md
Adds EWS OAuth build settings, callback registration, environment keys, setup guidance, browser-based OAuth labels, and device-local sign-in text.
OAuth authorization and persistence
apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift
Implements validated OAuth configuration, PKCE authorization, callback validation, token exchange, token persistence, and concurrency-safe authorization storage.
Provider access refresh integration
apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift, apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift, apps/unwired-mail/unwired-mail/Services/Mailbox/PendingProviderActionService.swift
Adds refresh coordination, concurrent refresh deduplication, rotated-token persistence, rejected-grant cleanup, and transient failure handling for OAuth token exchange failures.
OAuth validation and documented contract
apps/unwired-mail/unwired-mailTests/DotEnvFileTests.swift, apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift, apps/unwired-mail/unwired-mailTests/OutboxDeliveryServiceTests.swift, apps/unwired-mail/unwired-mailTests/PendingProviderActionServiceTests.swift, docs/adr/0031-refresh-ews-oauth-on-device.md
Tests authorization, persistence, expiry, rotation, rejection, concurrency, redirects, retry classification, and request credentials. Documents the OAuth storage and error-handling contract.

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

Sequence Diagram(s)

sequenceDiagram
  participant EWSSetupService
  participant EWSOAuthService
  participant AuthenticationServices
  participant EWSAuthorizationPersisting
  participant EWSMailboxConnectionAdapter
  participant EWSOAuthRefreshCoordinator
  participant EWSProvider

  EWSSetupService->>EWSOAuthService: Start PKCE authorization
  EWSOAuthService->>AuthenticationServices: Present system browser
  AuthenticationServices-->>EWSOAuthService: Return callback and state
  EWSOAuthService-->>EWSSetupService: Return OAuth tokens
  EWSSetupService->>EWSAuthorizationPersisting: Save device-local authorization
  EWSMailboxConnectionAdapter->>EWSOAuthRefreshCoordinator: Request provider authorization
  EWSOAuthRefreshCoordinator->>EWSOAuthService: Refresh expiring token
  EWSOAuthService-->>EWSOAuthRefreshCoordinator: Return refreshed tokens
  EWSOAuthRefreshCoordinator->>EWSAuthorizationPersisting: Persist rotated tokens
  EWSMailboxConnectionAdapter->>EWSProvider: Make authorized request
Loading

Possibly related PRs

Suggested reviewers: rajzik

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 describes the primary change: durable OAuth token refresh for EWS connections.
Description check ✅ Passed The description accurately covers EWS OAuth authorization, durable refresh, configuration, testing, and the known full-suite hang.
Linked Issues check ✅ Passed The changes implement the requirements in issue #203, including device-local OAuth, refresh, rotation, reauthorization, tests, and documentation.
Out of Scope Changes check ✅ Passed The changes remain within the EWS OAuth objectives, including related transient-failure handling, configuration, documentation, and tests.
✨ Finishing Touches 💡 1
📝 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-203-durable-ews-oauth

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

@gipity-bot

gipity-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@rajzik
rajzik marked this pull request as ready for review August 4, 2026 21:03
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 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: 15

🤖 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 @.changeset/refresh-ews-oauth.md:
- Around line 1-4: Update the changeset frontmatter in refresh-ews-oauth.md by
regenerating it with pnpm changeset, selecting the affected package and
appropriate release type; preserve the existing OAuth summary in the body.

In `@apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.swift`:
- Around line 47-55: Update EWSSetupViewModel initialization so injected
authorizationStore and definitionSyncService are also used by connect() through
service. Either make service required or construct the default EWSSetupService
from those injected dependencies, ensuring reads and writes share one dependency
graph.

In `@apps/unwired-mail/unwired-mail/Services/DotEnvFile.swift`:
- Around line 27-31: Update DotEnvFile and the
EWSMailboxConnectionAdapter.value(...) resolution path so
EWS_OAUTH_CALLBACK_SCHEME is obtained only from the bundle/build setting, or
reject runtime values that differ from the registered URL scheme before
EWSOAuthRequest constructs redirect_uri. Keep other environment-backed settings
unchanged, and update README.md at the referenced configuration sections to
document that EWS_OAUTH_CALLBACK_SCHEME is build-time only.

In
`@apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift`:
- Around line 966-967: Synchronize InMemoryEWSAuthorizationStore’s values access
before relying on `@unchecked` Sendable: add a lock and wrap every read and
mutation in clearAll, connectionIds, load, and save with lock.withLock. Preserve
the existing persistence behavior and return values while ensuring all
dictionary access is serialized.
- Around line 1076-1091: Update refreshedTokens so only the caller that creates
the refreshTask schedules removal of refreshTasks[refreshKey], with the removal
performed by the task owner and guarded by the existing actor ownership check.
Remove the caller-wide defer so joiners and cancelled awaiters cannot clear an
unstructured task that is still running. Extend
testEWSConcurrentRefreshCallersShareOneTokenExchange with a third caller
arriving after a joiner resumes, verifying all callers share one token exchange.
- Around line 247-292: Update authenticate and its cancellation flow to prevent
concurrent authorizations from sharing or overwriting authenticationContinuation
and webAuthenticationSession. Reject or serialize a new authenticate call while
one is active, and ensure cancellation and completion only clear, cancel, and
resume state belonging to the active authorization rather than a newer session.
- Around line 213-245: The exchange method must prevent token request POST data
from being forwarded to another origin. Configure the URLSession used by
exchange with redirect handling that rejects redirects or permits only
same-origin targets, while preserving valid token exchanges; add a regression
test covering a cross-origin 307/308 redirect.

In `@apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift`:
- Around line 545-582: Add a sibling test alongside the existing refresh case
covering a token whose expiresAtMilliseconds is well beyond now plus the
five-minute leeway. Reuse the EWSMailboxConnectionAdapter setup, assert
oauthService.refreshCount is 0, and verify
client.loadedFolderAuthorizationCredentials contains the stored access token
rather than refreshed credentials.
- Around line 116-146: Add sibling tests near
testEWSOAuthRefreshMapsRejectedGrantToReauthorization covering an HTTP 503
transient refresh failure and asserting the thrown EWSOAuthError is not
.authorizationRejected. Add a coordinator-level failure-path test for
EWSOAuthRefreshCoordinator.refreshIfNeeded that simulates the same transient
failure and verifies the authorization record remains loadable, preserving the
refresh credential.
- Around line 6150-6167: Consolidate the duplicated request-body stream-reading
logic by making ewsRequestBodyText(_:) the shared extractor, then have both
ewsRequestBody(_:) and requestBody(_:) reuse it while preserving their
respective return types and fallback error behavior. Remove the standalone
stream loop from ewsRequestBody(_:) and keep the existing
httpBody/httpBodyStream handling semantics.
- Around line 51-63: Add tests around authorizationCode(from:) covering
rejection of callbacks with a mismatched redirect scheme or host, asserting the
appropriate callback validation error, and callbacks with an empty or missing
code, asserting EWSOAuthError.invalidAuthorizationCallback. Keep the existing
valid-code and invalid-state assertions unchanged.
- Around line 109-113: Extend the refresh-token request assertions in
EWSMailboxConnectionAdapterTests to verify that the captured body does not
contain any client-secret field or value. Keep the existing positive assertions
unchanged and use the existing body variable for the negative assertion.
- Around line 685-695: Update the shared-refresh test around
coordinator.refreshIfNeeded so it waits for an explicit signal that the second
caller has joined the in-flight refresh, rather than relying on Task.yield().
Only release refreshGate after that signal, while preserving the existing result
and refreshCount assertions.
- Around line 518-527: Extend
testEWSOAuthSetupUsesBrowserTokensAndPersistsRefreshCredential by capturing the
RecordingEWSDefinitionSyncService, encoding definitions.savedDefinition, and
asserting the encoded definition excludes both the access token and refresh
token. Keep the existing authorization and device-local persistence assertions
unchanged.

In `@docs/adr/0031-refresh-ews-oauth-on-device.md`:
- Around line 1-11: Update the ADR title/body terminology to use “On-Premises
Exchange Connection” instead of “supported on-premises EWS deployments.” Add a
concise consequences paragraph covering operator responsibility for supplying
and rotating provider endpoints and the client identifier, the user-visible
reauthorization prompt after invalid_grant, and loss of mailbox access when the
device’s Keychain authorization record is removed.
🪄 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: 1efa8f0e-f7e0-48bd-9a9d-61ee39b38c5d

📥 Commits

Reviewing files that changed from the base of the PR and between 6215253 and a1b51b7.

📒 Files selected for processing (10)
  • .changeset/refresh-ews-oauth.md
  • README.md
  • apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj
  • apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.swift
  • apps/unwired-mail/unwired-mail/Info.plist
  • apps/unwired-mail/unwired-mail/Services/DotEnvFile.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mailTests/DotEnvFileTests.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
  • docs/adr/0031-refresh-ews-oauth-on-device.md
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Apple · Unwired Mail
🧰 Additional context used
📓 Path-based instructions (7)
.changeset/**

📄 CodeRabbit inference engine (AGENTS.md)

Add a changeset with pnpm changeset for runtime behavior changes or exported type/API changes

Files:

  • .changeset/refresh-ews-oauth.md
apps/unwired-mail/**

📄 CodeRabbit inference engine (AGENTS.md)

Run xcodebuild test -project apps/unwired-mail/unwired-mail.xcodeproj -scheme unwired-mail -destination 'platform=iOS Simulator,name=iPhone 17' for Apple app test coverage

Files:

  • apps/unwired-mail/unwired-mail.xcodeproj/project.pbxproj
  • apps/unwired-mail/unwired-mail/Services/DotEnvFile.swift
  • apps/unwired-mail/unwired-mail/Info.plist
  • apps/unwired-mail/unwired-mailTests/DotEnvFileTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.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)

apps/unwired-mail/**/*.swift: Use Swift formatting with swift-format via xcrun and validate with zsh scripts/check-apple-lint.zsh
Use SwiftLint in strict mode for linting Apple app code

Files:

  • apps/unwired-mail/unwired-mail/Services/DotEnvFile.swift
  • apps/unwired-mail/unwired-mailTests/DotEnvFileTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.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/DotEnvFile.swift
  • apps/unwired-mail/unwired-mailTests/DotEnvFileTests.swift
  • apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.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/DotEnvFileTests.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
README.md

📄 CodeRabbit inference engine (AGENTS.md)

Update README.md with developer setup, common commands, and high-level project navigation

Files:

  • README.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:

  • README.md
  • docs/adr/0031-refresh-ews-oauth-on-device.md
docs/adr/**

📄 CodeRabbit inference engine (AGENTS.md)

Document durable architecture decisions and privacy-boundary decisions in docs/adr/

Files:

  • docs/adr/0031-refresh-ews-oauth-on-device.md
🪛 markdownlint-cli2 (0.23.2)
.changeset/refresh-ews-oauth.md

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

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

🔇 Additional comments (11)
apps/unwired-mail/unwired-mailTests/DotEnvFileTests.swift (1)

61-65: LGTM!

Also applies to: 79-83

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

592-639: LGTM!


5724-5727: LGTM!

Also applies to: 5761-5766, 5921-5948

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

719-723: 🗄️ Data Integrity & Integration

Verify that empty target settings do not mask local overrides.

The README says physical-device and Release builds can provide these values through LocalSigning.xcconfig, but both application target configurations assign the five keys directly. If target-level settings override Shared.xcconfig, the built Info.plist will contain empty OAuth endpoints, client ID, and scope, so OAuth will be unavailable outside dotenv-based Debug builds.

Inspect the resolved EWS_OAUTH_* values with xcodebuild -showBuildSettings for both Debug and Release after loading the intended local configuration. Move the defaults to the xcconfig or use inheritance if the local values are not preserved.

Also applies to: 758-762

apps/unwired-mail/unwired-mail/Info.plist (1)

21-27: LGTM!

Also applies to: 28-37

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

363-376: LGTM!

Also applies to: 389-391

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

38-142: LGTM!


321-436: LGTM!


1194-1201: LGTM!

Also applies to: 1244-1245, 1265-1266, 2149-2149, 2190-2194, 2283-2283


580-593: 🔒 Security & Privacy

Reachability path
● Entry
  apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.swift:105
  connect
│
▼
● Sink
  apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift

No change is required for Keychain accessibility.

KeychainEWSAuthorizationStore writes authorizations with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly. It does not set kSecAttrSynchronizable.


3742-3752: 🔒 Security & Privacy

Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information

Reachability: External

Reachability path
● Entry
  apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.swift:105
  connect
│
▼
● Sink
  apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift

Confirm that the EWS endpoint carrying the Bearer token is HTTPS-only.

This change routes every provider access through the coordinator, so an OAuth access token now travels as Authorization: Bearer to definition.endpoint in SystemEWSClient.request (SystemEWSClient.swift Lines 783-855). The endpoint originates from user-entered setup input and passes through EWSConnectionDefinition.validatedEndpoint, which is not in the provided context. EWSOAuthConfiguration enforces HTTPS for the provider endpoints, but that guard does not cover the EWS endpoint. Confirm that validatedEndpoint rejects http, so the access token never crosses a cleartext hop.

#!/bin/bash
# Description: Verify scheme enforcement for the EWS endpoint that receives the Bearer token.
set -euo pipefail

ast-grep run --pattern 'static func validatedEndpoint($$$) { $$$ }' --lang swift apps/unwired-mail
rg -n -C12 'func validatedEndpoint' apps/unwired-mail

Comment thread .changeset/refresh-ews-oauth.md
Comment thread apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.swift Outdated
Comment thread apps/unwired-mail/unwired-mail/Services/DotEnvFile.swift
Comment thread docs/adr/0031-refresh-ews-oauth-on-device.md
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 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.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

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: 2

Caution

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

⚠️ Outside diff range comments (2)
README.md (1)

40-40: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Exclude EWS_OAUTH_CALLBACK_SCHEME from the .env.local list.

Line 40 states that debug builds read "the EWS_OAUTH_* settings" from .env.local. EWSOAuthConfiguration.bundledValue() reads EWS_OAUTH_AUTHORIZATION_ENDPOINT, EWS_OAUTH_CLIENT_ID, EWS_OAUTH_SCOPE, and EWS_OAUTH_TOKEN_ENDPOINT from the environment or .env.local, but it reads the callback scheme only from Bundle.main Info dictionary key EWSOAuthCallbackScheme. A developer who follows line 40 and puts the callback scheme in .env.local gets a nil configuration with no error. Line 50 already states the build-time-only rule; make line 40 agree.

📝 Proposed wording fix
-Debug builds running on the development Mac or in Simulator read `CONVEX_URL`, `CONVEX_SITE_URL`, the `EWS_OAUTH_*` settings, `GMAIL_OAUTH_CLIENT_ID`, `GMAIL_PUBSUB_TOPIC`, and `MICROSOFT_GRAPH_CLIENT_ID` from the repository-root `.env.local`; backend-only values are ignored.
+Debug builds running on the development Mac or in Simulator read `CONVEX_URL`, `CONVEX_SITE_URL`, the `EWS_OAUTH_*` settings except `EWS_OAUTH_CALLBACK_SCHEME`, `GMAIL_OAUTH_CLIENT_ID`, `GMAIL_PUBSUB_TOPIC`, and `MICROSOFT_GRAPH_CLIENT_ID` from the repository-root `.env.local`; backend-only values are ignored.
🤖 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 `@README.md` at line 40, Update the README sentence describing debug-build
settings to list only the EWS OAuth values read from `.env.local`, excluding
`EWS_OAUTH_CALLBACK_SCHEME`; preserve the existing guidance that this callback
scheme is configured through the app’s bundled build-time setting as documented
elsewhere.
apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift (1)

1149-1176: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Persist rotated tokens from the owning task, not only from the awaiting caller.

Slot ownership is now correct: only the creating task removes refreshTasks[refreshKey], and joiners no longer clear an in-flight entry. One rotation gap remains.

refreshedTokens returns through try await refreshTask.value. If the awaiting caller is cancelled at that suspension point, the unstructured refreshTask keeps running and completes the token exchange. Persistence happens only in refreshIfNeeded after the await, so the caller never reaches authorizationStore.save. The provider has already consumed and rotated the refresh token, while the Keychain still holds the previous one.

The next provider access then presents the consumed refresh token. A rotating provider answers invalid_grant, refreshIfNeeded clears the authorization, and the connection becomes authorization-required even though no credential was actually revoked. Provider access is routinely cancelled in this codebase, so this path is reachable: EWSSetupPanel calls task?.cancel() on disappearance, invalidate() fences in-flight work, and the sync gate throws CancellationError.

Persist the refreshed tokens from the task that owns the exchange, so the rotation survives caller cancellation.

As per path instructions "Check actor isolation, cancellation, lifecycle transitions, persistence, retry and offline behavior, and Keychain accessibility."

🐛 Proposed fix to tie persistence to the exchange

Move the store write into the actor-isolated completion so it runs once per exchange, independent of which caller is still awaiting.

   private func refreshedTokens(
     _ tokens: EWSOAuthTokens,
-    refreshKey: String
+    refreshKey: String,
+    productAccountId: String,
+    template: DeviceLocalEWSAuthorization
   ) async throws -> EWSOAuthTokens {
     let refreshTask: Task<EWSOAuthTokens, Error>
     if let inFlight = refreshTasks[refreshKey] {
       await onRefreshTaskJoined?()
       refreshTask = inFlight
     } else {
       let owner = UUID()
       refreshTask = Task { `@MainActor` [oauthService] in
         try await oauthService.refresh(tokens)
       }
       refreshTasks[refreshKey] = refreshTask
       refreshTaskOwners[refreshKey] = owner
       Task { [weak self] in
-        _ = await refreshTask.result
-        await self?.removeRefreshTask(refreshKey: refreshKey, owner: owner)
+        let result = await refreshTask.result
+        await self?.completeRefreshTask(
+          refreshKey: refreshKey,
+          owner: owner,
+          result: result,
+          productAccountId: productAccountId,
+          template: template
+        )
       }
     }
     return try await refreshTask.value
   }
 
-  private func removeRefreshTask(refreshKey: String, owner: UUID) {
+  private func completeRefreshTask(
+    refreshKey: String,
+    owner: UUID,
+    result: Result<EWSOAuthTokens, Error>,
+    productAccountId: String,
+    template: DeviceLocalEWSAuthorization
+  ) {
     guard refreshTaskOwners[refreshKey] == owner else { return }
     refreshTasks[refreshKey] = nil
     refreshTaskOwners[refreshKey] = nil
+    guard case .success(let refreshedTokens) = result else { return }
+    try? authorizationStore.save(
+      DeviceLocalEWSAuthorization(
+        authorizationGeneration: template.authorizationGeneration,
+        credential: refreshedTokens.accessToken,
+        definition: template.definition,
+        hasOnlineArchive: template.hasOnlineArchive,
+        oauthTokens: refreshedTokens
+      ),
+      productAccountId: productAccountId
+    )
   }

refreshIfNeeded then passes current as the template, and its own authorizationStore.save becomes an idempotent rewrite of the same record. Add a test that cancels the sole caller during the exchange and asserts the stored oauthTokens equal the rotated tokens.

🤖 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/EWSMailboxConnectionAdapter.swift`
around lines 1149 - 1176, Update refreshedTokens so the owning refreshTask
persists the rotated EWSOAuthTokens after a successful oauthService.refresh,
independent of caller cancellation; use the existing authorizationStore and
preserve actor isolation and single-write-per-exchange behavior. Keep
refreshIfNeeded’s save as the existing idempotent rewrite using current as its
template, and add coverage that cancels the sole caller during refresh and
verifies the stored oauthTokens are the rotated values.

Source: Path instructions

🤖 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/EWSMailboxConnectionAdapterTests.swift`:
- Around line 865-878: Synchronize the third caller in the coordinator refresh
test before releasing refreshGate. Update onRefreshTaskJoined to count joined
callers using the existing TestCounter pattern, wait until both the second and
third callers have joined the in-flight task, then release refreshGate; preserve
the assertions verifying all callers share freshTokens and
oauthService.refreshCount remains 1.
- Around line 533-556: Extend
testEWSOAuthTokenExchangeRejectsCrossOrigin307And308Redirects with positive
assertions that same-origin 307 and 308 redirects return the redirected request,
and add a non-307/308 status case verifying the policy’s expected behavior. Keep
the existing cross-origin rejection coverage and ensure the assertions exercise
EWSOAuthTokenRedirectPolicy.redirectedRequest directly.

---

Outside diff comments:
In
`@apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift`:
- Around line 1149-1176: Update refreshedTokens so the owning refreshTask
persists the rotated EWSOAuthTokens after a successful oauthService.refresh,
independent of caller cancellation; use the existing authorizationStore and
preserve actor isolation and single-write-per-exchange behavior. Keep
refreshIfNeeded’s save as the existing idempotent rewrite using current as its
template, and add coverage that cancels the sole caller during refresh and
verifies the stored oauthTokens are the rotated values.

In `@README.md`:
- Line 40: Update the README sentence describing debug-build settings to list
only the EWS OAuth values read from `.env.local`, excluding
`EWS_OAUTH_CALLBACK_SCHEME`; preserve the existing guidance that this callback
scheme is configured through the app’s bundled build-time setting as documented
elsewhere.
🪄 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: d6da0e74-b6f5-4264-8f83-5e4a5807c5d5

📥 Commits

Reviewing files that changed from the base of the PR and between a1b51b7 and e07326a.

📒 Files selected for processing (7)
  • README.md
  • apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.swift
  • apps/unwired-mail/unwired-mail/Services/DotEnvFile.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift
  • apps/unwired-mail/unwired-mailTests/DotEnvFileTests.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
  • docs/adr/0031-refresh-ews-oauth-on-device.md
💤 Files with no reviewable changes (2)
  • apps/unwired-mail/unwired-mail/Services/DotEnvFile.swift
  • apps/unwired-mail/unwired-mailTests/DotEnvFileTests.swift
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Apple · Unwired Mail
🧰 Additional context used
📓 Path-based instructions (6)
docs/adr/**

📄 CodeRabbit inference engine (AGENTS.md)

Document durable architecture decisions and privacy-boundary decisions in docs/adr/

Files:

  • docs/adr/0031-refresh-ews-oauth-on-device.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/0031-refresh-ews-oauth-on-device.md
  • README.md
README.md

📄 CodeRabbit inference engine (AGENTS.md)

Update README.md with developer setup, common commands, and high-level project navigation

Files:

  • README.md
apps/unwired-mail/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

apps/unwired-mail/**/*.swift: Use Swift formatting with swift-format via xcrun and validate with zsh scripts/check-apple-lint.zsh
Use SwiftLint in strict mode for linting Apple app code

Files:

  • apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.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/Features/Account/EWSSetupPanel.swift
  • apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/EWSMailboxConnectionAdapter.swift
apps/unwired-mail/**

📄 CodeRabbit inference engine (AGENTS.md)

Run xcodebuild test -project apps/unwired-mail/unwired-mail.xcodeproj -scheme unwired-mail -destination 'platform=iOS Simulator,name=iPhone 17' for Apple app test coverage

Files:

  • apps/unwired-mail/unwired-mail/Features/Account/EWSSetupPanel.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/EWSMailboxConnectionAdapterTests.swift
🪛 LanguageTool
docs/adr/0031-refresh-ews-oauth-on-device.md

[style] ~15-~15: Consider using a different verb to strengthen your wording.
Context: ... device's Keychain authorization record removes mailbox access on that device while lea...

(REMOVE_REVOKE)

🔇 Additional comments (13)
README.md (1)

50-50: LGTM!

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

55-60: LGTM!

Also applies to: 368-381, 394-396

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

117-119: LGTM!


213-248: LGTM!

Also applies to: 337-373


250-334: LGTM!


421-435: LGTM!


1030-1077: LGTM!


1102-1147: LGTM!


1329-1330: LGTM!

Also applies to: 1350-1351, 1371-1372, 1384-1385, 1457-1458, 2275-2279, 2368-2368


644-644: LGTM!

Also applies to: 2247-2248


826-826: 🩺 Stability & Availability

No additional synchronization is required for the remaining conformers.

KeychainEWSAuthorizationStore and FailingConnectionIdEWSAuthorizationStore contain only immutable state. InMemoryEWSAuthorizationStore protects its mutable dictionary with NSLock.

			> Likely an incorrect or invalid review comment.
apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift (1)

64-74: LGTM!

Also applies to: 125-126, 161-192, 600-603, 668-714, 765-813, 1583-1592, 5710-5710, 5724-5734, 6325-6353

docs/adr/0031-refresh-ews-oauth-on-device.md (1)

5-15: LGTM!

Comment thread apps/unwired-mail/unwired-mailTests/EWSMailboxConnectionAdapterTests.swift Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

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 5, 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: 3a69b0837e

ℹ️ 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".

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 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: 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/PendingProviderActionServiceTests.swift`:
- Around line 62-83: Update testTransientEWSOAuthFailureRemainsPendingForRetry
to exercise the EWS path by using ewsConnection and assigning its id to the
message connectionId. Replace the count-only assertion with a load from
InMemoryPendingProviderActionStore that verifies the persisted action state is
.retrying. Keep the existing transient EWSOAuthError.tokenExchangeFailed failure
injection in PendingProviderActionService.perform so the test specifically
proves retry state retention rather than any generic stored action.
🪄 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: 198e6a89-dc64-421c-9b52-a54f890ce88b

📥 Commits

Reviewing files that changed from the base of the PR and between 3a69b08 and d64c99d.

📒 Files selected for processing (4)
  • apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/PendingProviderActionService.swift
  • apps/unwired-mail/unwired-mailTests/OutboxDeliveryServiceTests.swift
  • apps/unwired-mail/unwired-mailTests/PendingProviderActionServiceTests.swift
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Apple · Unwired Mail
🧰 Additional context used
📓 Path-based instructions (3)
apps/unwired-mail/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

apps/unwired-mail/**/*.swift: Use Swift formatting with swift-format via xcrun and validate with zsh scripts/check-apple-lint.zsh
Use SwiftLint in strict mode for linting Apple app code

Files:

  • apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift
  • apps/unwired-mail/unwired-mailTests/OutboxDeliveryServiceTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/PendingProviderActionService.swift
  • apps/unwired-mail/unwired-mailTests/PendingProviderActionServiceTests.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/OutboxDeliveryService.swift
  • apps/unwired-mail/unwired-mailTests/OutboxDeliveryServiceTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/PendingProviderActionService.swift
  • apps/unwired-mail/unwired-mailTests/PendingProviderActionServiceTests.swift
apps/unwired-mail/**

📄 CodeRabbit inference engine (AGENTS.md)

Run xcodebuild test -project apps/unwired-mail/unwired-mail.xcodeproj -scheme unwired-mail -destination 'platform=iOS Simulator,name=iPhone 17' for Apple app test coverage

Files:

  • apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift
  • apps/unwired-mail/unwired-mailTests/OutboxDeliveryServiceTests.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/PendingProviderActionService.swift
  • apps/unwired-mail/unwired-mailTests/PendingProviderActionServiceTests.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/OutboxDeliveryServiceTests.swift
  • apps/unwired-mail/unwired-mailTests/PendingProviderActionServiceTests.swift
🧠 Learnings (1)
📚 Learning: 2026-07-27T07:25:56.352Z
Learnt from: rajzik
Repo: unwired-dev/product PR: 201
File: apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift:0-0
Timestamp: 2026-07-27T07:25:56.352Z
Learning: In the EWS mail provider, keep the failure-disposition/classification logic intentionally different between `OutboxDeliveryService` and `PendingProviderActionService`. 
- `OutboxDeliveryService` should treat uncertain sends as “ambiguous” (classify HTTP 5xx, HTTP 408/409/425, `ErrorTimeoutExpired`, and invalid/failed EWS responses as ambiguous) and reconcile before allowing another send to prevent duplicates.
- `PendingProviderActionService` should treat its retry-safe EWS response cases as transient so they trigger provider actions as retries.
During review, do not refactor these into a shared helper solely because the response-code checks overlap—only extract if you can prove the semantics (ambiguous-before-retry vs retry-safe transient) remain unchanged for each service.

Applied to files:

  • apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift
  • apps/unwired-mail/unwired-mail/Services/Mailbox/PendingProviderActionService.swift
🔇 Additional comments (2)
apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift (1)

275-280: 🎯 Functional Correctness

Verify the shared invalid_grant contract without merging these classifiers.

If invalid_grant reaches either classifier as EWSOAuthError.tokenExchangeFailed(status: 400) or status 401, both services classify it as permanent instead of requiring reauthorization. Confirm that EWSMailboxConnectionAdapter performs the error-code mapping before these services receive the error.

  • apps/unwired-mail/unwired-mail/Services/Mailbox/OutboxDeliveryService.swift#L275-L280: preserve outbox ambiguity and retry semantics while mapping rejected grants to user action.
  • apps/unwired-mail/unwired-mail/Services/Mailbox/PendingProviderActionService.swift#L199-L204: preserve pending-action retry semantics while mapping rejected grants to user action.
apps/unwired-mail/unwired-mailTests/OutboxDeliveryServiceTests.swift (1)

47-55: 🎯 Functional Correctness | ⚡ Quick win

Cover the complete EWS OAuth retry boundary.

This test covers only status 429. The changed branch also handles 408, 409, 425, and 5xx. Add table-driven cases for those statuses and a non-retryable status such as 400.

[ suggest_recommended_refactor]

Proposed test shape
-  func testTransientEWSOAuthFailureIsRetryable() {
-    guard
-      case .transient = outboxFailureDisposition(
-        for: EWSOAuthError.tokenExchangeFailed(status: 429)
-      )
-    else {
-      return XCTFail("Expected transient EWS OAuth failure")
+  func testTransientEWSOAuthFailureIsRetryable() {
+    for status in [408, 409, 425, 429, 500, 503] {
+      guard case .transient = outboxFailureDisposition(
+        for: EWSOAuthError.tokenExchangeFailed(status: status)
+      ) else {
+        return XCTFail("Expected transient status \(status)")
+      }
     }
   }

Source: Path instructions

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 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 merged commit f39d856 into main Aug 5, 2026
6 of 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.

Add durable OAuth token refresh for EWS connections

1 participant