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 backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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 @@ -136,6 +136,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
6 changes: 5 additions & 1 deletion backend/jmap/jmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func (p *Provider) FetchEmails(_ context.Context, folder string, limit, offset u
Properties: []string{
"id", "subject", "from", "to", "replyTo", "receivedAt",
"preview", "keywords", "mailboxIds", "hasAttachment",
"messageId",
"messageId", "inReplyTo", "references",
},
})

Expand Down Expand Up @@ -614,6 +614,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 @@ -294,6 +297,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 @@ -335,16 +340,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
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
60 changes: 58 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,55 @@ 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
}

func IsFolderThreaded(folderName string) bool {
cache, err := LoadFolderCache()
if err != nil || cache.ThreadedFolders == nil {
return false
}
return cache.ThreadedFolders[folderName]
}

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)
}
if threaded {
cache.ThreadedFolders[folderName] = true
} else {
delete(cache.ThreadedFolders, folderName)
}
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)
}
30 changes: 16 additions & 14 deletions config/keybinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ type GlobalKeys struct {
}

type InboxKeys struct {
VisualMode string `json:"visual_mode"`
Delete string `json:"delete"`
Archive string `json:"archive"`
Refresh string `json:"refresh"`
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"`
Open string `json:"open"`
NextTab string `json:"next_tab"`
PrevTab string `json:"prev_tab"`
}

type EmailKeys struct {
Expand Down Expand Up @@ -138,13 +139,14 @@ 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,
"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,
"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 @@ -352,14 +352,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 @@ -459,14 +461,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
Loading
Loading