Merge pull request #339 from Coffey-Labs/docs/administration
Record what Administration proved on the live server
This commit is contained in:
+16
-2
@@ -32,9 +32,23 @@ works the same way — and dropped where 0.15 was the whole subject. Support for
|
|||||||
0.15 was removed on 2026-08-26; the last release that runs on it is tagged
|
0.15 was removed on 2026-08-26; the last release that runs on it is tagged
|
||||||
[`stalwart-0.15-support`](https://github.com/Coffey-Labs/ihasmail/releases/tag/stalwart-0.15-support).
|
[`stalwart-0.15-support`](https://github.com/Coffey-Labs/ihasmail/releases/tag/stalwart-0.15-support).
|
||||||
|
|
||||||
- **Administration has not yet been exercised against a live Stalwart.** Accounts were built on 2026-09-13 against the 0.16.22 source and the mock, which reproduces the shapes read there — lists as index-keyed objects, sets as `{"id": true}`, masked secrets, AND-only filters — and it has not touched a real server. Four things are read from source rather than proved: that `/api/account` lists permissions in camelCase (`sysAccountGet`) as the enum serialises them, where the documentation shows kebab-case — both are accepted, so the menu works either way; that a new password written to `credentials/<index>/secret` is hashed and keeps the credential's id; that the Basic credential ihasmail proxies with reaches the admin `x:` methods as it already reaches the self-service ones; and that Stalwart skips its grant check when only a password changes, which is the reason the outranking guard exists at all. The last is worth reproducing rather than trusting in either direction. Query and get are sent as two requests rather than one with a back-reference, because whether the registry methods resolve references was not checked.
|
- **Administration was built from Stalwart's source, and the first live run found the one thing the source reading got wrong.** Accounts and Domains were written on 2026-09-13 against the 0.16.22 source and a mock reproducing it, deployed the same day, and exercised against the live server from an administrator's session. On that server the Accounts list did not load: `x:Account/query` answered **`unsupportedFilter - type`**. A registry filter is keyed by the property's name *as it appears on the object*, and the discriminator is `@type`, so `{"type": "User"}` names nothing the server knows and fails the whole query; `{"@type": "User"}` is accepted. The research that fed the build had listed the field as `type`, and the mock took it without complaint — which is how it shipped. Fixed in [#336](https://github.com/Coffey-Labs/ihasmail/pull/336), and the mock now refuses any filter name the real server does not index, answering the way Stalwart does. Everything else was **confirmed live (2026-09-13)**, mostly read-only, with the domain writes made on a throwaway domain created for the purpose and removed afterwards:
|
||||||
|
|
||||||
- **The Domains section is read from source as well, on the same terms.** Five things are worth confirming on a live server before trusting them: that `dnsZoneFile` is written as `name IN TYPE value` with long TXT records split into a parenthesised run, which is what `dns-update`'s BIND serialiser does on its main branch while Stalwart pins 0.5; that `catchAllAddress` takes a whole address rather than a local part; that `x:DkimSignature/query` accepts a `domainId` filter, as the server's own record builder queries by it; that a refused delete's `linkedObjects` name each object by type as `{object, id}`; and that removing a domain's DKIM keys and then the domain in one request succeeds, since automatic DKIM gives every new domain keys and a domain its keys name cannot be removed. The zone-file reader keeps any line it cannot parse as a row of its own, so a format that differs shows up as an odd row rather than a missing record.
|
- **Permissions** come from `GET /api/account` in camelCase (`sysAccountGet`); an administrator's list held 641 of them and none were kebab-case, whatever the documentation shows. The menu gates on these.
|
||||||
|
- **The Basic credential ihasmail proxies with reaches the admin `x:` methods**, as it already reached the self-service ones. No separate token is involved.
|
||||||
|
- **An account reads back in the shapes the code expects**: `credentials` as `{"0": {"@type": "Password", …}}`, aliases and group memberships as objects, the disk limit under `quotas.maxDiskQuota`.
|
||||||
|
- **A new domain gets automatic DKIM straight away** — an Ed25519 and an RSA key, both `active`, with their records already in the zone file — and manual DNS and certificates.
|
||||||
|
- **`dnsZoneFile` is BIND text**, one record per line as `name IN TYPE value`, with a long TXT record split into a parenthesised run of quoted chunks. A throwaway domain's file held 18 records and 3 continuation lines; every record parsed and the panel showed 18 rows. The production domains also carry TLSA records, which show as rows like any other.
|
||||||
|
- **`x:DkimSignature/query` accepts a `domainId` filter.**
|
||||||
|
- **`catchAllAddress` wants a whole address.** A bare local part is refused with `invalidPatch`, *"Invalid email address"*.
|
||||||
|
- **A domain its keys still name cannot be destroyed**: `objectIsLinked`, with `linkedObjects` listing each as `{"object": "DkimSignature", "id": …}` and no description. Removing through the panel destroys the keys first and then the domain; both were gone afterwards.
|
||||||
|
- **A reserved TLD is refused**: `example` as a domain's top level comes back `invalidPatch`, *"Invalid domain name"*, naming `name`.
|
||||||
|
|
||||||
|
Two things remain unproved, and both need something this session could not do. **A password set by an administrator** is written to the account's existing credential (`credentials/<index>/secret`); the source says it is hashed and keeps the credential's id, which matters because OAuth tokens are tied to it, but signing in with the new password has not been tried. **The outranking guard** exists because the source shows Stalwart skipping its grant check when only a password changes and on a delete; confirming that needs a session with a role below administrator looking at one above it. Until both are tried, trust the guard rather than the gap.
|
||||||
|
|
||||||
|
- **Some refusals show Stalwart's own English description.** The server's `invalidPatch` — used for a reserved TLD and a malformed catch-all address, among others — is not one of the error types the client explains in its own words, so what appears is the server's text, untranslated. Nothing is lost: the description says what is wrong. It is simply English in a German interface.
|
||||||
|
|
||||||
|
- **Administration is off for a device not marked as your own, and for an installation that says so.** Both are enforced by the server rather than hidden by the menu: such a session is sent no permissions, and the JMAP proxy refuses registry methods beyond the account's own. That is worth stating because the proxy otherwise forwards whatever the browser sends, and before these gates an administrator's console could make any registry call their role allowed. For a session that may not administer, the proxy reads a request body only when it could name a registry method — a `"x:` in the text, or a `\u` escape that could spell one — so ordinary mail traffic is forwarded untouched.
|
||||||
|
|
||||||
- **All nine translations have never been read by anybody who speaks them.** They were produced by AI against standard dictionaries on 2026-08-31 — German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, which with English makes ten languages in the picker — and every one of the nine is marked **Beta** in the picker, with that stated in Settings beside a link for reporting anything that reads wrongly. This is the entry that matters most on this page, because it is the one thing here that cannot be closed by testing: a translation can be complete, consistent, pass every check, and still read like a machine wrote it, and nobody on this project can tell which. What *is* verified is the machinery around them. A missing key renders its English source, so a bad line can simply be deleted; a stale key — one whose English no longer exists — is caught by `npm run i18n:check` rather than sitting in the file looking correct and never being looked up. Plurals are asked of `Intl.PluralRules` rather than assumed, which is why Russian and Ukrainian carry three forms and Japanese and Chinese carry one; supplying `one` for Japanese would have been filling in a distinction the language does not draw. Confirmed live on the deployed instance (2026-08-31) against a 6,289-message mailbox: role folders localise and the ~20 custom folders keep the names their owner gave them, dates and the calendar follow the language, and 6,289 renders as *6289 листувань* — the genitive plural a number ending in nine takes, which is the first time the plural machinery ran on anything but a hand-picked value.
|
- **All nine translations have never been read by anybody who speaks them.** They were produced by AI against standard dictionaries on 2026-08-31 — German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, which with English makes ten languages in the picker — and every one of the nine is marked **Beta** in the picker, with that stated in Settings beside a link for reporting anything that reads wrongly. This is the entry that matters most on this page, because it is the one thing here that cannot be closed by testing: a translation can be complete, consistent, pass every check, and still read like a machine wrote it, and nobody on this project can tell which. What *is* verified is the machinery around them. A missing key renders its English source, so a bad line can simply be deleted; a stale key — one whose English no longer exists — is caught by `npm run i18n:check` rather than sitting in the file looking correct and never being looked up. Plurals are asked of `Intl.PluralRules` rather than assumed, which is why Russian and Ukrainian carry three forms and Japanese and Chinese carry one; supplying `one` for Japanese would have been filling in a distinction the language does not draw. Confirmed live on the deployed instance (2026-08-31) against a 6,289-message mailbox: role folders localise and the ~20 custom folders keep the names their owner gave them, dates and the calendar follow the language, and 6,289 renders as *6289 листувань* — the genitive plural a number ending in nine takes, which is the first time the plural machinery ran on anything but a hand-picked value.
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ More, including the mobile layout, on [ihasmail.org](https://ihasmail.org/#scree
|
|||||||
- **Nine new interface languages** — German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, alongside English and separate from the date-and-time locale. Every one is marked **Beta**: they were made by AI and no native speaker has read them yet, which Settings says plainly, with a link for reporting anything wrong
|
- **Nine new interface languages** — German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, alongside English and separate from the date-and-time locale. Every one is marked **Beta**: they were made by AI and no native speaker has read them yet, which Settings says plainly, with a link for reporting anything wrong
|
||||||
- **Twelve themes** — Classic and ihasmail's own, plus Catppuccin, Dracula, Gruvbox, Rosé Pine, Tokyo Night, Solarized, Ayu, Kanagawa, Everforest and Primer, each with the light and dark half its own project publishes. Palette and light-or-dark are separate choices, and the accent colour still sits on top of any of them. Only published colour values are used, taken from each project's own repository; the shades between them are derived and every text colour is measured against the surface it sits on, so a palette that would not meet the contrast this app claims is not written at all — see [Themes](FEATURES.md#themes)
|
- **Twelve themes** — Classic and ihasmail's own, plus Catppuccin, Dracula, Gruvbox, Rosé Pine, Tokyo Night, Solarized, Ayu, Kanagawa, Everforest and Primer, each with the light and dark half its own project publishes. Palette and light-or-dark are separate choices, and the accent colour still sits on top of any of them. Only published colour values are used, taken from each project's own repository; the shades between them are derived and every text colour is measured against the surface it sits on, so a palette that would not meet the contrast this app claims is not written at all — see [Themes](FEATURES.md#themes)
|
||||||
- **On a phone** — swipe a message to archive or delete it (either direction, your choice), hold one to select it, hold a folder for its menu, pull the list to refresh, swipe back from a conversation
|
- **On a phone** — swipe a message to archive or delete it (either direction, your choice), hold one to select it, hold a folder for its menu, pull the list to refresh, swipe back from a conversation
|
||||||
|
- **Administration** — for an account whose Stalwart role manages accounts or domains, from the account menu: create, edit and delete accounts and set their passwords; add domains, copy their DNS records one at a time or as a zone file, see their DKIM keys, and remove them once nothing uses them. Each control is there only when the role allows it, and Stalwart decides every call. Only for a session signed in with *This is my own device* ticked, and `ADMINISTRATION=0` turns it off for everyone — see [Administration](FEATURES.md#administration)
|
||||||
- **Platform** — installable PWA, Web Push with ihasmail closed, `mailto:` handler, no credentials in the browser, strict CSP, SSRF-safe image proxy
|
- **Platform** — installable PWA, Web Push with ihasmail closed, `mailto:` handler, no credentials in the browser, strict CSP, SSRF-safe image proxy
|
||||||
|
|
||||||
The long version is on [ihasmail.org](https://ihasmail.org/#features); how to
|
The long version is on [ihasmail.org](https://ihasmail.org/#features); how to
|
||||||
@@ -374,11 +375,12 @@ without a real mailbox. It reproduces the things a naive fake would get wrong,
|
|||||||
because each cost a live debugging session: `urn:stalwart:jmap` advertised
|
because each cost a live debugging session: `urn:stalwart:jmap` advertised
|
||||||
**per-account** rather than session-level, identity signatures capped at 2047
|
**per-account** rather than session-level, identity signatures capped at 2047
|
||||||
**bytes**, and `CalendarEvent/set` speaking Stalwart's vocabulary rather than
|
**bytes**, and `CalendarEvent/set` speaking Stalwart's vocabulary rather than
|
||||||
RFC 8984's. Three switches: `MOCK_NO_FUTURE_RELEASE=1` advertises FUTURERELEASE
|
RFC 8984's. Four switches: `MOCK_NO_FUTURE_RELEASE=1` advertises FUTURERELEASE
|
||||||
and then drops every hold; `MOCK_NO_REGISTRY=1` omits the Stalwart capability so
|
and then drops every hold; `MOCK_NO_REGISTRY=1` omits the Stalwart capability so
|
||||||
the sign-in refusal can be tested; and `MOCK_NO_SCHEDULING_SEND=1` refuses a
|
the sign-in refusal can be tested; and `MOCK_NO_SCHEDULING_SEND=1` refuses a
|
||||||
calendar write that asks for scheduling messages, the way an account without
|
calendar write that asks for scheduling messages, the way an account without
|
||||||
that permission is refused.
|
that permission is refused; and `MOCK_ROLE` decides who the demo user is for
|
||||||
|
Administration — `admin` (the default), `tenant-admin`, `helpdesk` or `user`.
|
||||||
|
|
||||||
It tracks the current release rather than 0.16 in general, and each behaviour
|
It tracks the current release rather than 0.16 in general, and each behaviour
|
||||||
is confirmed against a real server before it is copied here — the comments say
|
is confirmed against a real server before it is copied here — the comments say
|
||||||
|
|||||||
Reference in New Issue
Block a user