Commit Graph
1376 Commits
Author SHA1 Message Date
Linus Rath e738941950 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-08-01 12:28:21 +02:00
Linus Rath 1890cade08 fix: surface underlying network error cause in JMAP passthrough failures 2026-08-01 11:47:16 +02:00
Linus RathandGitHub fc7fec44a8 Merge pull request #714 from MathyV/respect-max-calls
fix: make bulwark respect server limits
2026-07-31 05:43:26 +02:00
Mathy Vanvoorden 1652a0ec62 fix: make bulwark respect server limits
If you have a large number of tags, getTagCounts would not be able to get the
unread counts because it did not respect maxCallsInRequest, even though the
value was actually read out, it was just ignored. There are also other places
where the limits were not respected.

Batching is now generalized in a helper that also takes maxObjectsInSet, which
also was ignored, into account and is applied to all functions.

In addition, the dev mock now also advertises and enforces the limits, so these
issues can get picked up during development.

Possible closes #699
Possibly closes #399
2026-07-30 22:11:40 +02:00
Linus Rath e659fe3d38 feat: allow contact cards for organizations #701 2026-07-30 19:44:36 +02:00
Linus Rath 348e032dce fix: files show creation date instead of modification date #700 2026-07-30 19:41:02 +02:00
Linus Rath a6c15b8ad6 fix: empty folder stopped after 500 emails #711 2026-07-30 19:40:25 +02:00
Linus Rath 5a2bc6b671 Merge branch 'main' of https://github.com/bulwarkmail/webmail
# Conflicts:
#	app/api/dev-jmap/[...path]/route.ts
2026-07-30 19:17:43 +02:00
Linus Rath 59bc7fd64c fix: reply to own thread message addresses original recipients #703 2026-07-30 19:02:39 +02:00
Linus Rath aa7a814b86 test: rewrite mock email content 2026-07-30 18:44:53 +02:00
Linus RathandGitHub c5c6509867 Merge pull request #710 from MathyV/nested-tags
Nested tags
2026-07-30 18:41:22 +02:00
Linus Rath 1cdbf75270 fix: use full tag path in drag-drop toasts, fresh email in context menu markAsRead
Nested tag toasts from drag-and-drop only showed the leaf name for
non-root tags, contradicting the comment above it and making two
same-named leaves under different parents (e.g. Personal/Receipts vs
Work/Receipts) indistinguishable in the toast.

The context menu's markAsRead handler was the one action left reading
the stale contextMenu.data instead of the live-refreshed
contextMenuEmail introduced alongside it, so it could act on outdated
email state while every sibling handler was already updated.
2026-07-30 18:39:49 +02:00
Mathy Vanvoorden ea7892b497 feat: Change the dev mode defaults to include nested tags 2026-07-29 19:52:31 +02:00
Mathy Vanvoorden d52dfebad4 Fix Catalan translation warnings 2026-07-29 19:16:31 +02:00
Mathy Vanvoorden 7bb58f4f9f Add translations for new tag functionality 2026-07-29 19:14:28 +02:00
Mathy Vanvoorden f1e1ed1df7 fix: make the tint of selected rows work the same way in dark and light mode 2026-07-29 17:18:42 +02:00
Mathy Vanvoorden d9d9f91a86 feat: Make it easier to handle multiple tags
- Tags can now be removed straight from the email header
- Tagging control now allows the user to (de)select multiple tags in one go
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden 108406a885 feat: improve visualization of tags
Previously tags where very much focused on color coding email and less about
adding additional information. They were also visualized in different ways in
different locations.

This commit gets rid of all "Color-coding" references, aligns visualization of
the tags across the whole project and tries to improve user experience of using
tags in general.

A search box is shown in the tagging control so the user can quickly search for
a tag if they have a huge (more than 10) amount of tags.
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden 013ef7d557 fix: remove unused code
This code is nowhere used, so to prevent extra work during an upcoming refactor
of tags, it is removed and some related tests are now actually made useful
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden 56d11b5759 fix: remove the reset-to-defaults button from tag settings
If you carefully crafted your tags and then click this button by accident, all
your hard work is gone. A confirmation message would be the other solution but
since I have difficulty to grasp when you would need such a button, I propose
to just remove it.
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden 0c1e238223 feat: allow hidden tags, either permanent or when there are no unread messages 2026-07-29 17:18:42 +02:00
Mathy Vanvoorden ca0ba818b7 feat: Add nesting of tags in a tree
- levels are joined by forward slashes in the keywords
- behaviour is opt-in for now
- long paths are shortened if there is not enough display room

Closes #687.
2026-07-29 17:18:38 +02:00
Linus RathandGitHub ce97a54aaa Merge pull request #705 from guisea/fix/forward-as-attachment-filename-privacy
fix: strip from/to names from forward-as-attachment filenames
2026-07-29 17:15:12 +02:00
Linus RathandGitHub b605b6d49d Merge pull request #707 from paulhenry46/prf-secu-fix
fix(plugins): prevent a privileged plugin to get PRF secret of anothe…
2026-07-29 17:14:52 +02:00
Linus RathandGitHub cea3ec0fe6 Merge pull request #709 from MathyV/fix-animations
fix: restore animations and replace tailwind.config.ts
2026-07-29 16:53:46 +02:00
Mathy Vanvoorden f56ca594dc fix: restore animations and replace tailwind.config.ts
- tailwind.config.ts is not actually being used so removed
- moved animate-fade-in to globals.css
- added tw-animate-css package to make animate-in work
- added a definition for animate-shake as it doesn't exist in tw-animate-css
2026-07-29 15:59:45 +02:00
Paulhenry Saux 7969fd09eb fix(plugins): prevent a privileged plugin to get PRF secret of another privileged plugin 2026-07-29 11:30:12 +02:00
Aaron Guise 3108b2f336 fix: avoid leaving TZ="undefined" when restoring an unset timezone
process.env coerces assigned values to strings, so
process.env.TZ = originalTZ left the literal string "undefined"
behind (instead of clearing TZ) when it was unset before the test
ran. Delete the var in that case instead of assigning undefined.
2026-07-29 13:31:56 +12:00
Aaron Guise 71565e9328 test: restore TZ after pinning it in forward-as-attachment tests
Setting process.env.TZ at module scope without restoring it could leak
into other test files sharing the same Vitest worker. Match the
beforeAll/afterAll restore pattern already used in
lib/__tests__/calendar-utils.test.ts.
2026-07-29 11:47:42 +12:00
Aaron Guise edd11ac27b fix: strip from/to names from forward-as-attachment filenames
buildForwardAsAttachmentPayload named the synthetic .eml attachment
using the user's own emailDownloadTemplate, which by default embeds
sender/recipient display names. Since this attachment can go to an
external recipient (e.g. an upstream spam gateway, or anyone else),
the filename now always renders as "{date}-{subject}.eml" regardless
of the user's configured template, while still honoring their
space/case/diacritics preferences.
2026-07-29 11:41:34 +12:00
Linus RathandGitHub f81b02ead9 Merge pull request #697 from shukiv/fix-impersonation-stale-account
fix(impersonation): reconcile stale account chip after handoff
2026-07-27 17:06:15 +02:00
Linus RathandGitHub 25e542867b Merge pull request #698 from guisea/feature/forward-as-attachment
feat: add "Forward as attachment" next to Export as .eml
2026-07-27 17:04:21 +02:00
Aaron Guise 0a1114f710 fix: fall back to a real tab title for subject-less Pro forward-as-attachment
buildForwardAsAttachmentPayload intentionally returns an empty subject
for a subject-less email (matching normal Forward's composer-subject
behavior, fixed in 3bfa73f3), but this handler was reusing that same
empty string as the Pro compose tab's title. handleForward, right
above it, already computes its title with a fallback
(email.subject || t('email_composer.new_message')) before prefixing -
mirror that instead of reusing payload.subject for the title.

Caught by GitHub Copilot's automated PR review.
2026-07-27 20:38:02 +12:00
Aaron Guise 3bfa73f375 fix: leave subject blank (not "Fwd:") for a subject-less message
buildForwardAsAttachmentPayload called buildForwardSubject(email.subject,
forwardPrefix) unconditionally, and buildForwardSubject("", prefix)
returns just the bare prefix rather than "". Normal Forward doesn't do
this - EmailComposer's getInitialSubject() returns "" outright when
!replyTo?.subject, only calling buildForwardSubject when there's an
actual subject to prefix. So forwarding a subject-less message as an
attachment produced "Fwd:" as the subject, while normal Forward left
it blank.

Only call buildForwardSubject when email.subject is truthy, matching
getInitialSubject()'s behavior exactly. Add a test.

Caught by GitHub Copilot's automated PR review.
2026-07-27 17:42:33 +12:00
Aaron Guise fc50e5b569 fix: wire "Forward as attachment" into Pro's popped-out email tab
The earlier fix (f3b68194) addressed the Pro/embedded composer-hoisting
path (composing FROM the Mail tab, then getting hoisted into a Pro
tab), but missed a second, entirely separate render path: viewing an
email that's already been popped into its own Pro tab
(components/pro/pro-email-tab-body.tsx). That component renders its
own <EmailViewer> with its own self-contained handleForward - it
fetches its own `email` and opens compose tabs directly via
useProTabStore, with no dependency on page.tsx's pendingDraft/
selectedEmail plumbing at all - so it never had onForwardAsAttachment
wired in the first place. The overflow menu there just silently had
no such item, since EmailViewer only renders it when the prop is
provided.

Add handleForwardAsAttachment here, mirroring handleForward but using
the shared buildForwardAsAttachmentPayload helper, with the same
filename-options handling as the page.tsx fix (7a483b3d/a34314ce). No
stale-closure risk here (unlike the list context menu fix) - `email`
is this component's own local per-tab state, not a global selection
being mutated synchronously before the call.
2026-07-27 17:33:53 +12:00
Aaron Guise a34314cef5 fix: pass email explicitly to handleForwardAsAttachment from the list
The list context-menu wiring called selectEmail(email) then invoked
handleForwardAsAttachment() synchronously in the same tick. Since
handleForwardAsAttachment read selectedEmail from its own closure, and
Zustand's store update doesn't propagate into this render's closure
until the next render, this could forward the previously selected
message (or no-op if nothing was selected yet) instead of the row the
user actually right-clicked.

Parameterize handleForwardAsAttachment to accept an explicit `email`
(defaulting to selectedEmail), the same pattern handleDelete already
uses in this file for the same class of problem, and pass it
explicitly from the list context-menu wiring. The EmailViewer overflow
menu's wiring is unaffected - it always operates on the single
currently-open email via the default parameter.

Caught by GitHub Copilot's automated PR review.
2026-07-27 17:13:53 +12:00
Aaron Guise 0563e88eb8 fix: hide "Forward as attachment" in overflow menu when blobId missing
The overflow menu ("...") showed "Forward as attachment" whenever the
handler was provided, regardless of whether the open email has a
blobId. If it doesn't, handleForwardAsAttachment immediately no-ops
(buildForwardAsAttachmentPayload returns null), so the item was
clickable but did nothing - inconsistent with the list context menu's
version, which is already disabled in that case
(!onForwardAsAttachment || !email.blobId).

Gate both occurrences (desktop and mobile layouts) on email?.blobId
too, matching the context menu's behavior.

Caught by GitHub Copilot's automated PR review.
2026-07-27 16:52:54 +12:00
Aaron Guise 7a483b3dd4 fix: honor the user's filename template for forward-as-attachment
buildForwardAsAttachmentPayload called emailExportFilename(email) with
no options, always using the default naming template regardless of
the user's configured emailDownloadTemplate (and space/case/diacritics
transforms) - the same settings the neighboring "Export as .eml"
action already respects. That could produce inconsistent .eml
filenames between the two actions for the same message.

Accept an optional EmailFilenameOptions parameter and pass it through.
handleForwardAsAttachment now reads the same settings
email-viewer.tsx's emailFilenameOptions useMemo does, via
useSettingsStore.getState() (a one-off read inside an event handler,
matching this file's existing pattern, rather than a new reactive
subscription). Add a unit test covering a custom template.

Caught by GitHub Copilot's automated PR review.
2026-07-27 16:45:53 +12:00
Aaron Guise 351f2d4e26 feat: add "Forward as attachment" to the message list context menu
Adds the same "Forward as attachment" action from the message viewer's
overflow menu to the right-click context menu on a message row in the
list, right after "Forward". Reuses the handleForwardAsAttachment
handler and buildForwardAsAttachmentPayload helper introduced earlier
in this PR - no new logic, just threading the prop down EmailList ->
EmailContextMenu the same way onForward already is.

blobId is already present on list-row emails (EMAIL_LIST_PROPERTIES
includes it specifically for the existing drag-out-to-filesystem .eml
export feature), so this works without any additional fetch. The menu
item is disabled if it's ever missing, matching how other actions
degrade when their handler prop isn't supplied.

Reuses the email_viewer.forward_as_attachment translation key already
added (via a second scoped useTranslations("email_viewer") call,
matching this file's existing cross-namespace pattern for
email_viewer.color_tag) rather than adding a duplicate key under
context_menu - avoids touching all 24 locale files again.
2026-07-27 16:37:04 +12:00
Aaron Guise f3b6819463 fix: honor pendingDraft.replyTo in the Pro embedded composer hoist
The Pro/embedded composer-hoisting effect built its own `replyTo`
straight from `selectedEmail`, unconditionally, ignoring
`pendingDraft.replyTo`. That meant intent set by the opener - e.g.
handleForwardAsAttachment's synthetic message/rfc822 attachment -
would silently get dropped when the composer is hoisted into a Pro
tab, falling back to a normal quoted forward instead. Mirror the same
precedence the non-embedded render path already uses just below
(`pendingDraft.replyTo` wins when set).

Caught by GitHub Copilot's automated PR review.
2026-07-27 16:18:47 +12:00
Aaron Guise 3ea22161d9 feat: add "Forward as attachment" next to Export as .eml
Adds a "Forward as attachment" action to the message overflow menu
(desktop and mobile), right beside the existing "Export as .eml"
action. Opens a new forward-mode compose window with the original
message attached as a message/rfc822 file instead of quoted inline -
useful for reporting spam/phishing to an upstream gateway that expects
the raw original as an attachment (the primary motivating use case:
gateways like MxGuarddog require complete original headers, including
the full mail path, for scanning), or for preserving a message's exact
formatting/headers when forwarding.

Implementation reuses the composer's existing attachment-carry-forward
mechanism (the `attachments` useState initializer in
email-composer.tsx already carries a forwarded message's own
attachments into the new compose via `replyTo.attachments`) - this
just adds one synthetic entry representing the whole original message,
referenced by its existing blobId. No re-fetch or re-upload needed,
since JMAP blobs are account-scoped rather than per-email. The inline
quote-header step (prepareComposerQuoteHeader) is skipped, so the body
starts blank instead of quoting the original.

The core "build subject + attachment entry" logic is extracted into a
pure, unit-tested helper (lib/forward-as-attachment.ts) rather than
left inline in the already-large page component.

Adds the forward_as_attachment locale key to all 24 locales (English
text as a placeholder pending translation, following the existing
add-a-key convention) to satisfy the translations completeness test.
2026-07-27 15:31:52 +12:00
shukiv 246df49c03 fix(impersonation): reconcile stale persisted account chip after handoff
After a master-user impersonation handoff (GET /api/auth/impersonate) the server
swaps the slot-0 session cookie but the client's persisted account registry
(account-registry / auth-storage in localStorage) still lists the previous
account, so the top-left account chip keeps showing the old mailbox until a
manual sign-out. Redirect impersonation to /?impersonated=1 and add a headless
ImpersonationReconciler that drops the stale persisted account/auth state (and
server-derived caches) then reloads to a clean URL, so the app rehydrates empty
and re-derives the single account from the fresh session. Cookies untouched, so
the just-granted session survives. Runs exactly once.

Reported downstream: shukiv/jabali-panel#646.
2026-07-27 05:20:53 +03:00
Linus Rath 9c04950a94 docs: use sentence case for headings 2026-07-25 17:46:48 +02:00
Linus Rath 934967b9df docs: document remaining env vars in env templates 2026-07-25 17:46:12 +02:00
Linus Rath 755201c92a docs: fix facts and rewrite tone 2026-07-25 17:38:55 +02:00
Linus RathandGitHub 9b69d89dfd Merge pull request #681 from marc0s/feature/catalan-translation
feat: add Catalan translation
2026-07-24 21:55:42 +02:00
Linus RathandGitHub 17b69e68f3 Merge pull request #673 from dealerweb/i18n/editor-toolbar
i18n: localize the editor toolbar in all 23 locales
2026-07-24 21:55:01 +02:00
Linus RathandGitHub 83cd675ccf Merge pull request #680 from hildebrandttk/fix/cross-account-move
Fix/cross account move
2026-07-24 21:54:25 +02:00
Linus RathandGitHub ddcab88e56 Merge pull request #686 from hildebrandttk/feat/unified-mailbox-always-available
feat(settings): always show the Unified Mailbox switch in Layout sett…
2026-07-24 21:53:40 +02:00
Stefan Hildebrandt f188e29152 feat(settings): always show the Unified Mailbox switch in Layout settings
Drop the `accounts.length > 1 || hasGroupInboxes` gate that hid the
Unified Mailbox toggle for single-account users with no visible shared
folder. The admin `isSettingHidden('enableUnifiedMailbox')` policy gate
is preserved, so admins can still hide it.
2026-07-24 19:39:01 +02:00