Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 27. října 2026

Procházet dokumentaci · Email background alert delivery

Tento článek zatím není přeložen. Je dostupný v angličtině.

Email background alert delivery

src/alerts.ts exports registerEmailAlerts(ctx, store, factory, isReconnectRequired). The foundation wires it once in index.ts, with the same EmailStore and the real MailBackendFactory used by mail operations. No fallback backend or credential store exists.

Ownership and registration

Only ctx.authoritativeProcess registers registerInterval('alerts', tick, 120_000). The host owns the timer, skips overlapping ticks, drains active work on shutdown and clears the timer. A runner never registers polling. The job enumerates store.alertAccounts(); before any credential access, it checks ctx.host.stores().usersRead.mayUsePlugin(userId, 'email'). This is the native live predicate for loaded/enabled plugin state, user existence and the owner's grant, and requires the manifest's reads: ['stores'] capability. Denied owners are skipped without failing the interval; access-lookup errors still reject it. Regrant resumes polling. Each factory call receives the explicit account, including userId. Cursor reads and writes always supply that owner and account ID. The job rereads owner-scoped accounts through store.list(userId) before polling, so a stale enumeration cannot reopen a disabled or deleted account.

The factory must acquire the account owner's credentials through the authorized host vault seam, not ambient userSecrets(), an instance vault, a retained interactive bag, a fabricated account identity or direct host SQL. Integration requires the owner-authorized background vault contract and actual adapters; this module does not implement those seams.

Registration returns { stopUser(userId), clearAccount(userId, accountId, change) }. The API wraps alert-disabling updates and account deletion in clearAccount. It closes poll admission for that account, drains the owner's admitted poll through backend close, delivery and cursor persistence, then awaits the supplied metadata/removal operation. Only a successful operation clears the owner's standing reconnect key through ctx.alerts.clear and forgets the in-memory reconnect episode. Failed changes retain the warning and propagate the failure to HTTP. Concurrent changes keep admission closed until all settle. Admission is released afterward: a later explicit re-enable can poll and start a new reconnect episode. This hook coordinates background polling, not unrelated interactive mailbox operations.

The foundation's user-removal callback must await stopUser(userId) before deleting any account rows or credentials. It blocks admission for that owner for the lifetime of this registration and drains their already admitted factory, poll, refresh, close, delivery and cursor commit. Accounts from an enumeration snapshot taken before removal are skipped too. A failed admitted operation still rejects its interval tick; draining does not turn it into success. This ordering prevents a late refresh from recreating credentials after cleanup. It does not replace the host vault's own removal guarantees or coordinate refreshes started by other consumers.

Baselines and delivery

MailBackend.poll(null) establishes a baseline. The job commits it without mail alerts. Later polls return only new headers and the next cursor. An IMAP UIDVALIDITY reset is the backend's responsibility: it returns a fresh silent baseline, not old UIDs as new mail. The job neither parses Graph delta tokens nor interprets provider cursors.

One to three headers raise one alert per message using newMail.title/body. Four or more raise one newMailSummary.title/body with the count and an inbox link. Every alert uses scope: { user: account.userId }; there is no project or administrator broadcast. Individual links are /p/email/<accountId>/<encodeURIComponent(publicMessageId)>; summary and reconnect links are /p/email/<accountId>. The page must authorize the account and decode the full public message ID using the existing message-ID parser.

Keys begin mail:<userId>:<accountId>:. Message occurrence suffixes use the shared hashText(publicMessageId), and summary suffixes use summary:<hashText(JSON.stringify(previousCursor))>. Hashing preserves full provider identity within the host's 200-character key limit. Retries reuse the last committed checkpoint, even when the provider issues a different next delta token or changes header order. The next cursor is saved only after the backend has closed and every alert delivery has resolved. A delivery, close or cursor-write failure leaves the previous cursor available for retry; already delivered alerts are deduplicated by the host.

The host limits alert URLs to 500 characters. Integration must verify actual adapter IDs and page deep links against that existing limit; this job does not introduce a second URL lookup store or silently replace a message link with another destination.

Authentication and operational failures

The injected classifier must recognize typed authentication failures from factory acquisition or polling. It must not inspect exception text or classify transport, database, delivery or cleanup failures as login rejection. The adapter owner supplies that mapping; there is no default classifier. Adapters must release partially acquired resources when their factory rejects. Once a backend is returned, the job always awaits close(), including after a failed poll.

A typed login rejection raises the owner's standing reconnect alert once per failure episode. Failed alert delivery is retried. Successful polling clears the persistent reconnect key even after a process restart, then permits a later rejection to ring again. A handled login rejection does not fail the job. Network and other operational failures are collected while remaining accounts are checked, then reject the tick as an AggregateError. The host interval's three-consecutive-failure fault signal and recovery logging therefore remain authoritative; this plugin does not duplicate that signal. Because the host logs only the rejected tick's message, that message names every failing account as <kind> account <accountId>: <cause>, never its address, joined by ; . A schema rejection is rendered on the same line as its issue paths and messages, without the rejected values; the original errors stay in the AggregateError.

Verification

tests/emailAlerts.test.ts supplies explicit typed backend and store fixtures. It covers silent initialization, one/three/four-header thresholds, owner-scoped factory/cursors/alerts, links and bounded occurrence keys, successful commits, retry without cursor loss, backend UIDVALIDITY reset results, login failure episodes and recovery, operational outages, close/read/write failures, user-removal drain, denied grants, failed access lookup, stale disabled/deleted enumeration and overlapping account changes. tests/emailRegistration.test.ts checks real grant revocation/regrant through the native predicate and encrypted vault, plus successful and failed disable/delete routes and a login rejection already in flight. Two tests use the actual host registry and service runner to verify overlap suppression, shutdown drain/timer cleanup, the three-failure fault signal and recovery. These are not live IMAP/SMTP tests or proof of browser deep-link navigation.