Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/default_keybinds.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"email": {
"reply": "r",
"reply_all": "shift+r",
"forward": "f",
"delete": "d",
"archive": "a",
Expand Down
2 changes: 2 additions & 0 deletions config/keybinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type InboxKeys struct {

type EmailKeys struct {
Reply string `json:"reply"`
ReplyAll string `json:"reply_all"`
Forward string `json:"forward"`
Delete string `json:"delete"`
Archive string `json:"archive"`
Expand Down Expand Up @@ -127,6 +128,7 @@ func ValidateKeybinds(kb KeybindsConfig) []string {
},
"email": {
"reply": kb.Email.Reply,
"reply_all": kb.Email.ReplyAll,
"forward": kb.Email.Forward,
keyDelete: kb.Email.Delete,
"archive": kb.Email.Archive,
Expand Down
1 change: 1 addition & 0 deletions docs/docs/Features/Keybinds.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Plain text, not encrypted. Edit with any text editor. Restart matcha to apply ch
},
"email": {
"reply": "r",
"reply_all": "shift+r",
"forward": "f",
"delete": "d",
"archive": "a",
Expand Down
109 changes: 109 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1667,6 +1667,115 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo
m.syncPluginKeyBindings()
return m, m.current.Init()

case tui.ReplyAllEmailMsg:
// Determine the original sender (Reply-To if present, else From)
var sender string
if len(msg.Email.ReplyTo) > 0 {
sender = strings.Join(msg.Email.ReplyTo, ", ")
} else {
sender = msg.Email.From
}
subject := msg.Email.Subject
normalizedSubject := strings.ToLower(strings.TrimSpace(subject))
if !strings.HasPrefix(normalizedSubject, "re:") {
subject = "Re: " + subject
}
quotedText := fmt.Sprintf("\n\nOn %s, %s wrote:\n> %s", msg.Email.Date.Local().Format("Jan 2, 2006 at 3:04 PM"), msg.Email.From, strings.ReplaceAll(msg.Email.Body, "\n", "\n> "))

// Build set of the user's own addresses to exclude from reply-all
selfAddrs := make(map[string]bool)
if m.config != nil {
for _, acc := range m.config.Accounts {
selfAddrs[strings.ToLower(acc.Email)] = true
selfAddrs[strings.ToLower(acc.GetFetchEmail())] = true
selfAddrs[strings.ToLower(acc.GetSendAsEmail())] = true
}
// For catch-all accounts, also exclude the delivery address
accountID := msg.Email.AccountID
if accountID == "" && len(m.config.Accounts) > 0 {
accountID = m.config.GetFirstAccount().ID
}
for _, acc := range m.config.Accounts {
if acc.ID == accountID && acc.CatchAll && len(msg.Email.To) > 0 {
if addr, err := mail.ParseAddress(msg.Email.To[0]); err == nil {
selfAddrs[strings.ToLower(addr.Address)] = true
} else {
selfAddrs[strings.ToLower(strings.TrimSpace(msg.Email.To[0]))] = true
}
break
}
}
}

// Collect Cc recipients from original To list, excluding self and the sender
seen := make(map[string]bool)
var ccRecipients []string
for _, addr := range msg.Email.To {
parsed, err := mail.ParseAddress(addr)
var addrLower string
displayAddr := addr
if err == nil {
addrLower = strings.ToLower(parsed.Address)
displayAddr = parsed.Address
} else {
addrLower = strings.ToLower(strings.TrimSpace(addr))
}
if selfAddrs[addrLower] || seen[addrLower] {
continue
}
seen[addrLower] = true
ccRecipients = append(ccRecipients, displayAddr)
}
cc := strings.Join(ccRecipients, ", ")

var composer *tui.Composer
hideTips := false
if m.config != nil {
hideTips = m.config.HideTips
}
if m.config != nil && len(m.config.Accounts) > 0 {
accountID := msg.Email.AccountID
if accountID == "" {
accountID = m.config.GetFirstAccount().ID
}
composer = tui.NewComposerWithAccounts(m.config.Accounts, accountID, sender, subject, "", hideTips)
// For catch-all accounts, pre-fill From with the specific address the email was delivered to.
if len(msg.Email.To) > 0 {
for i := range m.config.Accounts {
if m.config.Accounts[i].ID == accountID && m.config.Accounts[i].CatchAll {
acc := &m.config.Accounts[i]
deliveryAddr := msg.Email.To[0]
if addr, err := mail.ParseAddress(deliveryAddr); err == nil {
deliveryAddr = addr.Address
}
fromVal := deliveryAddr
if acc.Name != "" {
fromVal = fmt.Sprintf("%s <%s>", acc.Name, deliveryAddr)
}
composer.SetFromOverride(fromVal)
break
}
}
}
} else {
composer = tui.NewComposer("", sender, subject, "", hideTips)
}
if cc != "" {
composer.SetCc(cc)
}
composer.SetQuotedText(quotedText)

// Set reply headers
inReplyTo := msg.Email.MessageID
references := append(msg.Email.References, msg.Email.MessageID) //nolint:gocritic
composer.SetReplyContext(inReplyTo, references)

m.applySpellcheckOptions(composer)
m.current = composer
m.current, _ = m.current.Update(m.currentWindowSize())
m.syncPluginKeyBindings()
return m, m.current.Init()

case tui.ForwardEmailMsg:
subject := msg.Email.Subject
if !strings.HasPrefix(strings.ToLower(subject), "fwd:") {
Expand Down
32 changes: 23 additions & 9 deletions tui/email_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ func (m *EmailView) Init() tea.Cmd {
return nil
}

// handleComposeAction checks if the key matches reply, reply-all, or forward
// and returns the corresponding message. Returns false if no match.
func (m *EmailView) handleComposeAction(key string) (tea.Msg, bool) {
kb := config.Keybinds
switch key {
case kb.Email.Reply:
ClearKittyGraphics()
return ReplyToEmailMsg{Email: m.email}, true
case kb.Email.ReplyAll:
ClearKittyGraphics()
return ReplyAllEmailMsg{Email: m.email}, true
case kb.Email.Forward:
ClearKittyGraphics()
return ForwardEmailMsg{Email: m.email}, true
}
return nil, false
}

func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
cmds := make([]tea.Cmd, 0, 1)
Expand Down Expand Up @@ -261,14 +279,10 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.SetContent(wrapped + "\n")
return m, nil
}
case kb.Email.Reply:
// Clear Kitty graphics before opening composer
ClearKittyGraphics()
return m, func() tea.Msg { return ReplyToEmailMsg{Email: m.email} }
case kb.Email.Forward:
// Clear Kitty graphics before opening composer
ClearKittyGraphics()
return m, func() tea.Msg { return ForwardEmailMsg{Email: m.email} }
case kb.Email.Reply, kb.Email.ReplyAll, kb.Email.Forward:
if composeMsg, ok := m.handleComposeAction(msg.String()); ok {
return m, func() tea.Msg { return composeMsg }
}
case kb.Email.Delete:
accountID := m.accountID
uid := m.email.UID
Expand Down Expand Up @@ -385,7 +399,7 @@ func (m *EmailView) View() tea.View {
help = helpStyle.Render(helpText)
} else {
var shortcuts strings.Builder
shortcuts.WriteString("\uf112 r: reply • \uf064 f: forward • \uea81 d: delete • \uea98 a: archive • \uf435 tab: focus attachments • \ueb06 esc: back to inbox")
shortcuts.WriteString("\uf112 r: reply • \uf064 shift+r: reply all • \uf064 f: forward • \uea81 d: delete • \uea98 a: archive • \uf435 tab: focus attachments • \ueb06 esc: back to inbox")
if view.ImageProtocolSupported() {
shortcuts.WriteString("• \uf03e i: toggle images")
}
Expand Down
4 changes: 4 additions & 0 deletions tui/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ type ReplyToEmailMsg struct {
Email fetcher.Email
}

type ReplyAllEmailMsg struct {
Email fetcher.Email
}

type ForwardEmailMsg struct {
Email fetcher.Email
}
Expand Down
Loading