Skip to content

feat: show disconnected email inboxes in the UI - #397

Closed
Melvin0070 wants to merge 3 commits into
abhinavxd:mainfrom
Melvin0070:feat/388-inbox-disconnected-indicator
Closed

feat: show disconnected email inboxes in the UI#397
Melvin0070 wants to merge 3 commits into
abhinavxd:mainfrom
Melvin0070:feat/388-inbox-disconnected-indicator

Conversation

@Melvin0070

@Melvin0070 Melvin0070 commented Jul 4, 2026

Copy link
Copy Markdown

Closes #388.

OAuth (Google/Microsoft) and IMAP inboxes can silently stop receiving mail when a token is revoked or credentials are rejected — today the only signal is a log line. This surfaces that state so admins know to reconnect.

Approach

  • Detect — flag the inbox on IMAP auth / OAuth-refresh failure and clear it on the next successful auth, reusing the existing TokenRefreshCallback pattern (email channel → inbox manager). Only credential failures flip it; transient dial/network errors are left alone so the indicator doesn't flap. Status is tracked in memory and seeded from the DB on load, so it writes only on a real transition, not on every poll.
  • Store — a nullable inboxes.disconnected_at column (migration v2.5.0). I kept this out of the config JSONB since that's encrypted and rewritten on every update, and a column is cleaner to surface/query. Happy to move it into config JSON instead if you'd prefer no schema change.
  • Show — a warning badge + tooltip in the inboxes table, and the existing OAuth box in the inbox edit form becomes a reconnect banner when disconnected. The reconnect flow itself already existed.

Testing

go build ./..., go vet, the email package tests, eslint, and pnpm build:main all pass. I wasn't able to exercise a real token revocation end-to-end; glad to walk through any manual verification you'd like.

Notes

  • Only en-US.json is touched (source locale); other languages come through Crowdin.
  • Touches a few files also in Various IMAP improvements #357 (imap.go, models.go, EmailInboxForm.vue), so whichever lands second will need a rebase.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Inbox lists now show connection status for email inboxes.
    • Email inbox settings now display a clear disconnected state with reconnect guidance.
  • Bug Fixes

    • Inbox connection status is now updated automatically when mail sync disconnects or reconnects.
    • Database upgrades now include the latest schema changes for inbox connection tracking.

OAuth and IMAP inboxes could silently stop receiving mail when a token was
revoked or credentials were rejected, with a log line as the only signal.
Persist a per-inbox connection status and surface it so admins can reconnect.

- Add nullable inboxes.disconnected_at column (migration v2.5.0).
- Flag the inbox on IMAP auth / OAuth-refresh failure and clear it on the next
  successful auth, via a ConnectionStatusCallback mirroring the existing token
  refresh callback. Writes only on a transition, seeded from the DB on load.
- Show a warning badge in the inbox table and a reconnect banner in the inbox
  edit form when disconnected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 4, 2026 10:55
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 87712ddc-a541-406a-8512-3b0e76192813

📥 Commits

Reviewing files that changed from the base of the PR and between 70fe307 and 742d0fd.

📒 Files selected for processing (5)
  • cmd/upgrade.go
  • frontend/apps/main/src/features/admin/inbox/EmailInboxForm.vue
  • i18n/en-US.json
  • internal/migrations/v2.6.0.go
  • schema.sql
✅ Files skipped from review due to trivial changes (2)
  • internal/migrations/v2.6.0.go
  • i18n/en-US.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • schema.sql
  • frontend/apps/main/src/features/admin/inbox/EmailInboxForm.vue

📝 Walkthrough

Walkthrough

Adds a disconnected_at timestamp column to the inboxes table via migration V2_6_0, backend logic to detect and persist email inbox connection failures during IMAP authentication (MarkDisconnected/MarkConnected), and frontend UI components/i18n strings to surface disconnected inbox status.

Changes

Inbox Disconnection Detection

Layer / File(s) Summary
Schema, migration and model updates
schema.sql, internal/migrations/v2.6.0.go, cmd/upgrade.go, internal/inbox/models/models.go, internal/inbox/queries.sql
Adds nullable disconnected_at column, a V2_6_0 migration registered in the upgrade command, a matching Inbox.DisconnectedAt model field, and updated/new SQL queries to select and set/clear the column.
Email connection status tracking
internal/inbox/channel/email/email.go, internal/inbox/channel/email/imap.go
Adds ConnectionStatusCallback, cached disconnected state with mutex, and setConnectionStatus logic invoked from IMAP OAuth/password authentication success and failure paths, including transient OAuth error handling.
Manager persistence methods and init wiring
internal/inbox/inbox.go, cmd/init.go
Adds MarkDisconnected/MarkConnected Manager methods backed by new prepared statements, wired into email.New via a new callback during inbox initialization.
Frontend disconnected status UI
frontend/apps/main/src/features/admin/inbox/InboxConnectionStatus.vue, frontend/apps/main/src/features/admin/inbox/EmailInboxForm.vue, frontend/apps/main/src/views/admin/inbox/InboxList.vue, i18n/en-US.json
Adds a new status indicator component, conditional connected/disconnected OAuth UI in the inbox form, a status column in the inbox list table, and new localization strings.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IMAP as imap.go
  participant Email as Email.setConnectionStatus
  participant Manager as inbox.Manager
  participant DB as Database
  participant UI as InboxList / EmailInboxForm

  IMAP->>Email: authentication succeeds or fails
  Email->>Email: check if status transitioned
  alt error occurred
    Email->>Manager: ConnectionStatusCallback(inboxID, err)
    Manager->>DB: MarkDisconnected -> set-inbox-disconnected
  else authentication succeeds
    Email->>Manager: ConnectionStatusCallback(inboxID, nil)
    Manager->>DB: MarkConnected -> set-inbox-connected
  end
  UI->>DB: fetch inbox (disconnected_at)
  DB-->>UI: return disconnected_at value
  UI->>UI: render InboxConnectionStatus / OAuth alert state
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: surfacing disconnected inboxes in the UI.
Linked Issues check ✅ Passed The PR adds disconnected-state UI messaging and an inbox table indicator with reconnect guidance, matching #388.
Out of Scope Changes check ✅ Passed All changes support disconnected inbox detection, persistence, migration, or UI display, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI 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.

Pull request overview

Adds end-to-end support for surfacing “disconnected” email inboxes (IMAP + OAuth) so admins can see and act on inbox auth/connection loss without relying on logs.

Changes:

  • Adds inboxes.disconnected_at (nullable) and DB helpers to set/clear it.
  • Tracks/persists email inbox connection health from the email channel (IMAP auth / OAuth refresh paths) into the inbox manager.
  • Updates the admin UI to display a disconnected badge in the inbox list and show a reconnect-style banner in the email inbox form, with new i18n strings.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
schema.sql Adds disconnected_at column to inboxes.
internal/migrations/v2.5.0.go Migration to add disconnected_at to existing DBs.
internal/inbox/queries.sql Extends inbox selects + adds set/clear disconnected queries.
internal/inbox/models/models.go Exposes disconnected_at on the inbox model (DB/JSON).
internal/inbox/inbox.go Adds manager methods + prepared statements for setting/clearing disconnected state.
internal/inbox/channel/email/imap.go Hooks connection-status updates into IMAP auth/refresh flow.
internal/inbox/channel/email/email.go Adds connection-status callback plumbing + transition logic.
cmd/init.go Wires email inbox initialization to seed and persist disconnected state.
cmd/upgrade.go Registers the new v2.5.0 migration.
i18n/en-US.json Adds strings for disconnected status/banners.
frontend/apps/main/src/views/admin/inbox/InboxList.vue Adds a “Status” column using the new status component.
frontend/apps/main/src/features/admin/inbox/InboxConnectionStatus.vue New component to display disconnected badge + tooltip.
frontend/apps/main/src/features/admin/inbox/EmailInboxForm.vue Shows OAuth reconnect banner styling/messages when disconnected.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +113 to +130
e.connStatusMu.Lock()
disconnected := connErr != nil
if disconnected == e.disconnected {
e.connStatusMu.Unlock()
return
}
e.disconnected = disconnected
e.connStatusMu.Unlock()

if disconnected {
e.lo.Warn("inbox disconnected from mail server", "inbox_id", e.id, "error", connErr)
} else {
e.lo.Info("inbox reconnected to mail server", "inbox_id", e.id)
}

if err := e.connStatusCallback(e.id, connErr); err != nil {
e.lo.Error("failed to persist inbox connection status", "inbox_id", e.id, "error", err)
}
Comment on lines 93 to 99
if e.authType == imodels.AuthTypeOAuth2 && e.oauth != nil {
// Refresh OAuth token if needed
oauthConfig, _, err := e.refreshOAuthIfNeeded()
if err != nil {
e.setConnectionStatus(err)
return err
}

@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 `@internal/inbox/channel/email/email.go`:
- Around line 106-131: The cached connection state in Email.setConnectionStatus
is updated before connStatusCallback succeeds, so a failed persistence leaves
e.disconnected out of sync and blocks future retries. Move the state commit in
setConnectionStatus so e.disconnected is only updated after connStatusCallback
returns nil, while keeping the transition check and logging around
Email.setConnectionStatus, e.connStatusMu, and e.connStatusCallback intact.
🪄 Autofix (Beta)

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 793dac33-d308-4034-a607-ea8f576b564b

📥 Commits

Reviewing files that changed from the base of the PR and between 82d3ef7 and 78b1557.

📒 Files selected for processing (13)
  • cmd/init.go
  • cmd/upgrade.go
  • frontend/apps/main/src/features/admin/inbox/EmailInboxForm.vue
  • frontend/apps/main/src/features/admin/inbox/InboxConnectionStatus.vue
  • frontend/apps/main/src/views/admin/inbox/InboxList.vue
  • i18n/en-US.json
  • internal/inbox/channel/email/email.go
  • internal/inbox/channel/email/imap.go
  • internal/inbox/inbox.go
  • internal/inbox/models/models.go
  • internal/inbox/queries.sql
  • internal/migrations/v2.5.0.go
  • schema.sql

Comment thread internal/inbox/channel/email/email.go
Address review feedback on abhinavxd#397:
- Cache the disconnected flag only after the DB write succeeds, so a failed
  persist is retried on the next poll instead of being swallowed by the
  transition check.
- Don't flag a disconnect on transient transport errors while refreshing the
  OAuth token; only credential rejections from the provider count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Melvin0070

Copy link
Copy Markdown
Author

Thanks for the reviews — both findings addressed in 70fe307:

  • setConnectionStatus persistence ordering — the cached disconnected flag is now committed only after the DB write succeeds, so a failed persist is retried on the next poll instead of being swallowed by the transition check.
  • Transient OAuth errors — a refresh failure now flags a disconnect only when the provider actively rejects our credentials; transport-level failures (network/DNS/timeout, surfaced as *url.Error) are left alone, matching the intended "no flap on transient errors" behavior.

Upstream claimed the v2.5.0 migration slot (per-type drafts), so the
disconnected_at migration moves to v2.6.0. No other conflicts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@abhinavxd

Copy link
Copy Markdown
Owner

Hey, thanks for the PR.

I am working whatsapp channel side by side, I ended up fixing this issue there.
Where instead of storing disconnected_at in database I've simply save it in a map here

@abhinavxd abhinavxd closed this Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Show UI messages when a inbox is disconnected and recommend reconnecting the inbox.

3 participants