Commit Graph
318 Commits
Author SHA1 Message Date
Nathan DaeilaandHieu Nguyen f0fddecd2e Use the full width for message bodies on mobile (#112)
* Use the full width for message bodies on mobile

The reader indents the subject, sender row and body by the avatar column
(pl-12/pl-16) at every width, which leaves a 64px gutter on a phone. Below
the sm breakpoint the subject, body, attachments and the scheduled-send
banner's wrapper now start at the page edge; wider screens are unchanged.

* improve mobile layout for email content

---------

Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com>
2026-10-13 14:09:56 +07:00
Hieu Nguyen (Jack) b24535647c ignore auth for new db 2026-10-13 12:38:15 +07:00
sean0407andSean ac5546d53a fix: show self-hosted update guidance (#107)
Co-authored-by: Sean <sean@SeandeMac-mini-2.local>
2026-10-13 09:53:36 +07:00
Hieu Nguyen (Jack) 2e24bfd3b2 Improve read state 2026-10-11 16:23:47 +07:00
Namindu Sanchila Nadeeshan d7795ac632 Add Sinhala interface language (#110) 2026-10-11 16:09:55 +07:00
Hieu Nguyen (Jack) eb9b79e49e pump version 2026-10-11 02:55:07 +07:00
Hieu Nguyen (Jack) b8e2fbe3cc Add migration check 2026-10-11 02:55:02 +07:00
Hieu Nguyen (Jack) e7df653f39 Improve email color in dark/light mode 2026-10-11 02:50:58 +07:00
Namindu Sanchila NadeeshanandClaude Opus 5.5 af266d9533 Serve attachments and Drive files inline only when their type cannot run script (#103)
Message attachments honoured the sender's inline disposition for any type, and
the Drive and preview checks compared raw Content-Type strings, so
image/svg+xml;charset=utf-8 or image/png, text/html passed. The per-route CSP
sandbox was the only guard, and on the Node runtime Next.js drops it in favour of
the global CSP from next.config, which allows inline script.

Parse the MIME essence, allowlist inline types, and send back only the parsed
type.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:44:16 +07:00
960dda4656 Raise the contrast of secondary text to WCAG AA (#95)
Secondary text was mostly text-neutral-400 or neutral-500 on the tinted sidebar background, which measured 2.4:1 and 4.45:1 against the 4.5:1 AA minimum. In the dark theme neutral-400 on the card surfaces measured 3.5 to 3.9:1.

Light theme: neutral-500 goes from #737373 to #6b6b6b, which clears 4.5:1 on the sidebar and selected-row backgrounds, and text that used neutral-400 (11 and 12 px captions, timestamps, helper lines, key hints, chart labels, empty-state lines) now uses neutral-500. Placeholders, decorative icons and disabled text are left alone. Dark theme: neutral-400 becomes #9aa0a6 and neutral-500 #a8aeb4. The active command palette item used text-blue-100, which the dark theme remaps to a dark blue (1.75:1 on the blue background); it is white now. The success badge uses green-800 instead of green-600 on its tint.

A few informative icons (close buttons, the recipient chip remove button, the shared-folder marker) go from neutral-400 to neutral-500 as well, for the 3:1 non-text minimum.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 02:41:22 +07:00
b50031bd35 Give pages a title, one h1 and unique landmarks (#94)
* Give pages a title, one h1 and unique landmarks

Settings, admin, calendar, booking, drive and compose pages kept the title of whatever page was shown before, because only the mail folders updated document.title. A small RouteTitle component, mounted in each layout, now sets "<page> - <app name>" from a table of the existing navigation labels. The sign-in, forgot-password and reset-password pages get their own titles from generateMetadata.

Headings: the folder list on the desktop layout had no h1 (it was only rendered on narrow screens), and the admin pages hid their h1 with display: none on phones in favour of an h2 outside the main landmark. The list now always has a heading (an h2 when a message with its own h1 is open), the visible title next to the toolbar is decorative, admin pages keep their h1 on phones through sr-only, the mobile top bar title is decorative, and the calendar, account, inbox settings, import, export and rules pages get a screen-reader-only h1. The admin overview cards use h2 so the heading levels no longer skip.

Landmarks: the sidebar nav is named after the app and the settings navs after their sections, so they can be told apart. The admin page chrome (license badge and account menu) is a header instead of a bare span, so it is inside a landmark.

* Test the route title lookup

Covers the longest-prefix match and the routes that keep their own title.

---------

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 02:37:59 +07:00
Can K.andCan Kirca 08316b5727 Announce errors and status messages and name the controls that had no name (#93)
Error and status messages were mounted as plain paragraphs, so a screen reader said nothing when sign-in failed, a form was saved or a request was rejected. Error lines now have role="alert" and neutral status lines role="status" across the settings, admin, auth, contact and message dialogs. On the sign-in form the error is also tied to the email and password fields with aria-invalid and aria-describedby. The compose toast is an alert for errors and a status otherwise, and new-mail notifications are announced through a persistent status region.

The loading overlay put aria-label on a plain div, which is ignored, and left its icon image readable after loading. It is now a status region that says "Loading" only while it is visible, and the logo is decorative.

Names and labels: the "open booking page" button contained a link and had no name (now a single link with a name), the import file input was not tied to its label, the AWS and Resend key fields only had placeholders, the QR code has a text alternative, the quoted-text toggles in the reader and composer are named (the reader's was hard-coded English), the composer link field has a label, the "Move to" button no longer overrides its visible text with a different name, and the snooze presets report which one is selected. The drive sort buttons no longer carry aria-sort, which is not allowed on a button, and its rows no longer claim role="row" outside a grid.

Identity fields in the profile, forwarding and sign-up forms get autocomplete tokens, and the domain field no longer says it is a URL.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:35:24 +07:00
65df2554ec Give dialogs, menus and the composer proper focus handling (#92)
The command palette and the shortcuts help were plain divs: no dialog role, no label, focus stayed on the page behind them, Tab walked out, and closing did not return focus. The palette now is a labelled modal dialog with a combobox input, a listbox of options and aria-activedescendant, and keeps the highlighted option in view. The help dialog gets role="dialog", a label from its heading, focus on its close button, Escape and "?" to close, and a Tab trap. Both give focus back to what opened them. The shared logic is in use-dialog-focus.ts.

The "More actions" menus on a message and on a conversation card, and the account menu, closed only on an outside click. Escape now closes them and returns focus to their button, and moving focus out of them closes them too (use-dismissible.ts).

The composer did not take focus when opened and did not give it back. A new message now focuses the recipients field, an opened draft focuses the body, closing returns focus to the button that opened it, and the floating composer is a labelled region.

Tooltips blurred whatever you had activated, so keyboard users lost their place after every icon button. They no longer do, and Escape dismisses them.

Single-key shortcuts no longer fire while focus is inside a dialog, menu or listbox.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 02:35:05 +07:00
1504333795 Make message rows reachable by keyboard and add a skip link (#91)
The link that opens a message in the wide list used display: contents, so it had no box and could not take focus. Tab went through the checkbox, star and row buttons of every message but never to the message itself, which left keyboard users unable to open mail. The star button also sat inside that link, which is nested interactive content.

The link now lives on the subject and stretches over the row with a pseudo-element, so clicking anywhere on the row still opens the message. The checkbox, star and hover actions sit above it. The focus ring is drawn around the whole row, because the subject's own outline was clipped by truncate. The rows also get list and listitem roles, the open message is marked aria-current, unread rows say so to screen readers, and the checkbox is named after the sender like in the compact layout. The star keeps one name and reports its state with aria-pressed.

Every layout gets a "Skip to main content" link as its first tab stop, pointing at the main landmark. It stays hidden until it is focused. The two new strings are in all catalogs.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 02:30:32 +07:00
Can K.andCan Kirca f10093dcf5 Make "Move to Inbox" work from Snoozed and custom folders (#90)
Moving a snoozed message to the Inbox only set its status to received, which it already was, and left snoozedUntil alone. The Snoozed list keeps showing messages whose snooze has not run out, and the Inbox hides them, so nothing visible happened. Moving to the Inbox now clears the snooze.

The toolbar also left "Inbox" out of the move menu whenever it did not know the folder, which is always the case in a custom folder, so mail could not be moved back to the Inbox from there. It is only hidden in the Inbox itself now.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:27:31 +07:00
Can K.andCan Kirca c105dedd43 Make the "g" navigation shortcuts match reliably (#89)
useHotkeys pushed every keydown into the sequence buffer and only matched when the whole buffer equalled a shortcut. Two things broke that: the Shift keydown of "g" then "!" put "shift" in the buffer (so "g !", go to Spam, never matched), and any key pressed within 800 ms before a sequence (for example "j" and then "g i") left a stale entry in front of it.

Modifier keys no longer enter the buffer, and a sequence now matches when it ends the buffer. A single key still only matches as the whole buffer, so lone shortcuts behave as before. The matching moved to use-hotkeys-utils.ts with a small test.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:26:08 +07:00
Can K.andCan Kirca 635ed0ca3b Tell the user when a list action fails (#88)
Archive, trash, spam, move and delete from the message list did nothing visible when the request failed: the toolbar buttons and the row buttons let the error go unhandled, so the message simply stayed where it was. The list now shows a dismissing alert with the existing "Could not update message" text, for both the bulk toolbar and the row actions.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:25:50 +07:00
Can K.andCan Kirca e938c0f0ec Drop removed messages from the selection and step back from an empty page (#87)
Actions on a single row (the hover buttons and swipe) took the message out of the list but left it in the selection. The toolbar kept saying "1 selected" while nothing was ticked, and a bulk action then ran on a message that was no longer there, for example moving a message you had just trashed to Archive. The selection is now trimmed to the rows that are still in the list.

Archiving or trashing every row on the last page left the list on an offset past the end: it showed the empty-folder text while Previous page was the only way back. When a page other than the first comes back empty, the list now goes to the last page that has messages.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:25:26 +07:00
dffcc2394e Keep the composer usable when a request fails, and keep what was typed (#86)
* Close the composer after a message is sent

After a successful send the composer kept its window open, emptied, and the "Message sent" toast lived inside it. The popup now closes (the full-page composer goes to Sent) and the confirmation is shown by FloatingComposer through the compose context, so it stays visible once the composer is gone. It is a status region so screen readers announce it.

Sending also cancels the pending autosave and invalidates in-flight ones. Before, a save that fired during the send could recreate the message as a draft, which then sat in Drafts next to the sent copy.

* Keep the composer usable when a request fails, and keep what was typed

Sending while offline, or getting a non-JSON error page back, threw before the busy flag was cleared. The button stayed on "Preparing…" and disabled, with no message. A failed request now shows the send error and frees the form.

Discarding had the same problem: the delete button stayed disabled forever after a network error, and a draft that was already gone (404) could not be discarded from the full-page composer. A 404 now counts as deleted.

The draft is saved 900 ms after you stop typing, and closing the composer or opening another one within that time dropped the pending save, so a message typed and closed quickly was lost. The pending save now runs when the composer unmounts. Send and discard still cancel it.

---------

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com>
2026-10-11 02:24:18 +07:00
Can K.andCan Kirca 4fedfe7388 Fix the star button on conversation cards (#85)
toggleMessageStar returns { starred }, but the card put that whole object into its boolean state. The object is always truthy, so the first click filled the star and every click after that kept it filled until the page was reloaded. Use the starred flag instead. This also clears the type error tsc reported on that line.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:19:56 +07:00
Can K.andCan Kirca e4c4dd5b97 Stay in the current folder after spam, trash and archive (#84)
Marking a message as spam, moving it to Trash or archiving it from the reading view took you to the Spam, Trash or Archive folder. You now go back to the list you opened the message from, so you can keep working through it. The same goes for blocking a sender, which always jumped to Trash.

getMessageActionRedirect takes the list's href instead of a direction, and the message actions read it from the list visibility context the folder pages already provide, so custom folders work too.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:18:28 +07:00
Can K.andCan Kirca fed60c1b18 Close the composer after a message is sent (#83)
After a successful send the composer kept its window open, emptied, and the "Message sent" toast lived inside it. The popup now closes (the full-page composer goes to Sent) and the confirmation is shown by FloatingComposer through the compose context, so it stays visible once the composer is gone. It is a status region so screen readers announce it.

Sending also cancels the pending autosave and invalidates in-flight ones. Before, a save that fired during the send could recreate the message as a draft, which then sat in Drafts next to the sent copy.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:08:08 +07:00
Can K.andCan Kirca 21817cedcb Bring the tests back in line with the code (#82)
* Mark the fixture admin as primary admin in the account tests

Creating accounts needs a primary admin or an admin with user management rights since migration 0050, but the fixture inserted its admin without is_primary_admin, so every create call got a 403.

* Let the agent tool test call mutating tools without chat approval

In chat, mark_email_read, move_email and discard_draft now return pending_approval instead of acting. The test still expected the change to happen right away. It now checks that chat only asks, and runs the direct calls with the mcp origin.

* Test the migrated schema instead of the removed bootstrap SQL

The old test read MIGRATION_NAMES and INITIAL_SCHEMA_SQL out of src/lib/setup/migration.ts, which now just runs the migration files. The replacement applies drizzle/migrations to an empty database, checks that every file is recorded and not re-applied, and keeps the mailbox, license and spam table checks.

* Expect the shortened pt-BR inbox label in the i18n tests

6978c01 changed navigation.inbox from "Caixa de entrada" to "Entrada" in pt-BR, but four tests still expected the old label.

---------

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:06:49 +07:00
Can K.andCan Kirca 8ee28cfd04 Render the branded page title on the server (#81)
* Render the branded page title on the server

The root layout sent a fixed "Mailflare" title and the browser swapped in the configured app name after the page loaded. Screen readers announced both. The title now comes from the branding settings in the first response, and the branding is passed to the provider as its initial value so the name and the icon do not flash their defaults first.

The inbox page also kept its own "Inbox (n)" title after leaving it, so the sign-in page after a logout still read as the inbox. It now puts the previous title back when it unmounts.

* Stub the branding lookup in the layout i18n test

The root layout now reads the app name from the server, which pulls in the Workers runtime. The i18n test bundles the layout with stubs for its server-only imports, so add one for the branding helper too.

---------

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
2026-10-11 02:06:28 +07:00
Hieu Nguyen (Jack) ca41c56a07 pump version 2026-10-10 17:28:19 +07:00
Hieu Nguyen (Jack) 5635ab0f48 Merge branch 'main' of https://github.com/hieunc229/mailflare 2026-10-10 15:41:50 +07:00
Hieu Nguyen (Jack) 4b31136ac7 Add scheduled tab #97 2026-10-10 15:41:48 +07:00
Hieu Nguyen (Jack) a5c2efabff improve drafts view on mobile 2026-10-10 15:21:56 +07:00
yi111 f90de963d6 Show the IAM policy when a save is refused for missing SES permissions (#98)
Saving credentials that validate against AWS but carry no SES permissions
answers 400 with "Attach the policy shown below", yet the panel rendered
nothing below it. Two causes: the 400 body carried the report but not the
policy, and requestJson discarded the parsed body on a rejected response,
so the report never reached the panel. The block was also gated on
`configured && !editing`, which is exactly the state a refused save leaves.

The 400 now carries the policy, requestJson throws an ApiError that keeps
the parsed body, and the panel keeps a refused report so the missing
permissions and the policy render under the message, expanded. A later
successful save, re-check, replace or remove clears it.

Refs #79
2026-10-10 07:50:28 +07:00
Hieu Nguyen (Jack) 83ade981e5 Add support for AWS S3, BackBlaze B2 as storage 2026-10-09 17:07:33 +07:00
Hieu Nguyen (Jack) 3c90a03171 update Drive layout 2026-10-09 16:04:19 +07:00
Hieu Nguyen (Jack) 9f66e13560 Added Drive 2026-10-08 21:52:49 +07:00
Hieu Nguyen (Jack) 850661ae3e update localization to reduce sizes 2026-10-08 21:52:35 +07:00
Hieu Nguyen (Jack) 6978c01de1 update languages 2026-10-07 17:09:03 +07:00
Hieu Nguyen (Jack) 0a05fb565e Merge pull request #57 from wpfyorg/feat/pwa-web-push
feat: add PWA and Web Push notifications
2026-10-07 16:28:15 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 c8795ae39e Merge main into feat/pwa-web-push
Resolve conflicts in favor of main; re-apply Web Push on top.
Renumber push_subscriptions migration 0032 -> 0054 (0032 now taken).
Skip push for agent_draft notifications.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 16:27:13 +07:00
Hieu Nguyen (Jack) 9816a61b3c support more languages 2026-10-07 16:15:13 +07:00
Hieu Nguyen (Jack) d8664ad802 Merge pull request #67 from forge-z/feat/pt-br-navigation
Add localization
2026-10-07 13:45:35 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 0d561f1b25 Move language and time zone to account settings, add README language docs
Add a General section to Settings > Account holding language and time zone,
render the homepage language selector as a compact text label sized to its
value, drop the sidebar language field, and document localization.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:44:01 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 5ac0931101 Add Urdu interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:43:45 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 d7ae8f77d1 Move language and time zone to account settings, add README language docs
Add a General section to Settings > Account holding language and time zone,
render the homepage language selector as a compact text label sized to its
value, drop the sidebar language field, and document localization.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:43:41 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 149eb85bd5 Add Bengali interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:39:40 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 ae5d9badc5 Add Arabic interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:35:04 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 2d2c1507a8 Add Hindi interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:30:24 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 634614bb70 Add Vietnamese interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:26:43 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 4fc5689cba Add Turkish interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:23:42 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 303c9e7548 Add Indonesian interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:20:27 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 4919d82e5f Add Japanese interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:17:46 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 40e5899ae8 Add Simplified Chinese interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:15:09 +07:00
Hieu Nguyen (Jack)andClaude Sonnet 5.5 abfb8e3653 Add Russian interface language
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:12:35 +07:00