Skip to content
Merged
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 backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type Email struct {
Date time.Time
IsRead bool
MessageID string
InReplyTo string
References []string
Attachments []Attachment
AccountID string
Expand Down
1 change: 1 addition & 0 deletions backend/imap/imap.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ func toBackendEmails(emails []fetcher.Email) []backend.Email {
Date: e.Date,
IsRead: e.IsRead,
MessageID: e.MessageID,
InReplyTo: e.InReplyTo,
References: e.References,
Attachments: toBackendAttachments(e.Attachments),
AccountID: e.AccountID,
Expand Down
10 changes: 9 additions & 1 deletion backend/jmap/jmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ func (p *Provider) FetchEmails(_ context.Context, folder string, limit, offset u
Name: "Email/query",
Path: "/ids",
},
Properties: []string{"id", "subject", "from", "to", "replyTo", "receivedAt", "preview", "keywords", "mailboxIds", "hasAttachment", "messageId"},
Properties: []string{
"id", "subject", "from", "to", "replyTo", "receivedAt",
"preview", "keywords", "mailboxIds", "hasAttachment",
"messageId", "inReplyTo", "references",
},
})

resp, err := p.client.Do(req)
Expand Down Expand Up @@ -697,6 +701,10 @@ func jmapEmailToBackend(eml *email.Email, uid uint32, accountID string) backend.
if len(eml.MessageID) > 0 {
e.MessageID = eml.MessageID[0]
}
if len(eml.InReplyTo) > 0 {
e.InReplyTo = eml.InReplyTo[0]
}
e.References = append(e.References, eml.References...)
return e
}

Expand Down
41 changes: 32 additions & 9 deletions backend/pop3/pop3.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"io"
"mime"
"net/mail"
"regexp"
"strings"
"time"

Expand All @@ -27,6 +28,8 @@ import (
"github.com/floatpane/matcha/sender"
)

var pop3MessageIDRE = regexp.MustCompile(`<[^>]+>`)

func init() {
backend.RegisterBackend("pop3", func(account *config.Account) (backend.Provider, error) {
return New(account)
Expand Down Expand Up @@ -298,6 +301,8 @@ func entityToEmail(header *message.Header, msgInfo pop3client.MessageID, account
subject := header.Get("Subject")
dateStr := header.Get("Date")
messageID := header.Get("Message-ID")
inReplyTo := firstMessageID(header.Get("In-Reply-To"))
references := messageIDList(header.Get("References"))

var to []string
if toHeader := header.Get("To"); toHeader != "" {
Expand Down Expand Up @@ -339,16 +344,34 @@ func entityToEmail(header *message.Header, msgInfo pop3client.MessageID, account
}

return backend.Email{
UID: hashUID(uidStr),
From: from,
To: to,
ReplyTo: replyTo,
Subject: subject,
Date: date,
IsRead: false,
MessageID: messageID,
AccountID: accountID,
UID: hashUID(uidStr),
From: from,
To: to,
ReplyTo: replyTo,
Subject: subject,
Date: date,
IsRead: false,
MessageID: messageID,
InReplyTo: inReplyTo,
References: references,
AccountID: accountID,
}
}

func firstMessageID(value string) string {
ids := messageIDList(value)
if len(ids) == 0 {
return ""
}
return ids[0]
}

func messageIDList(value string) []string {
matches := pop3MessageIDRE.FindAllString(value, -1)
if len(matches) == 0 {
return strings.Fields(value)
}
return matches
}

// parseMessageBody extracts the body text and attachments from a raw message.
Expand Down
18 changes: 10 additions & 8 deletions config/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import (

// CachedEmail stores essential email data for caching.
type CachedEmail struct {
UID uint32 `json:"uid"`
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
Date time.Time `json:"date"`
MessageID string `json:"message_id"`
AccountID string `json:"account_id"`
IsRead bool `json:"is_read"`
UID uint32 `json:"uid"`
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
Date time.Time `json:"date"`
MessageID string `json:"message_id"`
InReplyTo string `json:"in_reply_to,omitempty"`
References []string `json:"references,omitempty"`
AccountID string `json:"account_id"`
IsRead bool `json:"is_read"`
}

// EmailCache stores cached emails for all accounts.
Expand Down
5 changes: 5 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ type Config struct {
HideTips bool `json:"hide_tips,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty"`
EnableSplitPane bool `json:"enable_split_pane,omitempty"`
EnableThreaded bool `json:"enable_threaded,omitempty"`
Theme string `json:"theme,omitempty"`
MailingLists []MailingList `json:"mailing_lists,omitempty"`
DateFormat string `json:"date_format,omitempty"`
Expand Down Expand Up @@ -398,9 +399,11 @@ type secureDiskConfig struct {
HideTips bool `json:"hide_tips,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty"`
EnableSplitPane bool `json:"enable_split_pane,omitempty"`
EnableThreaded bool `json:"enable_threaded,omitempty"`
Theme string `json:"theme,omitempty"`
MailingLists []MailingList `json:"mailing_lists,omitempty"`
DateFormat string `json:"date_format,omitempty"`
Language string `json:"language,omitempty"`
}

// SaveConfig saves the given configuration to the config file and passwords to the keyring.
Expand Down Expand Up @@ -543,6 +546,7 @@ func LoadConfig() (*Config, error) {
HideTips bool `json:"hide_tips,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty"`
EnableSplitPane bool `json:"enable_split_pane,omitempty"`
EnableThreaded bool `json:"enable_threaded,omitempty"`
Theme string `json:"theme,omitempty"`
MailingLists []MailingList `json:"mailing_lists,omitempty"`
DateFormat string `json:"date_format,omitempty"`
Expand Down Expand Up @@ -579,6 +583,7 @@ func LoadConfig() (*Config, error) {
config.HideTips = raw.HideTips
config.DisableNotifications = raw.DisableNotifications
config.EnableSplitPane = raw.EnableSplitPane
config.EnableThreaded = raw.EnableThreaded
config.Theme = raw.Theme
config.MailingLists = raw.MailingLists
config.DateFormat = raw.DateFormat
Expand Down
1 change: 1 addition & 0 deletions config/default_keybinds.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
},
"inbox": {
"visual_mode": "v",
"toggle_threaded": "T",
"delete": "d",
"archive": "a",
"refresh": "r",
Expand Down
64 changes: 62 additions & 2 deletions config/folder_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"encoding/json"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/floatpane/matcha/internal/threading"
)

// CachedFolders stores folder names for a single account.
Expand All @@ -17,8 +20,9 @@ type CachedFolders struct {

// FolderCache stores cached folders for all accounts.
type FolderCache struct {
Accounts []CachedFolders `json:"accounts"`
UpdatedAt time.Time `json:"updated_at"`
Accounts []CachedFolders `json:"accounts"`
ThreadedFolders map[string]bool `json:"threaded_folders,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}

// folderCacheFile returns the full path to the folder cache file.
Expand Down Expand Up @@ -179,3 +183,59 @@ func LoadFolderEmailCache(folderName string) ([]CachedEmail, error) {
}
return cache.Emails, nil
}

func LoadFolderEmailHeaders(folderName string) ([]threading.EmailHeader, error) {
emails, err := LoadFolderEmailCache(folderName)
if err != nil {
return nil, err
}
headers := make([]threading.EmailHeader, 0, len(emails))
for _, email := range emails {
headers = append(headers, threading.EmailHeader{
ID: email.MessageID,
InReplyTo: email.InReplyTo,
References: email.References,
Subject: email.Subject,
Date: email.Date,
EmailID: cachedEmailID(email),
Sender: email.From,
})
}
return headers, nil
}

// IsFolderThreaded returns the threading state for a folder. If the user has
// explicitly toggled threading for this folder, that override is returned.
// Otherwise defaultEnabled (from Config.EnableThreaded) is used.
func IsFolderThreaded(folderName string, defaultEnabled bool) bool {
cache, err := LoadFolderCache()
if err != nil || cache.ThreadedFolders == nil {
return defaultEnabled
}
v, ok := cache.ThreadedFolders[folderName]
if !ok {
return defaultEnabled
}
return v
}

// SetFolderThreaded stores an explicit per-folder threading override.
func SetFolderThreaded(folderName string, threaded bool) error {
cache, err := LoadFolderCache()
if err != nil {
cache = &FolderCache{}
}
if cache.ThreadedFolders == nil {
cache.ThreadedFolders = make(map[string]bool)
}
cache.ThreadedFolders[folderName] = threaded
return SaveFolderCache(cache)
}

func cachedEmailID(email CachedEmail) string {
return email.AccountID + ":" + formatUID(email.UID)
}

func formatUID(uid uint32) string {
return strconv.FormatUint(uint64(uid), 10)
}
38 changes: 20 additions & 18 deletions config/keybinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ type GlobalKeys struct {
}

type InboxKeys struct {
VisualMode string `json:"visual_mode"`
Delete string `json:"delete"`
Archive string `json:"archive"`
Refresh string `json:"refresh"`
Search string `json:"search"`
Filter string `json:"filter"`
Open string `json:"open"`
NextTab string `json:"next_tab"`
PrevTab string `json:"prev_tab"`
VisualMode string `json:"visual_mode"`
ToggleThreaded string `json:"toggle_threaded"`
Delete string `json:"delete"`
Archive string `json:"archive"`
Refresh string `json:"refresh"`
Search string `json:"search"`
Filter string `json:"filter"`
Open string `json:"open"`
NextTab string `json:"next_tab"`
PrevTab string `json:"prev_tab"`
}

type EmailKeys struct {
Expand Down Expand Up @@ -140,15 +141,16 @@ func ValidateKeybinds(kb KeybindsConfig) []string {
"nav_down": kb.Global.NavDown,
})
check("inbox", map[string]string{
"visual_mode": kb.Inbox.VisualMode,
"delete": kb.Inbox.Delete,
"archive": kb.Inbox.Archive,
"refresh": kb.Inbox.Refresh,
"search": kb.Inbox.Search,
"filter": kb.Inbox.Filter,
"open": kb.Inbox.Open,
"next_tab": kb.Inbox.NextTab,
"prev_tab": kb.Inbox.PrevTab,
"visual_mode": kb.Inbox.VisualMode,
"toggle_threaded": kb.Inbox.ToggleThreaded,
"delete": kb.Inbox.Delete,
"archive": kb.Inbox.Archive,
"refresh": kb.Inbox.Refresh,
"search": kb.Inbox.Search,
"filter": kb.Inbox.Filter,
"open": kb.Inbox.Open,
"next_tab": kb.Inbox.NextTab,
"prev_tab": kb.Inbox.PrevTab,
})
check("email", map[string]string{
"reply": kb.Email.Reply,
Expand Down
36 changes: 20 additions & 16 deletions daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,14 +360,16 @@ func (d *Daemon) syncAllAccounts(ctx context.Context) {
var cached []config.CachedEmail
for _, e := range emails {
cached = append(cached, config.CachedEmail{
UID: e.UID,
From: e.From,
To: e.To,
Subject: e.Subject,
Date: e.Date,
MessageID: e.MessageID,
AccountID: e.AccountID,
IsRead: e.IsRead,
UID: e.UID,
From: e.From,
To: e.To,
Subject: e.Subject,
Date: e.Date,
MessageID: e.MessageID,
InReplyTo: e.InReplyTo,
References: e.References,
AccountID: e.AccountID,
IsRead: e.IsRead,
})
}
if err := d.updateFolderCache("INBOX", acct.ID, cached); err != nil {
Expand Down Expand Up @@ -474,14 +476,16 @@ func (d *Daemon) fetchAndCache(accountID, folder string) {
var cached []config.CachedEmail
for _, e := range emails {
cached = append(cached, config.CachedEmail{
UID: e.UID,
From: e.From,
To: e.To,
Subject: e.Subject,
Date: e.Date,
MessageID: e.MessageID,
AccountID: e.AccountID,
IsRead: e.IsRead,
UID: e.UID,
From: e.From,
To: e.To,
Subject: e.Subject,
Date: e.Date,
MessageID: e.MessageID,
InReplyTo: e.InReplyTo,
References: e.References,
AccountID: e.AccountID,
IsRead: e.IsRead,
})
}

Expand Down
3 changes: 3 additions & 0 deletions docs/docs/Features/Keybinds.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ Plain text, not encrypted. Edit with any text editor. Restart matcha to apply ch
},
"inbox": {
"visual_mode": "v",
"toggle_threaded": "T",
"delete": "d",
"archive": "a",
"refresh": "r",
"search": "/",
"filter": "f",
"open": "enter",
"next_tab": "l",
"prev_tab": "h"
Expand Down
Loading
Loading