Files
webmail/lib/jmap/jmap-error.ts
T
Linus Rath ffa909600c fix: keep web push on when someone shares a mailbox with you
Stalwart refuses a push subscription's whole emailPush map as
`forbidden` when it names a mailbox shared by ACL (0.16.16 and later,
and 1.0). Bulwark put every account with mailboxes in the map, so
enabling push showed raw JSON, and the background re-sync destroyed the
working subscription after its refresh was refused and dropped its key:
push stopped without a word and could not be turned back on while the
share existed. A network error, a timeout or rate limiting during the
refresh ended it the same way.

- The map holds the primary account and the accounts that list
  `urn:ietf:params:jmap:emailpush`, which 1.0 leaves off ACL shares.
  0.16 lists it everywhere, so a refused map narrows to the accounts
  that look owned, then to the primary account, as the mobile app does.
  Refused accounts are remembered under
  `bulwark.push.emailPushRefused.v1.<id>`; Re-register and disable
  clear the list.
- A subscription ends only when a verified replacement exists, the user
  turns push off, or the server no longer has it. The old record is
  destroyed after the new one is verified. A failed listing, a network
  error or a refused method call leave it and its key alone.
- PushSubscription/set failures throw typed errors (`jmapSetError` for
  a refused object, `jmapError` for a refused call) instead of JSON in
  the message or `false`, and the client exposes `getAccountInfo`.
- Settings says that mailboxes others share don't send background
  notifications, and `forbidden` and `overQuota` read as plain text in
  settings and the onboarding prompt.
2026-09-25 17:59:17 +02:00

55 lines
2.1 KiB
TypeScript

/**
* JMAP errors carried on a thrown `Error`.
*
* A method error or a SetError names what went wrong in its `type`
* (`overQuota`, `forbidden`, `invalidProperties`, ...) and, for
* `invalidProperties`, which `properties` the server refused. Callers
* branch on those instead of parsing the human-readable description, which
* differs between servers and Stalwart releases.
*/
/** The parts of a JMAP method error or SetError a caller can branch on. */
export interface JmapErrorInfo {
type?: string;
description?: string;
properties?: string[];
}
export type JmapError = Error & { jmapType?: string; jmapProperties?: string[]; jmapSetError?: boolean };
/** An `Error` with `message` that keeps the server's error type and properties. */
export function jmapError(message: string, info?: JmapErrorInfo | null): JmapError {
const error: JmapError = new Error(message);
if (info?.type) error.jmapType = info.type;
if (info?.properties?.length) error.jmapProperties = [...info.properties];
return error;
}
/**
* {@link jmapError} for a SetError: the server looked at one object of a /set
* call and refused it (`notCreated`, `notUpdated`, `notDestroyed`, RFC 8620
* §5.3), as opposed to refusing the whole method call. The same type can come
* either way (`forbidden` is both), and only the SetError says something about
* that object.
*/
export function jmapSetError(message: string, info?: JmapErrorInfo | null): JmapError {
const error = jmapError(message, info);
error.jmapSetError = true;
return error;
}
/** Whether a thrown value is a SetError (see {@link jmapSetError}). */
export function isJmapSetError(error: unknown): boolean {
return (error as JmapError | null | undefined)?.jmapSetError === true;
}
/** The JMAP error type a thrown value carries, if any. */
export function jmapErrorType(error: unknown): string | undefined {
return (error as JmapError | null | undefined)?.jmapType;
}
/** The properties an `invalidProperties` error named, if any. */
export function jmapErrorProperties(error: unknown): string[] {
return (error as JmapError | null | undefined)?.jmapProperties ?? [];
}