- t(key, vars) interpolates {placeholders}; numeric count picks a CLDR plural variant
- createTranslator(locale) shares the translator with server code
- registry entries accept dir: "rtl"; layout and provider set <html dir>
- DialogContent translates its close label itself (closeLabel stays an override)
- memoize the language context value, merge double map in dashboard-nav
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Stub the DOM-only email HTML module in the recipient-display test bundle,
since css-tree/parse5 do not bundle for node. Add contain:paint to the
reader host so fixed/absolute email CSS cannot cover the app UI.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- IMAP import: walk a folder newest-first in batches of up to 100 via an
`offset` / `nextOffset` pair, so folders with more than 100 messages can be
imported completely. The import page loops until every folder is done and
shows progress. The per-request limit stays at 100 (default 100).
- IMAP import: recognise iCloud `Sent Messages` and `Deleted Messages`.
- Agent model list: use `redirect: "manual"` instead of `"error"` so
providers that redirect do not fail the request with a generic error.
- Domains: require typing the hostname before removing a domain, since
removal deletes the domain's mailboxes and all their messages.
- Add tests for the batch selection helper.
Adds a "Delete forever" action and an "Empty Trash"/"Empty Spam" button so
mail can be removed for good from the web UI (closes#68, part 1).
- Bulk action "delete" only destroys messages already in Trash or Spam
- POST /api/messages/empty empties a mailbox's Trash or Spam in batches
- Message view: the delete button becomes "Delete forever" in Trash/Spam
- Every removal deletes R2 objects and writes an email.delete audit entry
The domain rule dialog submits its whole form state, so "Deliver to
mailbox" and "Reject / block" rules arrive with forwardTo: "". The
schema only allowed an email or null, so those requests failed with a
400 that the dialog shows as "Request failed". This blocked creating
the "*" catch-all, editing such rules, and toggling them on or off.
Treat a blank forwardTo as null. Forward rules still require a valid
address through the existing superRefine check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inbound To no longer says "me". To, Cc, and Bcc show the mailbox
address. A switch on the signed-in user, under Settings → Inbox,
shows Name <user@domain.tld> when the header includes a display name.
* fix(jmap): answer the header filter in Email/query instead of ignoring it
conditionToSql() had no `header` branch, so a condition it did not
recognise was dropped and the query matched every message in the
mailbox. A client that de-duplicates drafts by Message-ID before
importing therefore either adopted an unrelated draft or refused to
send because "multiple remote drafts" came back.
Handle `header` per RFC 8621 §4.4.1 from the columns Mailflare already
stores — Message-ID, In-Reply-To and References — and make every other
header name an `unsupportedFilter` error, which is what RFC 8620 §5.5
requires of a filter the server cannot apply. Message-IDs compare
without angle brackets, since inbound rows store them and outbound rows
do not, and header names are matched case-insensitively.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(jmap): Email/import so MIME-composing clients can send
Clients that build MIME locally — Flectar Mail and anything else on the
jmap-client crate — send by uploading the raw message, importing it into
Drafts and submitting it. Email/import answered `unknownMethod`, so
reading worked but every send failed.
Implement it for the Drafts mailbox (RFC 8621 §4.8). The blob must be an
upload this account owns, the target exactly one Drafts mailbox, and the
parsed From has to pass the same getAuthorizedSenderAddress() check a
draft created through Email/set does; anything else comes back in
notCreated as blobNotFound, invalidProperties, invalidEmail, forbidden
or tooLarge. Importing into arbitrary mailboxes is deliberately left
out: delivered mail belongs to the inbound pipeline, which owns
threading, spam scoring and inbound state.
The parsed headers fill the same columns Email/set create writes —
Email/set create and Email/import now share one insert and one target
rule — plus providerMessageId, which is what lets the client find its
own draft again with a Message-ID filter. The uploaded bytes are kept
verbatim under `drafts/<messageId>.eml`, so downloading the message's
blob returns the client's own MIME instead of the rebuilt minimal
message, and the jmap-uploads object is released once claimed.
Email/copy and Email/parse stay unsupported.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(jmap): cover Email/import validation and the header filter
tests/jmap-email-import.test.mjs is a node:test file over the pure
pieces — the mailbox rule, keyword and receivedAt parsing, and the SQL
Email/query builds for header conditions, including the unsupportedFilter
throw and the regression that a Message-ID filter narrows the mailbox
instead of matching every draft. The modules are TypeScript behind the
`@/*` alias, so they are bundled with the esbuild the repo already uses
for the self-hosted server rather than loaded directly; nothing here
needs a Workers binding.
scripts/jmap-import-check.mjs is the end-to-end pass against
`npm run dev`: upload a multipart message with an attachment, import it,
read it back, download the blob byte-for-byte, query it by Message-ID,
submit it, and check every rejection path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(jmap): document Email/import and the header query filter
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(jmap): clean up imported draft objects
---------
Co-authored-by: egamer24 <25360766+egamer24@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com>
Self-hosted Node/Docker has no Email Worker named mailflare, so the
catch-all PUT 404s (Cloudflare code 2016) during domain setup even when
CF_TOKEN can edit zone routing. Gate ensureEmailRoutingCatchAllToWorker
with isNodeRuntime; Workers still bind catch-all as before.
Renaming any personal mailbox renamed all of them. The account profile and
every personal mailbox shared one identity: syncPersonalIdentity wrote the
profile name and avatar to every row matching (userId, type = 'personal'),
PATCH /api/mailboxes/[id] redirected a rename into that sync instead of the
mailbox row, and both the list response and the client overrode whatever was
stored with the account name. Outgoing mail and message lists did the same,
so a secondary mailbox also sent under the account owner's name.
Only the primary mailbox — the one at the account address — carries the
account identity now. Every other personal mailbox keeps the name and avatar
it was given. The rule lives in one place, lib/profile/identity-utils.ts, and
the API routes, the sender, and the client all go through it.
Existing secondary mailboxes keep whatever name the old blanket update left
on them until they are renamed; the rename now sticks.
Fixes#36
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>