An account linked for its files also offered its calendar and its address
book, and neither had been shared. That was not ihasmail inventing them:
asked about the other account, the live 0.16.19 returns every calendar
and every book it holds, each with full rights -- read, write, share,
delete, all true. There is nothing in the rights to tell "shared with me"
from "reachable at all", because the server does not distinguish them.
`isSubscribed` does, and it is the field JMAP has for exactly this: it
came back false on all of them. So a shared calendar or book is listed
under "Shared with me" once the reader has added it, and under "Available
to add" until then, with one button either way.
Nothing unsubscribed contributes anything. A calendar that has not been
added draws no events, and a book that has not been added lends no cards
to the To field -- which is the one that mattered most, since it is the
difference between offering a colleague's contacts and offering a
stranger's without anyone having asked.
The mock's shared calendar and address book now arrive unsubscribed, the
way the real server hands them over, so the adding is exercised rather
than skipped; and its `Calendar/set` and `AddressBook/set` route by
account, since subscribing to somebody else's is a write to their
account and the mock had nowhere to put it.
Verified against the mock: the shared calendar sits under "Available to
add" with no events in the grid, adding it moves it to "Shared with me"
and its events appear, removing it undoes both; and `suggest("katherine")`
finds nothing until the shared book is added, then finds her.
Address book sharing was withdrawn a few hours ago on a report that it
behaved like mail folder sharing. That was wrong -- it works -- and it is
back, built the way Files is rather than the way it was.
Three things it inherits from Files. Shared books are listed in the app's
own left pane instead of behind an account switch in the profile menu.
The reader's books and other people's sit under separate headings, since
a book belonging to somebody else behaves differently and a single merged
list would be quiet about whose contacts you are reading. And opening
Contacts re-reads the session, so a book shared while the tab was open
turns up without signing out and in again.
The books pane the view kept to itself is gone, and with it the last
module that ignored the sidebar it was given.
The one thing Files does not need: shared contacts have to answer when
somebody types a name into a To field, so they are loaded up front rather
than when a book is opened, and they are offered by `suggest` and found
by `lookupByEmail` alongside the reader's own. Their own cards win a tie,
since a card someone wrote themselves should beat a colleague's copy of
the same person. That is the difference between a shared book you can
look at and one you can use.
Cards from a shared account are held apart from the reader's rather than
merged in, and keyed by account as well as id. Ids are only unique within
an account -- two accounts each having a book `ab1` is ordinary -- and a
flat map would have had one silently replace the other.
The mock grew an address book in its shared account, with contacts in it,
because none of this could be exercised otherwise.
KNOWN-ISSUES records the withdrawal as the mistake it was rather than
leaving it in the history looking like a finding. Mail folder sharing
stays withdrawn: that one really is broken.
Sharing a mail folder does nothing. `Mailbox/set` takes the `shareWith`
map, `Mailbox/get` reads it back, and the folder never appears for the
account it was shared with -- confirmed on the live 0.16.19 with a folder
shared read-only to another account on the same server, which never saw
it. Stalwart's sharing documentation lists calendars, address books and
file storage; mail folders are not among them. Nothing anywhere reports a
failure, so a client that trusts what it reads back shows the share as
live for ever, which is what happened.
The entry point is withdrawn. Address book sharing goes with it on a
report that it behaved the same way -- not reproduced, and contradicted
by Stalwart's own docs, so that one is expected back; it is out because
offering a share nobody can verify was worse than the gap. Files and
calendars are untouched.
Removing a share was impossible, for a reason worth writing down. The
dialog rendered the list of who a thing was shared with *inside* the
branch that runs when the directory has principals to offer. A server
with `allowDirectoryQueries` off returns none -- that is the default, and
it is how these shares came to be made in the first place -- so the
dialog showed one line of hint and nothing else. The share was there, and
there was no way to see it, let alone remove it. The list is now rendered
whatever the directory says; only the control for adding somebody new
depends on having somebody to add.
So the withdrawn entry points do not strand what they created: a folder
or book already shared still offers "Stop sharing", which is the one
thing you want when the share is invisible everywhere else.
The API was never the problem, which is worth recording since it was the
first guess: `shareWith: null` is accepted and clears the map, tested
against the live server on the stuck folder, which is now unshared.
Right-clicking a sender, or any address in the message details, opens a menu
offering to add that person to the address book - plus edit them when they are
already known, write to them, or copy the address.
"Add to contacts" opens the contact editor prefilled rather than saving
silently, so the address book gets a real card that the user can complete,
not a bare email address. contactFromAddress splits the display name into
JSContact name components: "Ada Lovelace" into given and surname, "Lovelace,
Ada" unpicked, a single word as the given name, and a name that is really
just an address left off entirely.
Addresses in the details block were joined into one string, so they are now
rendered per address to be individually targetable.
ContactEditor previously ignored a prefilled name on an unsaved card - it read
name components only when the card had an id - so it now reads them either
way.
Browsers render <input type="date"> and datetime-local in their own locale and
ignore the page's, so #1 left a German user on an English browser reading
22.11.2025 everywhere but still entering dates through an mm/dd/yyyy widget.
#3 makes the case that people use the picker rather than typing, which is
where the AM/PM mistakes happen.
New DateField and DateTimeField (web/src/ui/datefield.tsx) replace all nine
native controls — event editor (all-day and timed start/end, recurrence
until), out-of-office, contact birthday, advanced search. They take and emit
the same ISO strings the native inputs did, so call sites barely changed.
Each is a text box in the configured order plus a popover: a month grid
(week start from settings, locale weekday and month names, today and the
selection marked) and, for date-times, a list of times in the configured
clock. Keyboard: arrows move by day, PageUp/PageDown by month, Home/End
across the week, Enter picks, Escape closes, ArrowDown opens; the focused day
holds DOM focus so screen readers follow, and the dialog has an accessible
name (Popover gained an ariaLabel prop).
Text entry is lenient — the configured order with any separator, unseparated
digits (221125), day and month alone, non-Latin digits, and bare ISO always;
times take 18:23, 1823, 6:23pm, 930. What will not parse reverts on blur
rather than clearing the field, and impossible dates like 31 February are
rejected instead of rolling into March.
Editable boxes stay Gregorian and Latin-digit even where display does not
(fa-IR, th-TH, ar-EG): the locale's field order and separator are kept, but a
Buddhist-era year in a text box cannot round-trip against a Gregorian grid.
Noted in the README.
The out-of-office format echo added in #2 is gone — the fields now show the
right format themselves.
Closes#3
Every user-visible date now goes through web/src/lib/datetime.ts, driven by
three settings (Settings > General > Locale):
- Language & region: automatic, or any of the 618 locales CLDR has data for,
each named in its own language and script (web/src/lib/locales.ts, generated
by probing Intl over the subtag space).
- Date format: automatic (locale order), 22.11.2025, 22/11/2025, 11/22/2025,
or ISO 8601 2025-11-22.
- Time format: automatic (locale), 24-hour, or 12-hour.
Automatic takes the locale Stalwart has for the account, read best-effort at
login via x:Account/get (urn:stalwart:jmap) and passed to the client in the
session; servers without the capability, or that deny sysAccountGet to a
regular user, fall back to the browser locale. POSIX forms are normalised
(de_DE.UTF-8 -> de-DE) and script modifiers kept (sr_RS@latin -> sr-Latn-RS,
uz_UZ@cyrillic -> uz-Cyrl-UZ), while dialect/variant/currency modifiers are
dropped and a script the locale already implies is not appended.
Numerals follow the locale (22.11.2025 renders as Arabic-Indic digits under
ar-EG); ISO 8601 is the exception and pins date and clock to Latin digits so
one line never mixes digit systems.
Rewired: message list and headers, quoted reply headers, calendar (titles,
weekday and hour gutters, mini calendar, agenda, popovers, invite cards,
free/busy), contacts, files, sessions. No raw toLocale*String date calls are
left in web/src.
Native <input type="datetime-local"> pickers always follow the browser locale
and cannot be restyled by a page, so the out-of-office fields echo the entered
instant in the chosen format underneath.
Also: month-grid day labels no longer wrap when they hold a date, and the mock
server serves x:Account/get (MOCK_LOCALE, default en_US).
Closes#1