msg.Collect()) during email synchronization. Implemented true io.Reader streaming that writes raw IMAP streams directly to a local .eml temporary file. This ensures O(1) memory consumption per folder regardless of email and attachment sizes.MinConns=5 and bounding MaxConns limits (defaulting to 100) to prevent connection drop timeouts under heavy load.SyncWorker. Batched multiple message UIDs into a single IMAP FETCH command to reduce latency, and implemented a bounded errgroup (max 4 concurrent CPU parsers per worker) to accelerate MIME parsing and prevent long CPU blocking loops.syncPool (5 connections) using poolctx.WithSync(ctx), guaranteeing that heavy sync jobs never exhaust the primary API connection pool (20 connections).panic: pattern "/api/health" conflicts on startup by removing duplicate route registrations in the ServeMux.ResetAccountSync to properly purge orphaned physical files (.eml and attachments) from the disk rather than just clearing the database, preventing indefinite storage bloat.PRAGMA busy_timeout=30000; on SQLite connections to mitigate database is locked (SQLITE_BUSY) errors during heavy read/write contention in Mono edition.Peek: true in the IMAP FetchOptions body section specifier.align, valign, bgcolor) and body backgrounds by refactoring the HTML sanitizer policy and preserving the <body> tag as a surrogate <div>.new-email events, preventing initial IMAP syncs from spamming the backend and triggering global IP rate limiting lockouts./api/users, /api/groups).SyncAllFolders is explicitly called during runSyncCycle, fixing an issue where newly connected or reset accounts failed to download historical emails until manually poked.*server* from .gitignore to unblock go vet and remote builds that were failing due to missing internal/async/server.go.OFFSET and nested OR conditions for email pagination. Implemented strict tuple comparison (is_pinned, date_sent, id) < ($1, $2, $3) for O(1) query latency at any depth, and added a dedicated covering index idx_emails_pagination to schema.sql.(folder_id, is_read, is_muted) to eliminate sequential scans when the background worker recalculates folder unread counts every 30 seconds.idx_emails_folder_isread index to schema_mono.sql to eliminate full table scans and database freeze scenarios during RefreshUnreadCounts background tasks.IMAP server (e.g. Gmail) was automatically marking newly pushed emails as \Seen on the server-side because our fetcher requested the email body without the Peek parameter, causing them to appear as "read" in the UI instantly.
Fix (worker.go):
Peek: true to FetchItemBodySection in the sync worker to prevent IMAP fetches from altering the \Seen flag.Triggering "Reset Account Sync" deleted the database rows for emails and attachments, but left the physical .html and attachment files orphaned on the disk, causing infinite storage bloat.
Fix (storage.go for Postgres & SQLite):
ResetAccountSync. The backend now explicitly queries and iterates over all body_path and attachment path strings and calls os.Remove() before wiping the database rows.Opening an email thread or rendering the inbox list experienced massive delays (up to 10s) due to missing covering indexes and sub-optimal queue lookups.
Fix (schema.sql, storage.go):
idx_emails_thread, idx_emails_unread_fast, and idx_emails_folder_isread to speed up the massive COUNT(*)and thread queries.idx_sync_queue_fetch_active to prioritize account_id as the primary prefix, rescuing the sync worker from table scans.Emails formatted with legacy HTML tables (align, valign, bgcolor) and body backgrounds lost all structural layout and colors because bluemondayaggressively stripped the <body> tag and deprecated presentation attributes.
Fix (email_handlers.go, email_normalize.go):
align, valign, and bgcolor attributes globally across table, tr, td, and th tags in the bluemonday policy.normalizeEmailHTML to dynamically rename the top-level <body> tag into <div id="rms-mail-body-surrogate"> before sanitization, bypassing bluemonday's body-stripping behavior while safely preserving all inline body styles and backgrounds.Heavy background tasks, specifically IMAP syncs and folder unread counts, caused SQLite to throw database is locked (5) (SQLITE_BUSY) and block the API, Webhook poller, and other workers.
Fix (schema_mono.sql, storage.go):
idx_emails_folder_isread to schema_mono.sql which instantly resolved 10s+ table scans during the RefreshUnreadCounts background task.PRAGMA busy_timeout=30000; on the LibSQL driver initialization, instructing SQLite to queue concurrent write locks instead of failing instantly.The frontend sidebar displayed Project Groups even when they contained no accounts (e.g. after removing accounts from a group).
Fix (email-sidebar.tsx & models/email.go):
accounts_count to the ProjectGroup backend model.accounts_count == 0.When logging into a new account, the backend's initial IMAP sync emitted an SSE new-email event for every single email downloaded. The frontend's useEmails.ts reacted to each event instantly by invalidating queries, causing thousands of concurrent API requests that hit the backend's InMemoryRateLimiter limit (300 requests/minute), locking the user's IP globally across all /api/* endpoints.
Fix (useEmails.ts):
setTimeout debounce.Newly connected IMAP accounts (or accounts that were manually reset) were relying solely on the IMAP IDLE push mechanism or periodic jobs to fetch historical emails. They didn't proactively perform a full folder walk.
Fix (worker.go):
SyncAllFolders(ctx, c, acc) call into runSyncCycle() so both Mono and Unified editions reliably execute a full top-down synchronization of all folders during their primary sync loop.The go vet and remote CI builds were failing with "undefined" reference errors for the background job server.
Fix (.gitignore):
*server* from the .gitignore exclusions to ensure internal/async/server.go is properly committed to the repository.In the Mono (or whitelabel) edition, the backend deliberately doesn't mount the /api/users and /api/groups endpoints. React Query in the frontend received a 404 Not Found for these endpoints and entered an aggressive exponential backoff retry loop, creating unnecessary network noise and contributing to rate limiting.
Fix (useEmailQueries.ts):
enabled gating checks typeof window !== "undefined" && localStorage.getItem("geomail_edition") !== "mono" && !window.location.host.startsWith("wm.") to all administrative queries so they simply skip fetching when running in a standalone environment.Gmail IMAP returns BODY[HEADER] and BODY[TEXT] in arbitrary order. The old code took only the first section (break after one iteration), passing either bare headers or bare body text to enmime.ReadEnvelope — causing malformed MIME header initial line on every email synced via the consumer queue.
Fix (fetcher.go — ProcessMessage + ProcessMessageToFolder):
Specifier and reordered: HEADER always first, then \r\n separator, then TEXT. Forms a complete RFC822 message.repairMIMEBoundaries() regex fixes Gmail's missing CRLF between MIME boundaries and the next header (--abc123Content-Type: → --abc123\r\nContent-Type:). Matches hex, alphanumeric, and _/+= boundaries.CheckWorker.runSession() created a temporary SyncWorker per attempt. When refreshToken() succeeded, it updated only the temporary worker's local Account copy — the CheckWorker's Account remained stale. Each retry re-created a new SyncWorker from the stale copy → infinite AUTHENTICATIONFAILEDloop.
Fix (checker.go, worker.go, manager.go):
"token refreshed, need reconnect" error.Manager.LockTokenRefresh(accountID) — per-account mutex serializes OAuth token refreshes. Only one goroutine calls Google; others wait and proceed with fresh tokens.refreshToken() in SyncWorker acquires the per-account lock before refreshing.EnqueueUIDs (ON CONFLICT (account_id, folder_name, uid)) and SaveEmailToFolder (ON CONFLICT (msg_id, account_id, folder_id)) require unique indexes that may not exist in production databases after resync/rebuild.
Fix (sync_queue.go, storage.go):
EnqueueUIDs catches 42P10 → falls back to INSERT ... WHERE NOT EXISTS.SaveEmail / SaveEmailToFolder catch 42P10 → fall back to check-then-insert-or-update.emails_msg_id_account_folder_key unique index (3-column).Correlated COUNT(*) subquery with nested smart-category NOT IN executed per folder row. On 50K emails × 10 folders = minutes.
Fix (storage.go, schema.sql, queue_manager.go):
folders.unread_count INT DEFAULT 0 column.GetFolders now reads COALESCE(f.unread_count, 0) directly (sub-millisecond).RefreshUnreadCounts() runs every 30s — single batch UPDATE for all folders, not per-folder.idx_emails_folder_unread for fast counting.e.msg_id NOT IN (SELECT e2.msg_id ... WHERE e2.account_id = e.account_id ...) — correlated subquery executed per email row.
Fix (storage.go, schema.sql):
emails.smart_category BOOLEAN DEFAULT false column.RefreshUnreadCounts tags Promotions/Social/Updates emails in background.GetEmails filters with simple boolean: e.smart_category = false instead of correlated NOT IN.idx_emails_smart_cat for fast filtering.Webhook poller and queue manager hit database is locked and retried at fixed intervals, causing CPU thrashing.
Fix (webhook_queue_mono.go, queue_manager.go):
SQLITE_BUSY / database is locked errors.Separate SELECT COUNT(*) per agent row instead of aggregating in the main query.
Fix (sqlite/storage.go):
SUM(CASE WHEN e.is_read = 0 THEN 1 ELSE 0 END) in GROUP BY — single query.misc_handlers.go:491-493). OAuth error responses no longer include full HTTP body (may contain tokens). Removed DEBUG username log in authenticate().169.254.169.254, metadata.google.internal) in all editions including Mono.StopAll() now waits for worker goroutines via sync.WaitGroup (15s timeout) instead of returning immediately.statement_timeout = 30s (overridable via PG_STATEMENT_TIMEOUT env). Prevents hung queries from blocking workers indefinitely.GetEmails no longer swallows json.Marshal errors — returns 500 instead of empty 200.HandleLicense and other handlers now set Content-Type: application/json.build-and-push-test.sh + docker-compose-u-test.yml / docker-compose-m-test.yml for isolated staging deployments with :test image tags.SYNC_MAX_WORKERS, PG_SYNC_MAX_CONNS environment variables.GET /api/health returns {"status":"ok"} for Docker healthcheck. GET /metrics exposes 16 Prometheus counters (rms_mail_emails_synced_total, http_requests_total, etc.) via api.MetricsHandler.asynq with automatic retries, exponential backoff, and queue priorities (critical:6, default:3, low:1). internal/async/ package with TaskClient + TaskServer.ncruces/go-sqlite3 to tursodatabase/libsql-client-go. Pure Go WASM driver, CGO_ENABLED=0, single-writer connection (MaxOpenConns=1), WAL mode, busy_timeout=30000. Zero external dependencies — local file only.saveEmailFallback, saveEmailToFolderFallback) now use ON CONFLICT DO NOTHING to prevent race conditions on concurrent saves. MIME boundary regex expanded to support - character in boundaries.runSyncCycle now calls SyncAllFolders on every connection — new accounts get all folders discovered and synced immediately (previously INBOX-only until CheckWorker intervened).ListFolders no longer skips parent folders that have children — emails in nested folders are no longer silently lost.SendEmail handler now uses Asynq task queue (EnqueueSendEmailDelayed) when Redis is available, with Scheduler fallback. Full send pipeline via HandleSendEmail callback./mon/ (Unified only, Redis required) for task queue inspection.worker.go, manager.go). JWT leak in MCP logs fixed (r.URL.Path instead of r.URL.String()). Identity CREATE/DELETE now require CheckAccountAccess. OAuth error responses stripped of full HTTP body. Shared http.Client for webhook dispatches with proper body drain.JWTAuthMiddleware. Mono edition is a safe no-op (nil Redis).webhook_queue.go and job_worker.go were sending the RAW SECRET as X-Signature-256 header — now compute HMAC-SHA256(secret, payload). Secret never leaves the server.encryptPassword now derives per-domain AES keys via SHA256(raw || ":" || domain) — IMAP passwords, OAuth tokens, MCP keys, and Telegram tokens each use independent key material (imap_password, oauth_token, mcp_key, telegram_token). Decryption falls back to raw key for legacy data.sessionAccountID — an MCP client bound to account A cannot receive new-email events from account B. SSE (regular) already had CheckAccountAccess.?token= on non-SSE routes. Both Authorization: Bearer and ?token=accepted; frontend already uses headers.MaxOpenConns raised from 1 to 25, MaxIdleConns from 1 to 5. WAL mode now actually parallelizes: readers don't queue behind writers. DSN includes _synchronous=NORMAL, _cache_size=-64000, _foreign_keys=ON so every pooled connection gets them. Result: M edition handles 20-30 concurrent users without queueing.maybeRotateWorkers evicts the oldest worker every 5 minutes when all maxWorkers slots are occupied. bootstrapMissingWorkers fills freed slots from the waiting queue. Prevents a single heavy account from starving others indefinitely.maxWorkers Default: Raised from 10 to 50. New accounts (created_at DESC) get priority for initial sync.waitWithTimeout goroutine now respects parent context cancellation — won't block on send when caller has abandoned the channel. IMAP-layer context-awareness remains a future improvement.recoverFromRedis/recoverFromDB goroutines now start via explicit Start() call after SetStore/SetContext, eliminating the race window where they polled with nil dependencies.CloseIdleConnections: Added deferred cleanup to per-call http.Client in webhook dispatchers to prevent socket accumulation under load.CASE WHEN UPPER(name) = 'INBOX' THEN 1 ... THEN 3 ELSE 2 END — INBOX always first, custom labels alphabetical, Trash/Spam/Junk forced to bottom. Gmail [Gmail]/Trash and [Gmail]/Spam recognized. Applied to both Postgres and SQLite.smart_categories now calls RefreshUnreadCounts immediately instead of waiting for the 30-second periodic refresh.WakeUpCh Dead Code Removed: Channel never had a consumer. WakeUpAccount now calls TriggerRefresh().OnNewEmail De-globalized: Moved from package-level var to Manager.OnNewEmail field. Wired through Fetcher struct.camoCache LRU: Replaced unbounded sync.Map with container/list-backed LRU (10K entries, periodic cleanup at 80% capacity).stripHTMLTagsFast Entity Decoding: Post-processes with strings.NewReplacer for &, <, >, ", ', — improves AI summary quality.slogWriter Level: IMAP debug trace now uses slog.Debug instead of slog.Info to avoid log spam at default level.notification.RateLimiter Drop Message: Now logs queue depth (%d pending) when dropping.ProcessAutoDraftJob Robustness: context.Background() replaced with proper ctx; ignored error on GetAccount now logged; AppendToDraftsDeduplicated is synchronous (error → job retry instead of silent loss).forceRestartWorkers TOCTOU: Added LockChecker call before starting each worker — an account locked between GetAccounts and worker start is now skipped.ProcessMessage and ProcessMessageToFolder now os.Remove(bodyPath) when SaveEmail fails after the EML file was already written.generateAIDraft Asynq Wiring: HandleGenerateAIDraft now delegates to OnGenerateAIDraft callback → ProcessAutoDraftJob — Stage 3 is operational.sendWebhookWithRetry now enqueues through AsyncClient.EnqueueDispatchWebhook when Redis is available. Webhooks get persistent queue, automatic retries (5x), and asynqmon dashboard visibility. Falls back to Redis ZSET (Unified without Asynq) → SQLite (Mono).handleAutoDraft now prefers EnqueueGenerateAIDraft over DB job queue when AsyncClient is available. Falls back to DB queue for Mono.ResetAccountSync Full Cleanup: Clearing email_sync_queue, imap_move_queue, scheduled_emails, email_comments, and emails_fts (SQLite) on account reset. Previously, email_sync_queue retained completed tasks that blocked re-enqueue via WHERE status != 'completed' guard in EnqueueUIDs — causing 0 emails after resync. Now all sync state is fully purged so the worker performs a clean full re-sync.OnNewEmail and SetEventBroadcast callbacks no longer call store.GetEmail on the main DB pool. Previously, every synced email triggered 2 GetEmail queries on the main pool (one in OnNewEmail, one in InvalidateEmailCacheByEmailID). With 200K+ Gmail inboxes, this generated 400K queries competing with HTTP handlers — causing 10-minute API paralysis after restart. Now OnNewEmail receives subject/senderName/senderAddr directly from the Fetcher (zero DB queries), and broadcast cache invalidation uses account_id from the payload instead of re-fetching the email.InvalidateMetaCache on mutations:GetEmails (5min), GetFolders (30s), GetAccounts (10s), GetGroups (30s)GetLabels (60s), GetRules (30s), GetTemplates (60s), GetContacts (30s)GetIdentities (60s), GetWebhooks (30s), AIModels (1h)InvalidateMetaCache clears all per-account caches on CRUD operations. InvalidateEmailCache also clears folder caches. New InvalidateMetaCache helper for bulk invalidation on account/group mutations. Reduces DB load on every page load from 4+ queries to 0 (cache hit) or 1 (cache miss + write). Previously, every synced email triggered 2 GetEmail queries on the main pool (one in OnNewEmail, one in InvalidateEmailCacheByEmailID). With 200K+ Gmail inboxes, this generated 400K queries competing with HTTP handlers — causing 10-minute API paralysis after restart. Now OnNewEmail receives subject/senderName/senderAddr directly from the Fetcher (zero DB queries), and broadcast cache invalidation uses account_id from the payload instead of re-fetching the email.ResetAccountSync deleted emails from the DB but left 6148 completed rows in email_sync_queue. When SyncAllFolders re-enqueued UIDs, the ON CONFLICT ... DO UPDATE ... WHERE status != 'completed' clause skipped them all. The consumer loop dequeued nothing → 0 emails appeared after resync.
Fix (postgres/storage.go, sqlite/storage.go):
DELETE FROM email_sync_queue WHERE account_id = $1 added before folder/account UID reset.imap_move_queue, scheduled_emails, email_comments (belt-and-suspenders for tables with FK CASCADE).DELETE FROM emails_fts (FTS5 virtual table has no FK support, would accumulate orphaned index entries).GetEmailsCursor in both Postgres and SQLite stores: replaces LIMIT 50 OFFSET 50000 with composite cursor (date_sent, id) — constant-time regardless of page depth.X-Next-Cursor response header with Access-Control-Expose-Headers in CORS config.useEmailsInfinite uses pageParam as cursor string, reads X-Next-Cursor from axios response headers.?offset= continue to work via original GetEmails.GetFolders, GetUnreadCountByAccount, GetUnreadInboxCountByAccount, GetUnreadCountByFolder now use direct COUNT(*) FROM emails WHERE is_read=false instead of cached folders.unread_count column. Partial index idx_emails_folder_unread makes these sub-millisecond.MarkEmailRead, BulkMarkEmailsRead, BulkMarkEmailsUnread update folders.unread_count atomically via CTE — but the authoritative source is now live COUNT.RefreshUnreadCounts removed from queue_manager.go — no more 30-second background timer wasting CPU on idle counters.publishEvent includes account_id in bulk action messages — cache invalidation now fires correctly for read/unread toggles.MemoryCache with TTL and periodic cleanup. When Redis is unavailable (Mono), 11 GET endpoints use in-memory cacheGet/cacheSet/tryCache helpers — transparently switching between Redis and local memory.CheckAccountAccess caching: Uses cache:account:meta:{id} (30s TTL) to avoid GetAccount DB query on every API call. Cuts auth overhead from N+1 to O(1).Get/Set/Ping timeouts: redisOpTimeout=3s applied to all Redis operations — prevents hung HTTP workers on slow Redis.InvalidateEmailCache uses passed ctx instead of context.Background() for proper lifecycle management.OnSendTelegram granular cache: cache:account:meta:{id} replaces full accounts:list JSON parse — O(1) lookup instead of O(n) scan.publishEvent context.Background() → ctx for Redis invalidation.MaxConns=100 default for PostgreSQL pool (PG_MAX_CONNS env overrides)._synchronous=NORMAL&_cache_size=-64000&_foreign_keys=ON for all pool connections.Shutdown() before syncMgr.StopAll() — lets in-flight SMTP/webhook tasks complete.Asynq.Start(ctx) bound to application context instead of context.Background().WithTimeout(1s) — prevents goroutine leaks during shutdown.case "webhook" from StartJobWorker (dead code — webhooks go through Asynq/Redis ZSET/SQLite poller).EnqueueRefreshUnread (dead async type — unread counts now live).HandleSendEmail SkipRetry for cancelled jobs — stops 10x retry spam.SendScheduler.Start() explicit lifecycle — goroutines start after dependencies are set.callAIChatWithTools and callAIChat in ai_handlers.go replaced 80 lines of in-place shared-provider mutation with a 6-line shallow copy via ai.OverrideProviderSettings(). Concurrent AI requests with different models/keys no longer race.OverrideProviderSettings exported: gateway.go — renamed from overrideProviderSettings to exported OverrideProviderSettings. Added *OpenCodeProvider case.resolveAPIKey now iterates ALL keys from ENCRYPTION_KEYS via new crypto.GetAllEncryptionKeys() instead of only the first key. AI settings encrypted with old keys after rotation now decrypt correctly.MemSessionStore thread-safe: Added sync.RWMutex guarding all map operations — prevents fatal error: concurrent map writes crash under concurrent Telegram traffic.decryptTelegramToken in main.go tries all domain-derived keys (telegram_token) and raw keys before falling back to ciphertext — fixes bot auth failure when token is loaded from DB.UpsertAISettings now returns HTTP 500 when ENCRYPTION_KEY is not configured but API keys are being saved — prevents silent plaintext storage.api_keys_encrypted → api_keys: Renamed JSON field on the wire (frontend ↔ backend). DB column name unchanged.AICategorizeEmail parsing: Replaced strings.Split with ai.ParseCategories — properly handles bulleted lists and multi-line AI responses.GetWebhooks no longer returns the Secret field to the frontend.GetTelegramSettings returns XXXX...XXXX instead of the real token.err.Error() == "no rows in result set" → idiomatic errors.Is(err, pgx.ErrNoRows) in all 5 locations.mixtral-8x7b → mixtral-8x7b-32768 in ProviderModels.callAIChat for Ollama now passes effectiveKey instead of hardcoded "".EnqueueWebhook now logs when Redis is unavailable.refetch() call during refresh — was causing triple re-render.FetchAnthropicModels (Anthropic API) and FetchOpenAICompatModels (OpenCode) to fetchProviderModels. All providers now fetch live models from their APIs instead of hardcoded lists. Hardcoded ProviderModels updated to current 2026 models for all 10 providers.FetchGeminiModels now filters to only gemini-* models, excluding legacy PaLM models (chat-bison, text-bison, embedding, aqa).FetchGeminiModels and FetchOllamaModels — previously swallowed API errors silently.ChatWithTools now uses BaseURL instead of hardcoded api.opencode.com. ListModels was return nil, nil — now fetches from provider. ProviderEnvKey added "opencode" case. Cloud URL detection (/zen/go/v1 for opencode.ai, /v1/models for local).callAPI and callAPIWithTools rewritten to proper Gemini format: system → systemInstruction (separate field), assistant → model role mapping. Previously squashed all messages into one text blob.UpsertAISettings now merges incoming keys with existing stored keys. Entering one provider's key no longer wipes all other stored keys. Decrypts existing, merges non-empty values, re-encrypts.resolveAPIKey global fallback no longer requires ALLOW_GLOBAL_AI_KEYS=true (now on by default, disabled by =false). Removed setting.Preset == "" blocker. Proper loop over candidate account IDs.fetchProviderModels now tries all keys from ENCRYPTION_KEYS (comma-separated), not just the first — fixes model fetching after key rotation.AIChat, summarizeEmail, AICategorize, AICategorizeEmail) now return {"error":"..."} JSON instead of generic "internal error" — users can see actual API error messages.callAIChat and callAIChatWithTools no longer overwrite provider model with empty string when apiKey is present but model is empty.grok-2 to DeepSeek API).InvalidateEmailCache MemCache support: Removed early return when Redis is nil (Mono edition). Now properly invalidates both Redis (Unified) and MemCache (Mono). Added MemoryCache.Keys() method for prefix-based invalidation.Del("folders:unified", "folders:") to scanAndDel("folders:*") — correctly clears all account-specific folder caches.markEmailRead, toggleFlagEmail, togglePinEmail, toggleMuteEmail, snoozeEmail, saveDraftReply, clearDraftReply, moveEmail, SetEmailLabels now call InvalidateEmailCache + publishEvent("email_updated") with proper account_id.account_id is not provided by frontend, explicitly calls InvalidateEmailCache("") — was previously silently skipped.email_updated event handler that invalidates ["email", id], ["emails-infinite"], and ["folders"] React Query caches — previously only new-email was handled.selectedEmail object reference changed on each refetch. Now uses useRef with email ID tracking — timer starts once and fires reliably after configured delay.__pending__ guard: Prevents re-timer after successful mutation while React Query cache hasn't updated yet. Clears when is_read actually flips.["accounts"] invalidation in useMarkEmailRead: Auto-mark-read was invalidating ["folders"] but not ["accounts"] — sidebar header counter (accounts.reduce(... a.unread_inbox)) never updated. Manual button worked because useBulkEmailAction did invalidate accounts.["folders"]: useFlagEmail, usePinEmail, useSnoozeEmail previously only invalidated ["emails-infinite"] — sidebar folder counters were stale after flag/pin/snooze actions. Added refetchQueries for folders to bypass staleTime.is_dirty_locally dot removed from email cards — confusing UX, users don't care about IMAP sync status.useUsers disabled for non-Teams editions: Prevents 404 spam on /api/users in Unified and Mono editions.IsTeams() block — works on U, M, T.[SSE] connecting, [SSE] connected, [SSE] event source error) from useEmails.ts.hasSavedKey preservation: Flag no longer cleared when saving without re-entered keys after page reload.