Files
webmail/lib/jmap/principal-directory.ts
T
Linus Rath 6152e45b88 fix: explain sharing and free/busy when Stalwart 1.0 keeps its directory closed
Stalwart 1.0 refuses Principal/query and Principal/getAvailability with
`forbidden` unless the administrator allows directory queries, which is
off by default. `getPrincipals` turned that into an empty list with no
signal: the share dialog said "No other users or groups found", existing
shares showed bare ids, and free/busy called every attendee "Not a user
on this server".

- `listPrincipals` says whether the listing is complete, `restricted`
  or failed. On `forbidden` it falls back to Principal/get without ids,
  which 1.0 still answers with the user's own account, their groups and
  the accounts that shared something with them.
- Principal/get asks for `id`, `type`, `name`, `description` and
  `email` only; 1.0's default list adds `capabilities` and `accounts`.
- One listing per client, kept for five minutes, serves the share
  dialog, recipient autocomplete and free/busy, which used to download
  the directory three times.
- The share dialog explains a restricted directory while still offering
  its candidates, says when the list failed to load, and shows "Unknown
  user" with the id for sharees it can't name.
- Free/busy first reads `mayGetAvailability` from the user's own
  principal and stays hidden when it is false or a lookup is refused
  with `forbidden`. "Not a user on this server" now needs a complete
  listing; failures and `rateLimit` show "Couldn't check".

On 0.16 with the default roles the listing is complete and the dialog
behaves as before.
2026-09-25 17:45:22 +02:00

38 lines
1.6 KiB
TypeScript

/**
* One principal listing per client, shared by everything that needs the
* server's directory: the share dialog, recipient autocomplete and free/busy.
* Each used to download the whole directory on its own - the share dialog on
* every open - and a listing pages through up to 20,000 principals.
*/
import type { IJMAPClient } from "./client-interface";
import type { PrincipalListing } from "./types";
export const PRINCIPAL_DIRECTORY_TTL_MS = 5 * 60 * 1000;
const listings = new WeakMap<IJMAPClient, { loadedAt: number; listing: Promise<PrincipalListing> }>();
function fetchListing(client: IJMAPClient): Promise<PrincipalListing> {
if (client.listPrincipals) return client.listPrincipals();
return client.getPrincipals().then((principals) => ({ principals, status: "ok" as const }));
}
/**
* The client's principal listing, shared while it is fresh; callers that
* arrive while it loads get the same request. Never rejects.
*/
export function loadPrincipalDirectory(client: IJMAPClient): Promise<PrincipalListing> {
const cached = listings.get(client);
if (cached && Date.now() - cached.loadedAt < PRINCIPAL_DIRECTORY_TTL_MS) {
return cached.listing;
}
const listing = fetchListing(client).catch((): PrincipalListing => ({ principals: [], status: "error" }));
const entry = { loadedAt: Date.now(), listing };
listings.set(client, entry);
void listing.then(({ status }) => {
// A failed listing isn't kept, so the next caller asks again.
if (status === "error" && listings.get(client) === entry) listings.delete(client);
});
return listing;
}