···11# Changelog
2233+# 2026-04-14
44+- **CC/BCC display in reader** — the email reader now shows CC and BCC headers when viewing sent emails; both fields appear conditionally (only when present) between To and Subject in the header display; particularly useful for confirming auto_bcc was applied when replying; note that BCC visibility depends on IMAP provider behavior (some providers strip BCC headers from Sent folder, others preserve them)
55+- **Extended link support (99 links)** — link opener now supports up to 99 links per email (previously limited to 10); `space+1-0` opens links 1-10, `space+l11-99` opens links 11-99 using intuitive numeric shortcuts (e.g. `space+l26` for link [26]); status line provides progressive feedback during multi-key input; footer help and `?` overlay updated
66+- **Fix: link extraction with brackets in text** — markdown link regex now correctly matches links with brackets inside the link text (e.g. `[[Watch the studio tour here]](url)`); changed from `[^\]]+` (anything except `]`) to non-greedy `.+?` to handle nested brackets; fixes newsletter links from Beehiiv and similar services
77+38# 2026-04-13
49- **Emoji reactions (`ctrl+e`)** — fast, keyboard-driven emoji reactions from inbox or reader; press `ctrl+e` to open emoji picker overlay, select with `1`-`8` for instant send or navigate with `j`/`k` and press `enter`; sends minimal reaction email (emoji + italic footer + quoted original message) with proper threading headers; available reactions: 👍 ❤️ 😂 🎉 🙏 💯 👀 ✅; original email marked with `\Answered` flag; reaction saved to Sent folder; auto-selects From address matching recipient (same logic as regular replies)
510- **Email threading headers** — all replies (regular `r`/`R` and emoji reactions `ctrl+e`) now include proper `In-Reply-To` and `References` headers for conversation threading; ensures replies appear correctly grouped in Gmail, Outlook, and Apple Mail conversation views; `References` header extracted from IMAP message body and preserved in reply chain
+2-1
docs/content/docs/keybindings.md
···132132| `e (pre-send)` | re-open editor to edit body |
133133| `enter (pre-send)` | confirm and send |
134134| `1-9 (reader)` | download attachment N to ~/Downloads and open with xdg-open |
135135-| `space+1-9 (reader)` | open link N in $BROWSER (0 = 10th link) |
135135+| `space+1-0 (reader)` | open link 1-10 in $BROWSER (0 = 10th link) |
136136+| `space+l11-99 (reader)` | open link 11-99 in $BROWSER (e.g. space+l26 for [26]) |
136137| `e (reader)` | open in $EDITOR read-only — search, copy, vim motions |
137138| `E (reader)` | continue draft — re-open as editable compose (Drafts folder) |
138139| `o (reader)` | open in w3m (terminal browser) |
+2-1
internal/ui/keys.go
···9999 {"e (pre-send)", "re-open editor to edit body"},
100100 {"enter (pre-send)", "confirm and send"},
101101 {"1-9 (reader)", "download attachment N to ~/Downloads and open with xdg-open"},
102102- {"space+1-9 (reader)", "open link N in $BROWSER (0 = 10th link)"},
102102+ {"space+1-0 (reader)", "open link 1-10 in $BROWSER (0 = 10th link)"},
103103+ {"space+l11-99 (reader)", "open link 11-99 in $BROWSER (e.g. space+l26 for [26])"},
103104 {"e (reader)", "open in $EDITOR read-only — search, copy, vim motions"},
104105 {"E (reader)", "continue draft — re-open as editable compose (Drafts folder)"},
105106 {"o (reader)", "open in w3m (terminal browser)"},
+34-2
internal/ui/model.go
···27492749 pending := m.readerPending
27502750 m.readerPending = ""
27512751 switch pending {
27522752- case " ": // space + digit opens link
27522752+ case " ": // space + digit opens link (1-10)
27532753 if len(key) == 1 && key >= "0" && key <= "9" {
27542754 var idx int
27552755 if key == "0" {
···27612761 return m, m.openLinkCmd(m.openLinks[idx].URL)
27622762 }
27632763 m.status = fmt.Sprintf("No link [%s].", key)
27642764+ return m, nil
27652765+ }
27662766+ // space + l = prefix for links 11-99 (two digits)
27672767+ if key == "l" {
27682768+ m.readerPending = "l"
27692769+ m.status = "link number (11-99): l__"
27702770+ return m, nil
27712771+ }
27722772+ // Not a digit or 'l' — fall through
27732773+ case "l": // l + first digit (waiting for second digit)
27742774+ if len(key) == 1 && key >= "0" && key <= "9" {
27752775+ m.readerPending = "l" + key
27762776+ m.status = fmt.Sprintf("link number: l%s_", key)
27642777 return m, nil
27652778 }
27662779 // Not a digit — fall through
···27682781 if key == "g" {
27692782 m.reader.GotoTop()
27702783 return m, nil
27842784+ }
27852785+ default:
27862786+ // Handle "l[0-9]" pattern (first digit entered, waiting for second)
27872787+ if len(pending) == 2 && pending[0] == 'l' && pending[1] >= '0' && pending[1] <= '9' {
27882788+ if len(key) == 1 && key >= "0" && key <= "9" {
27892789+ // Parse two-digit number
27902790+ numStr := string(pending[1]) + key
27912791+ num, _ := strconv.Atoi(numStr)
27922792+ idx := num - 1 // convert to 0-based index
27932793+ if idx >= 0 && idx < len(m.openLinks) {
27942794+ return m, m.openLinkCmd(m.openLinks[idx].URL)
27952795+ }
27962796+ m.status = fmt.Sprintf("No link [%d].", num)
27972797+ return m, nil
27982798+ }
27712799 }
27722800 }
27732801 }
···28212849 case " ":
28222850 if len(m.openLinks) > 0 {
28232851 m.readerPending = " "
28242824- m.status = "open link: space+1-9 (0=10th)"
28522852+ if len(m.openLinks) > 10 {
28532853+ m.status = "open link: 1-0 (links 1-10), l11-99 (links 11+)"
28542854+ } else {
28552855+ m.status = "open link: 1-0"
28562856+ }
28252857 return m, nil
28262858 }
28272859 case "g":