···11# Changelog
2233+# 2026-04-05
44+- **`auto_bcc` config** — root-level `auto_bcc = "addr@example.com"` appends an address to every outgoing email's Bcc so you keep a copy in an external mailbox (e.g. a hey.com archive); visible in the composer and pre-send review (no silent BCC), deduped against any manual Bcc entry
55+- **`shift+tab` in compose** — navigate back through To/Cc/Bcc/Subject fields (previously could only move forward with tab/enter)
66+- **Reader shows local time** — email dates in the reader header now convert to your system's local timezone and include the clock time (e.g. `Apr 05, 00:51`); previously showed the sender's timezone date without time
77+38# 2026-04-02
49- **Auto From on reply** — replying auto-selects the From address that matches the email's To/CC field (e.g. email sent to `simon@domain.com` replies from `simon@domain.com`); `r` now works from inbox list view; `# [neomd: from: ...]` shown in editor; `x` in pre-send discards the email
510- **Email safety hardening** — bulk operations show live progress counter ("Screening: 42/1000…") for batches >10; screener now moves emails before updating list files (no inconsistent state on failure); SaveSent failure shown as warning instead of silently swallowed; batch failures report exact moved/total counts; partial batch undo info preserved on error; undo stack capped at 20
+6
internal/config/config.go
···139139 Screener ScreenerConfig `toml:"screener"`
140140 Folders FoldersConfig `toml:"folders"`
141141 UI UIConfig `toml:"ui"`
142142+143143+ // AutoBCC, if set, is added to every outgoing email's Bcc field so the
144144+ // user keeps a copy in an external mailbox (e.g. their hey.com archive).
145145+ // Format: "addr@example.com" or "Name <addr@example.com>". Shown in the
146146+ // composer and pre-send review so it's never a silent BCC.
147147+ AutoBCC string `toml:"auto_bcc"`
142148}
143149144150// ActiveAccounts returns the list of configured accounts.
+33
internal/ui/compose.go
···188188 // Tab without suggestions → next field (same as enter)
189189 return c.advanceField()
190190191191+ case "shift+tab":
192192+ // Move to the previous field (To ← Cc ← Bcc ← Subject).
193193+ return c.retreatField()
194194+191195 case "enter":
192196 // Enter always advances to next field
193197 if len(c.suggestions) > 0 && c.suggestI >= 0 {
···256260 return c, nil, false
257261 case stepSubject:
258262 return c, nil, true
263263+ }
264264+ return c, nil, false
265265+}
266266+267267+// retreatField moves to the previous compose field. Skips Cc/Bcc when hidden.
268268+func (c composeModel) retreatField() (composeModel, tea.Cmd, bool) {
269269+ c.suggestions = nil
270270+ c.suggestI = -1
271271+ switch c.step {
272272+ case stepSubject:
273273+ if c.extraVisible {
274274+ c.step = stepBCC
275275+ c.subject.Blur()
276276+ c.bcc.Focus()
277277+ } else {
278278+ c.step = stepTo
279279+ c.subject.Blur()
280280+ c.to.Focus()
281281+ }
282282+ case stepBCC:
283283+ c.step = stepCC
284284+ c.bcc.Blur()
285285+ c.cc.Focus()
286286+ case stepCC:
287287+ c.step = stepTo
288288+ c.cc.Blur()
289289+ c.to.Focus()
290290+ case stepTo:
291291+ // already at the first field — no-op
259292 }
260293 return c, nil, false
261294}
+14
internal/ui/inbox.go
···162162 }
163163}
164164165165+// fmtDateFull returns a date+time string for the reader header (always
166166+// includes the clock time, unlike fmtDate which is compact for list rows).
167167+func fmtDateFull(t time.Time) string {
168168+ if t.IsZero() {
169169+ return ""
170170+ }
171171+ t = t.Local()
172172+ if t.Year() == time.Now().Year() {
173173+ return t.Format("Jan 02, 15:04")
174174+ }
175175+ return t.Format("Jan 02 2006, 15:04")
176176+}
177177+165178func fmtDate(t time.Time) string {
166179 if t.IsZero() {
167180 return " "
168181 }
182182+ t = t.Local()
169183 now := time.Now()
170184 if t.Year() == now.Year() && t.YearDay() == now.YearDay() {
171185 return t.Format("15:04 ")
+20-1
internal/ui/model.go
···1444144414451445 // Go to pre-send review instead of sending immediately.
14461446 m.pendingSend = &pendingSendData{
14471447- to: msg.to, cc: msg.cc, bcc: msg.bcc,
14471447+ to: msg.to, cc: msg.cc, bcc: mergeAutoBCC(msg.bcc, m.cfg.AutoBCC),
14481448 subject: msg.subject, body: cleanBody,
14491449 }
14501450 m.state = statePresend
···29662966}
2967296729682968// extractEmailAddr returns the bare email address from "Name <addr>" or "addr".
29692969+// mergeAutoBCC appends autoBCC to the existing bcc field, deduped by email
29702970+// address. Returns bcc unchanged when autoBCC is empty or already present.
29712971+func mergeAutoBCC(bcc, autoBCC string) string {
29722972+ autoBCC = strings.TrimSpace(autoBCC)
29732973+ if autoBCC == "" {
29742974+ return bcc
29752975+ }
29762976+ autoAddr := strings.ToLower(extractEmailAddr(autoBCC))
29772977+ for _, p := range strings.Split(bcc, ",") {
29782978+ if strings.ToLower(extractEmailAddr(strings.TrimSpace(p))) == autoAddr {
29792979+ return bcc
29802980+ }
29812981+ }
29822982+ if strings.TrimSpace(bcc) == "" {
29832983+ return autoBCC
29842984+ }
29852985+ return bcc + ", " + autoBCC
29862986+}
29872987+29692988func extractEmailAddr(s string) string {
29702989 if i := strings.IndexByte(s, '<'); i >= 0 {
29712990 if j := strings.IndexByte(s, '>'); j > i {