|
1 | 1 | # Changelog |
2 | 2 |
|
| 3 | +# 2026-05-12 |
| 4 | +- **Per-trigger Listmonk template override** — `[[listmonk.triggers]]` entries now accept an optional `template_id = N` field that pins the Listmonk template for campaigns created from that trigger; triggers without the field fall through to Listmonk's own default template, so existing configs keep working unchanged. Useful when one virtual address should always use a specialised template (e.g. `listmonk-book@ssp.sh` → "Book Update Template", ID 5) while the rest stay on the generic newsletter template. The pre-send "Newsletter via Listmonk" review now surfaces the resolved template ID alongside the target list IDs and schedule delay, so it's visible before scheduling. Plumbed through `ListmonkTrigger` (config), `listmonk.Trigger` + new `ResolveTemplateID` (hook), `campaignRequest.TemplateID` (Listmonk REST payload, `omitempty` so default triggers send no field at all), and the UI send path. Regression test covers the override/default/no-match cases |
| 5 | + |
| 6 | +# 2026-05-08 |
| 7 | +- **`Ms` move to Sent** — added `Ms` chord to move the cursor or marked email(s) to the configured Sent folder, mirroring the `gs` (go to Sent) shortcut. Previously every other major folder had a matching `M*` mover (`Mi`/`Ma`/`Mt`/…) but Sent was missing, so manually filing already-sent correspondence forwarded from another client required `:` commands or a multi-step copy. Lives in the same `M`-prefix dstMap as the rest, no behaviour change for other letters |
| 8 | +- **`/` filter searches recipients in Sent folder** — the in-memory inbox filter (`/`) previously matched on `From + Subject` in every folder. In Sent that meant typing `/hussain` after sending Hussain an email returned nothing, because `From` is always you. The filter now matches on `To + CC + BCC + Subject` whenever the active folder is the configured Sent folder; all other folders are unchanged. The server-side IMAP search (`<space>/` / `:search`) already searches To and is unaffected |
| 9 | +- Review and stability |
| 10 | + - **Concurrency: data race in spy pixel cache save eliminated** — `bodyLoadedMsg` and `spyScanProgressMsg` previously launched `safeGo(func() { saveSpyPixelCache(copyMap(m.spyPixelKeys), copyMap(m.spyScannedKeys)) })`, evaluating `copyMap` *inside* the goroutine — the closure captured the model and read the live maps while the main goroutine continued mutating them on the next message. Both call sites now snapshot the maps on the main goroutine before launching the writer, so `copyMap` cannot race with subsequent map writes. The `// Takes copied maps to avoid concurrent access` comment on `saveSpyPixelCache` was true of the helper signature but the call sites violated the contract; both are now correct |
| 11 | + - **Concurrency: `Screener` is now safe for concurrent use** — added `sync.RWMutex` guarding every map (`screenedIn`, `screenedOut`, `feed`, `paperTrail`, `spam`, `notify`). Auto-screen, background sync, search, notify, and TUI mutation paths all share one `*Screener`, so concurrent map writes would have panicked at runtime. `Classify`, `ClassifyDebug`, `IsEmpty`, `AllAddresses`, `ShouldNotify`, and `Snapshot` take a read lock; `Approve`, `Block`, `MarkSpam`, `MarkFeed`, `MarkPaperTrail`, `AddNotify`, `RemoveNotify`, and `Restore` take a write lock. Refactored `Snapshot`/`Restore` into public-locked + private `snapshotLocked`/`restoreLocked` variants so the existing transactional rollback inside `Approve`/`Block`/etc. doesn't double-lock. RWMutex keeps the hot read path parallel; `make test -race` clean |
| 12 | + - **Security: path traversal in `.ics` calendar attachment open (`<space> v o`)** — `filepath.Base("..")` returns `".."`, and `filepath.Join("~/.cache/neomd/ical", "..")` escapes the cache dir. The previous filter rejected `""`, `"."`, and `"/"` but not `".."`, so a hostile `Content-Disposition: filename` of `..` would let `os.WriteFile(att.Data)` overwrite `~/.cache/neomd/`. Now also rejects `".."` and any embedded path separator, falling back to `invite-<unix-ts>.ics` when the sender-supplied name is unusable |
| 13 | + - **Security: path traversal in attachment download (`xdg-open` flow)** — same root cause in `downloadOpenAttachmentCmd`: a malicious `.eml` part with `Content-Disposition: filename=".."` would write the attachment bytes to `~/Downloads/..` (i.e. `~`). This is reached by every reader-driven attachment open, so a hostile sender controlled the destination path. The filter now rejects `""`, `"."`, `".."`, `"/"`, and any embedded path separator, falling back to `attachment-<unix-ts>` |
| 14 | + - **Reliability: `?` no longer trapped in compose textinputs** — the global `?` → help binding fired from any state, including `stateCompose`. Compose feeds keys into a `bubbles/textinput` (To/CC/BCC/Subject), which never received `?` because the global handler intercepted first. Now gated to all states *except* `stateCompose`, so `?` types verbatim while composing and still toggles help everywhere else |
| 15 | + - **Reliability: `extractAddr` no longer issues blocking DNS lookups on send** — the helper called `net.LookupHost` for every recipient and From address inside the SMTP send hot path, blocking the bubbletea Update loop on flaky DNS for seconds at a time. The lookup was also functionally pointless: an `||` short-circuit returned the address whenever it contained `@`, so DNS only mattered for malformed-but-bracket-shaped strings. Replaced with pure string parsing — extracts the `<addr>` form when present, otherwise returns the trimmed input. Unused `"net"` import dropped |
| 16 | + - **Reliability: outbound emails preserve trailing whitespace on hard-break lines** — RFC 2045 §6.7 requires that any space or tab immediately before a CRLF be encoded as `=20` / `=09` in quoted-printable. The previous `writeQP` passed trailing whitespace through verbatim, which many SMTP relays silently strip — mangling Markdown's two-trailing-spaces hard-break syntax (precisely the syntax `normalizePlainText` adds when extracting plain text from HTML). The encoder now peeks ahead; trailing whitespace before `\n` or end-of-input is encoded |
| 17 | + - **Reliability: bare `go` calls promoted to `safeGo`** — `saveCmdHistory` (`:` command-line history persistence) was launched as a bare `go func()`, bypassing the project's `safeGo` panic-recovery contract documented in `CLAUDE.md`. A panic inside the writer would have torn down the TUI without a stack trace. Now wrapped in `safeGo`, with a snapshot of the history slice taken on the main goroutine before launch so the writer cannot race with the next command-history mutation |
| 18 | + - **Reliability: OAuth2 callback server panic recovery + non-blocking sends** — `runAuthFlow` launched the callback HTTP server as a bare `go func()`. A panic inside `net/http`'s request handling would have crashed the process during the OAuth2 dance. Goroutine now has a `defer recover()` that surfaces the panic as an `errCh` send. Additionally, `errCh` and `codeCh` are buffered size 1, but a re-fired callback (browser refresh, double redirect) would block on subsequent sends and leak the handler goroutine; all sends now use a `select`/`default` non-blocking pattern |
| 19 | + - **Reliability: IMAP retry catches more transient failures** — `isNetErr` previously matched only four substrings (`use of closed network connection`, `connection reset by peer`, `broken pipe`, `EOF`), missing real-world transient failures: `tls: connection lost`, `i/o timeout`, `connection timed out`, `connection refused`, `no route to host`, `network is unreachable`. Those errors fell through the retry path, surfacing as user-visible failures instead of a clean reconnect. Now uses `errors.Is(err, net.ErrClosed | io.EOF | io.ErrUnexpectedEOF)` and `net.Error.Timeout()` typed checks plus a widened substring list covering TLS, syscall, and DNS-layer error formats |
| 20 | + - **Reliability: `--headless` no longer panics when account 0 is `imap_disabled = true`** — `daemon.New(*cfg, imapClients[0], sc)` blindly passed the first IMAP client to the daemon. Accounts with `imap_disabled = true` produce a `nil` client by design; if account 0 happened to be send-only, `--headless` would crash on the first IMAP call. The launcher now scans `imapClients` for the first non-nil entry and fails fast with a clear error message ("`--headless requires at least one IMAP-enabled account`") if no IMAP-enabled account exists, instead of panicking deep in the screening loop |
| 21 | + |
3 | 22 | # 2026-05-06 |
4 | 23 | - **Fix: attachments invisible after pre-send round-trip** — re-opening the editor from pre-send via `e` (re-edit), `s` (spell-check), or `i` (AI handoff), or continuing a saved draft, now re-injects tracked attachments as `# [attach] /path` lines right under the existing `# [neomd: ...]` headers instead of silently keeping them only in `m.attachments`. Previously the `[attach]` lines were extracted on the way to pre-send (correctly — they should never reach the recipient as text) but the editor reopened with the cleaned body, leaving no signal that PDFs/files were attached; users would re-attach and end up with duplicates. `extractInlineAttachments` now accepts both forms — `# [attach] /path` (header form, used on re-injection, visually grouped with the metadata headers) and `[attach] /path` (the form `<leader>a` continues to insert at cursor position for inline image placement). The editor body is now the source of truth: `editorDoneMsg` replaces `m.attachments` with whatever `[attach]` lines come back, instead of appending — so removing a line in the editor cleanly removes the attachment. For draft continuation the injected paths are the temp-extracted `/tmp/neomd/draft-<name>-<random>` files written by `writeAttachmentsTemp` from the saved MIME parts; the user can replace them with the original local paths if those files moved |
5 | 24 | - **Theming with 6 built-in palettes** — pick via `[ui].theme = "kanagawa"` (default, byte-for-byte identical to pre-theme state), `kanagawa-paper`, `kanagawa-light` (only light theme — Lotus palette on a paperwhite `#F2EFE9` background), `rose-pine`, `gruvbox`, or `osaka-jade`. Optional top-level `[theme]` block overrides individual colour slots (`primary`, `unread`, `error`, `bg`, …) on top of any built-in. `ApplyTheme` runs in `ui.New()` before any rendering, so theme switching needs only a config edit + restart. Regression test verifies the kanagawa default does not drift; theme tests cover override merge, unknown-name fallback, and round-trip uniqueness |
|
0 commit comments