603 Commits

Author SHA1 Message Date
rustmailer
49ad2ae769 update 2026-10-31 13:58:42 +08:00
rustmailer
d7cdae19f2 feat(contacts): include sender display names in contact suggestions
/all-contacts now returns [{ email, name? }], so pickers can show and match the display name; filter values stay addresses. The mailbox avatar takes its initials from from_name.
2026-10-30 02:28:37 +08:00
rustmailer
5d66c8c87d fix(web): stop the search page from scrolling
Header, toolbar, table and pagination exceeded 100vh. Tighten spacing and size the table (and loading skeleton) to 100vh-18rem.
2026-10-30 02:28:33 +08:00
rustmailer
537834fd33 fix(i18n): translate sender header override copy into all 18 locales
Fill the four keys missing outside en/zh/zh-tw and reword the text: changes apply to new mail, existing mail needs an index rebuild.
2026-10-30 02:28:29 +08:00
root
2c2d0c06a3 fix(web): stop calling the Pro-only branding API from community and silence empty-dialog warnings 2026-10-30 00:23:22 +08:00
root
5abbef0cbb feat(account): resolve sender from configured headers (#386)
Accounts may now list ordered headers whose first valid mailbox replaces the stored `from` address at ingest. The real From header remains the fallback, and the original address is kept in `wire_from` whenever the rule actually changed it.

- store the sender display name in `from_name` and index both new fields into `from_text`/`from_text_tri`, so sender search and the dashboard use the resolved address
- append both fields to the email and attachment schemas, bump the index format to 4, and re-derive them from the raw message during reindex using the account's current rule list
- expose the rule on account create/update (advanced settings, collapsed by default) and render `Name <address>` plus a `via` hint in the web views
- archive rules keep matching the real From address; forged headers are deliberately not verified
2026-10-30 00:16:25 +08:00
rustmailer
b9197c06ec fix(docker): reap healthcheck zombies with tini as PID 1 (#387)
bichon-server runs as PID 1 without an init shim, so a healthcheck process
reparented to it (the shell is killed when a check times out, curl keeps
running) is never waited for and stays a zombie.

Install tini and make it the entrypoint so orphans are reaped and SIGTERM is
forwarded. Bound curl with --max-time 4 so a stalled check exits before
Docker's 5s timeout instead of being orphaned.
2026-10-29 15:05:44 +08:00
rustmailer
e6f7b9c898 style(web): tighten spacing between sidebar sections
Reduce the gap and vertical padding of sidebar groups so consecutive nav sections sit closer together.
2026-10-29 14:51:25 +08:00
rustmailer
dcff4e43c0 fix(web): validate the backup schedule cron before saving
Reject a malformed six-field expression in the form instead of sending it and showing the server's error.
2026-10-29 14:51:24 +08:00
rustmailer
36808ac6b4 i18n: backfill metrics-only token and message-view translations
These keys already existed in en.json; add them to the other 17 locales:
token filters and the metrics-only token labels, plus the message view's
show/hide details and search-entry labels.
2026-10-29 14:51:20 +08:00
rustmailer
64e29bf4ed fix(web): mailbox view sizing and folder delete button
- widen the folder tree and its labels so long names stay readable
- use a plain button for the row delete control: the Button svg sizing rule inflated the icon
- simplify the mailboxView message count key to a single pluralised string
2026-10-29 14:50:58 +08:00
rustmailer
5bceb87ce5 feat: report email-data and index sizes on the dashboard
- count the blob store and the search index separately, and honour a custom index directory
- expose storage_stats_available so callers that cannot see every account are told the figures are unavailable instead of seeing a partial total
- relabel the storage figure as email data size and explain the ratio
2026-10-29 14:50:39 +08:00
rustmailer
b0f1447751 feat: source-mailbox cleanup for IMAP accounts (#24)
- move archived mail into bichon_auto_created_trash, manually or on a schedule
- MOVE (RFC 6851) is the only move path; Bichon never issues EXPUNGE, and a server without MOVE is reported as unsupported
- metadata-only verification before every move; the MOVE capability is checked per connection, case-insensitively
- fill each batch with messages that can actually move; skips no longer shrink it
- scheduled runs drain batches under a 10-minute budget
- jobs are listed/polled/cancelled over the API and reattached after a reload
- reports keep the last 30 runs and show moved/failed only
- WebUI dialogs and 18 locales
2026-10-29 14:50:25 +08:00
rustmailer
010e7dbd8e gAP-Fill: apply archive rules before fetching bodies
Skip candidates rejected by header-only pre-filtering and surface
filtered counts in state, API and UI. Also fix cancellation finalization, advance highest_uid when every candidate is filtered, make partial-batch accounting consistent, and merge the duplicated progress/filtered state writes.
2026-10-26 12:57:39 +08:00
rustmailer
452f52fb37 feat(mailbox): add per-folder mail browser with reading pane 2026-10-22 08:58:46 +08:00
rustmailer
510d69214d feat(metrics): add /api/v1/metrics endpoint with 34 Prometheus metrics #162 2026-10-22 06:05:50 +08:00
rustmailer
7f31a5e95e update 2026-10-22 00:54:37 +08:00
rustmailer
c8e329c850 update 2026-10-21 06:49:49 +08:00
rustmailer
0da1620bf7 fix(search): AND semantics, lossless tag rebuilds, and a safer reindex flow
- Multi-word queries AND by default; legacy saved searches replay with
     `match_all: false` and keep their OR behaviour.
   - Tag edits rebuild documents losslessly: non-stored text, trigram
     siblings and (attachments) re-extracted content, from one implementation shared with the regression test.
   - reindex builds the whole index root aside, validates it, then swaps by renaming the old root; nothing is deleted, unreadable documents go to a 0600 NDJSON
     report. The v1.x migration can run it inline.
   - Index markers self-heal when the schema proves the current format; a custom `bichon_index_dir` is honoured.
   - Search box gains a syntax hint; tests that write the process-global index are serialized (Windows PermissionDenied).
2026-10-21 03:02:18 +08:00
rustmailer
ee92a95a95 fix(search): make the analyzer context-free, and reindex legacy archives 2026-10-20 23:05:36 +08:00
rustmailer
763d68221f bump to v2.2.0 2.2.0 2026-10-20 18:11:45 +08:00
rustmailer
6a18338f70 feat: search by envelope id 2026-10-20 15:55:38 +08:00
rustmailer
a73efe29b1 Merge pull request #379 from myml/feat/untagged-only-filter
feat(search): add an untagged-only filter for messages
2026-10-20 12:04:18 +08:00
wurongjie
d0b0caafa5 feat(search): add an untagged-only filter for messages
The tag filter could only express "has one of these tags": the absence of a
facet value is not a value that can be picked from the tag list. Add
`has_tags` to `EmailSearchFilter`, mirroring `has_attachment`:

- `Some(true)` keeps only tagged messages, via `ExistsQuery` over the tags
  fast field.
- `Some(false)` keeps only messages carrying no tag at all. A `BooleanQuery`
  holding nothing but `MustNot` clauses matches nothing in Tantivy, so the
  complement is built as `AllQuery` minus that existence check.
- `None`, the default, does not filter.

`has_tags = false` next to a non-empty `tags` is a contradiction between two
conditions that can never both hold, so `EmailSearchRequest::validate`
rejects the combination instead of returning an empty page the caller has to
explain.

The search page exposes the new filter as a pinned "No tags" entry in the tag
popover. It stays visible while the tag list is searched, because it is not a
tag value from the index, and it replaces (or is replaced by) the tag
selection, so the two are never sent together.
2026-10-19 22:10:12 +08:00
rustmailer
7f8b3d92fa FEAT(backup): add a local directory target alongside S3 2026-10-19 09:12:17 +08:00
rustmailer
3d8a5161f7 update 2026-10-19 04:38:38 +08:00
rustmailer
2ed7f0e9ea update 2026-10-17 15:11:54 +08:00
rustmailer
a64daa5354 report the client type in OAuth2 config events 2026-10-16 23:55:43 +08:00
rustmailer
a3f83805f6 Merge pull request #383 from jmakr0/feat/public-oauth-client
feat(oauth2): support public clients without a client secret
2026-10-16 21:46:24 +08:00
rustmailer
7eba7d791a update 2026-10-16 21:21:15 +08:00
rustmailer
6042980f7e Merge pull request #382 from Ksisu/feat/use-system-theme
feat(#381): add "use system theme" toggle to theme selectors
2026-10-16 16:46:06 +08:00
jmakr0
00a9efa75e docs: answer how to use an OAuth app without a client secret 2026-10-15 22:59:56 +02:00
jmakr0
ff1069970f feat(web): add a client type selector to the OAuth2 form 2026-10-15 22:59:56 +02:00
jmakr0
fea027db86 test(server): cover switching an OAuth2 config to a public client 2026-10-15 22:59:56 +02:00
jmakr0
cc5d355c23 feat(oauth2): support public clients without a client secret 2026-10-15 22:59:56 +02:00
rustmailer
8cd34bc73e feat: add import for access token and refresh token in frontend page 2026-10-16 00:36:51 +08:00
rustmailer
eb03d18073 feat(repair): verify against the archived hash, and keep a repair history ring 2026-10-15 23:09:54 +08:00
Kristóf Horváth
69b2fa8866 refactor(theme): centralize theme definitions in lib/theme
Theme names, labels, picker swatch colors and the browser meta color were
defined separately in lib/theme.ts, theme-switch.tsx and
appearance-form.tsx, and the two pickers paired light and dark variants by
array index. Adding or reordering a theme meant editing several copies in
lockstep, and a mismatch silently paired the wrong variants.

THEME_FAMILIES in lib/theme.ts is now the single table; Theme, ALL_THEMES
and the family/mode helpers derive from it, and both pickers iterate it, so
a family's variants can no longer be mispaired. The duplicated preview
components move to components/theme-preview.tsx.

Theme names and order are unchanged, so stored user preferences stay
valid; a test pins the list. Tests also check that theme.css has a block
for every theme and styles no unknown theme names, reading class names
from whole selectors.

Unknown theme values no longer produce invalid class names: the stored
theme and the login response's theme are checked with the new isTheme
and fall back to the default, and the helpers fall back to the default
family for anything that still gets through. LoginResult.theme is typed
as a plain string instead of a stale local 'dark' | 'light' union.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVBG38Df8bRPm3n8iDtD8W
2026-10-15 16:43:15 +02:00
Kristóf Horváth
b7fd5873ca feat(#381): add "use system theme" toggle to theme selectors
Users could only pick a fixed light or dark theme, so the UI ignored the
OS/browser color scheme. Add a "Use system theme" switch to Settings ->
Appearance and the header theme popover. When enabled, the chosen color
family is kept and its light/dark variant is resolved from
prefers-color-scheme, updating live when the client setting changes.

While the toggle is on, both pickers show one entry per color family
whose preview is split diagonally (light top-left, dark bottom-right).
Picking one keeps the variant currently shown, and turning the toggle off
stores the variant on screen as the chosen theme, so the look stays the
same.

The flag is stored per browser in localStorage
(vite-ui-theme-follow-system). The theme saved to the user account is
still one of the existing concrete theme names, so there are no backend,
API or data-format changes and it is fully backward compatible.

- lib/theme.ts: pure helpers (themeFamily, themeMode, themeWithMode,
  resolveTheme)
- theme-context: followSystem state, matchMedia listener, resolvedTheme
- theme pickers consume resolvedTheme for the applied theme; Settings ->
  Appearance keeps its form field on the provider's theme, so a pick in
  the header popover is highlighted there and is what Update saves
- folder tree: the item component moves to module level, so a theme
  change (now also an OS switch) no longer remounts every tree item
- i18n: theme.use_system and settings.appearance.description.use_system
  in all 18 locales

Verified with unit tests for the helpers and ThemeProvider (turning the
toggle off, OS changes, listener cleanup) and manually in the browser by
emulating light/dark color schemes.

Closes #381

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVBG38Df8bRPm3n8iDtD8W
2026-10-15 16:43:15 +02:00
rustmailer
721cd9bf14 Merge pull request #371 from MaxRink/codex/blob-repair-20261009-clean
feat(message): safely repair missing archived blobs
2026-10-15 15:43:27 +08:00
rustmailer
af8b591738 update 2026-10-14 15:00:33 +08:00
rustmailer
6afee48cba CHORE(memdb): log WAL replay count at debug level
The replay line was an unconditional eprintln to stderr, so it showed
up in every server log on startup after an unclean shutdown. Route it
through tracing at debug level; the count is only interesting when
diagnosing WAL behavior.
2026-10-14 14:53:03 +08:00
rustmailer
d2c4aa0580 docs(attachments): document the detach placeholder's hash domains
The `<<BICHON_DETACH_HASH:{hash}>>` prefix is unchanged but the hash it
carries switched domains in #375 (raw encoded body, domain-separated)
versus legacy records (decoded content). The literal was duplicated
across core, server export, and the Pro integrity checker, and the
server export doc still described the pre-#375 keying.

Centralize the prefix as core's DETACH_PLACEHOLDER_PREFIX with full
domain documentation, use it at every production site, and fix the
stale export comment. Test literals intentionally keep the raw string
to pin the on-disk format.
2026-10-14 02:40:02 +08:00
rustmailer
4adfb93adb fix(tests): serialize envelope tags test against backup capture windows
update_envelope_tags_guard_fires_for_held_account writes through the
backup write gate (account insert + tag update) but did not take the
BACKUP_STATE_TESTS lock, so while a backup test held its capture window
the insert was rejected with "writes are temporarily paused" — a
roughly one-in-three flake on the full suite. Follows the established
protocol documented on BACKUP_STATE_TESTS in backup/manager.rs.
2026-10-14 02:40:02 +08:00
rustmailer
41a90ae2c6 fix(backup): isolate test temp files per write_temp call
write_temp keyed its directory on pid + file name only, so parallel
tests writing the same name (mem1.txt is used by three tests, s1.txt by
three, mem2.txt/s1b.txt by two) shared one path on disk. One test's
fs::write truncating the file between another test's stat and read
surfaced as a spurious "changed size during capture" upload failure
roughly one full-suite run in three on Windows.

Add a per-call sequence number to the directory name; no test relies on
two write_temp calls resolving to the same path.
2026-10-14 02:31:48 +08:00
rustmailer
104bb15b32 refactor(attachments): share one detach implementation between core and admin
The attachment-detaching logic (range clamping, raw/decoded hashing,
placeholder splicing, metadata) existed as two hand-maintained copies:
detach_and_store_attachments in core (live ingest) and
detach_attachments_standalone in admin (offline store migration). They
had already drifted on the invalid-range branch (empty raw-domain
record vs. no record, Some vs. None raw_content_hash), which is the
kind of divergence that produced the #375 bugs.

Extract the pure part into core::envelope::extractor::strip_attachment_
bodies (stripped EML, blob records, attachment metadata, and optional
text-extraction candidates via ExtractionFilter). The live path wraps it
with account rules, background extraction, and the blob queue; the
offline migration delegates to it with no filter, adopting core's
invalid-range behavior for consistency.
2026-10-14 02:30:02 +08:00
rustmailer
f66ee98dc8 FIX(Attachments): recover mixed-format detached EMLs during reattach
A re-ingest refreshes envelope metadata to raw storage keys while the
blob store's `exists` dedup keeps the previously stored stripped EML,
whose placeholders still carry the legacy decoded hash. The raw-hash
placeholder scan then finds no occurrence and the reconstructed EML
keeps literal `<<BICHON_DETACH_HASH:...>>` placeholders.

reattach_parts now retries with the legacy decoded placeholder when the
primary scan finds nothing, loading the legacy blob under the decoded
key (or the identical raw-key blob as an alternate when the legacy one
was garbage-collected after the original envelope was deleted).

Also removes the blob-store roundtrip test, which depended on the
process-global BLOB_MANAGER and only passed in isolation; reattach
coverage is provided by the reattach_parts tests with injected loads.
2026-10-14 02:19:52 +08:00
rustmailer
0b0879196c Merge pull request #375 from caffeineflo/fh/fix/attachment-roundtrip
fix(attachments): preserve encoded MIME bodies during reconstruction
2026-10-14 02:01:14 +08:00
Florian Harr
0f1664ac71 fix(attachments): resolve upstream test-module conflict 2026-10-12 20:20:37 -04:00
rustmailer
bd5ecde270 fix: Import button never becomes enabled for .pst files #372 2026-10-12 21:58:36 +08:00