The in-app Web Push device is now told only when mail lands in the Inbox of a mailbox that has finished importing, and Outlook stops recording folder moves for messages that did not move. JMAP clients and FCM phones keep every StateChange. Migration: jmap_push_subscription.arrivals_only, set once for in-app devices.
Takes over #112.
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Eleven of the twelve items. The twelfth, a deleted label coming back
after the next sync on IMAP, is left for an issue of its own: there a
label is a folder, and what deleting one should do to the mail in it
needs deciding first.
A selection survives the pager. It was nothing but the ticked
checkboxes, so "next page" replaced the rows and the selection with
them. The rows that are not on screen are now remembered in
assets/mail_selection.js, per list, and the toolbar, the label menu,
the move menu and a drag all read the selection from there. The
checkbox stays the truth for anything visible.
The whole view can be selected from the select menu. It was only
offered by a banner that appears once a page is fully ticked. And a
whole-view selection no longer survives a row being unticked: the
action still ran on everything.
A sync no longer logs a stored message as one it could not build.
MessageSyncer reads the UIDs a folder holds once, before it fetches,
and a move made meanwhile files rows under UIDs that list does not
have. The INSERT was refused on (mailbox, uid), rightly, and each
message was then held back as a failure. The list is read again when
a batch is refused.
A pairing code and the JMAP address under App passwords carry the
configured public address, and the browser's only when none is set.
This reverses the earlier choice in DevicePairingController, which
handed a phone paired at home a LAN address.
Hiding quoted text gives the height back. scrollHeight cannot report
less than the frame it is measured in, so the collapse reports the
root element's box instead and holds other reports until the parent
has resized.
Mail in Sent shows its images without asking. Read off the Sent label
and not the From line, which a forged mail fills in as it likes. Spam
still wins.
Smaller: placeholders are lighter than typed text, the actions on a
connected integration are buttons on one baseline, Save signature and
four composer buttons no longer hover to white on white, the Spam
badge loses the accent, and German says "vor 12s".
Refs #37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every existing way a subscription disappears is the server's decision: the
endpoint answered 410, Firebase said UNREGISTERED, the browser re-subscribed
and replaced its own row. None of them reach the case that actually turns up
in support — an old build of an app still registered under a device id nothing
uses any more, delivering a second copy of every notification to the same
phone. That row is healthy and succeeds every time, so nothing retires it, and
until now the only remedy was a DELETE typed into psql.
Settings -> Notifications now has a Remove on each device row. POST only,
CSRF-protected, and the row is looked up scoped to the authenticated user so a
guessed id resolves to nothing rather than to somebody else's device.
The delivery log is deliberately untouched. PushDelivery is addressed by device
id and has no relation to the subscription precisely so its rows outlive it,
and this must not become the one path that quietly takes them down: "this
device stopped being delivered to on Tuesday" is exactly what somebody asks
about a device they removed. A test pins that, so a later tidy-up with a
OneToMany and a cascade fails rather than erasing history.
Wording says what removal does and does not do: deliveries stop, nothing is
uninstalled or switched off on the device itself, and an app that still wants
push registers again the next time it runs. EN and DE, in the confirm and in
the toast, and in the handbook in both languages.
Not in WebPushController: that is the service worker's JSON API, unsubscribing
the browser it is running in by the device id it holds locally. This is a
settings form about a device that is usually not the one being sat at.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every attempt both senders make lands in push_delivery - transport,
device, outcome, the error name that tells a dead phone from a Firebase
outage, latency - browsable under /admin/push/deliveries and echoed per
device in the user's notification settings. Nothing of the payload past
its @type is stored, and the record survives the subscription it is
about.
A held submission now answers EmailSubmission/get as pending with its
real release time, a cancelled one as canceled, and the changes feed
reports both transitions - the release time used to exist only in the
create response, which forced clients to keep schedules device-local.
Cancelling a never-submitted mail is refused instead of arming a flag
the next real send would silently trip over.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README says what plMail is and gets you running; CONTRIBUTING says how to
change it. Nothing covered the part in between — using it, operating it,
and registering the Google and Microsoft applications it talks to. That
is docs/: 34 pages across features, installation, providers and
internals, mirrored to the GitHub wiki.
The wiki is a mirror and never a source. bin/mirror-wiki.php flattens the
tree into the wiki's flat namespace, rewrites every relative link,
generates the sidebar from the index so the two cannot disagree, and
stamps each page with a footer saying where it came from. A browser edit
survives until the next push to main. That cost is deliberate:
documentation not versioned with the code drifts from it, and the drift
is invisible until somebody follows an instruction that stopped being
true.
Which is the whole problem, so the checkable half is checked.
DocumentationCoverageTest fails the build when a command is missing from
the reference or documented and gone, when a variable in .env is absent
from the configuration page, when a relative link dangles, when a page is
unreachable from the index or promised and missing, or when a page has no
"Things that bite" section. It cannot check whether a paragraph is TRUE,
and says so — this is the floor, so review attention goes to the part a
machine cannot have an opinion about. CONTRIBUTING has claimed since it
was written that the command table "is part of the definition of done";
nothing enforced it until now, and it was wrong in three places.
Writing it found things. They are fixed here rather than documented
around, because a handbook describing a bug accurately is still a
handbook nobody can follow.
compose.yaml mounted no volume for var/attachments, var/raw
or var/uploads. Workers write those during sync
and the web container serves them back, so each
kept its own copy in its own layer — downloads
404, and everything died on a recreate. The
override and the TrueNAS file both mount them;
the file the README tells an operator to run did
not. The same omission cost real data once
before. See the CHANGELOG for moving what is
currently stranded.
app:calendar:materialise new, nightly. Occurrences are drawn when an
event is saved and nothing moved that window
again, so a weekly standup ran out of dates in
two years and its reminders stopped with them —
silently. The query it runs had existed since
the materialiser did, documented as exactly this
sweep, and had never been called.
Microsoft wizard asked for three of the nine permissions plMail
requests, and a token already issued is not
upgraded when the rest are added later.
GmailWatchService told you to grant Pub/Sub Publisher to
gmail-api@system.gserviceaccount.com. The
account is gmail-api-push@; a topic granted
exactly what the docblock asked for never
delivered a notification.
IcsExporter said it wrote no VALARM. It has written them
since alerts landed.
CLIENT_DEVELOPMENT.md said there was no JMAP calendar API and that
SearchSnippet/get was absent. Both shipped.
CONTRIBUTING said only app:calendar:push registers a push
channel; subscribing has done it since the
subscribe flow learned to.
Three docblocks named getters removed long ago, or described a
method two below the one they sat on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>