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
73 changes: 61 additions & 12 deletions backend/jmap/jmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ func searchLimit(query backend.SearchQuery) uint32 {
return 100
}

func (p *Provider) FetchEmailBody(_ context.Context, _ string, uid uint32) (string, string, []backend.Attachment, error) {
jmapID, err := p.lookupJMAPID(uid)
func (p *Provider) FetchEmailBody(_ context.Context, folder string, uid uint32) (string, string, []backend.Attachment, error) {
jmapID, err := p.resolveUID(folder, uid)
if err != nil {
return "", "", nil, err
}
Expand Down Expand Up @@ -362,8 +362,8 @@ func (p *Provider) FetchAttachment(_ context.Context, _ string, _ uint32, partID
return io.ReadAll(reader)
}

func (p *Provider) MarkAsRead(_ context.Context, _ string, uid uint32) error {
jmapID, err := p.lookupJMAPID(uid)
func (p *Provider) MarkAsRead(_ context.Context, folder string, uid uint32) error {
jmapID, err := p.resolveUID(folder, uid)
if err != nil {
return err
}
Expand All @@ -380,8 +380,8 @@ func (p *Provider) MarkAsRead(_ context.Context, _ string, uid uint32) error {
return err
}

func (p *Provider) MarkAsUnread(_ context.Context, _ string, uid uint32) error {
jmapID, err := p.lookupJMAPID(uid)
func (p *Provider) MarkAsUnread(_ context.Context, folder string, uid uint32) error {
jmapID, err := p.resolveUID(folder, uid)
if err != nil {
return err
}
Expand All @@ -398,8 +398,8 @@ func (p *Provider) MarkAsUnread(_ context.Context, _ string, uid uint32) error {
return err
}

func (p *Provider) DeleteEmail(_ context.Context, _ string, uid uint32) error {
jmapID, err := p.lookupJMAPID(uid)
func (p *Provider) DeleteEmail(_ context.Context, folder string, uid uint32) error {
jmapID, err := p.resolveUID(folder, uid)
if err != nil {
return err
}
Expand Down Expand Up @@ -428,8 +428,8 @@ func (p *Provider) DeleteEmail(_ context.Context, _ string, uid uint32) error {
return err
}

func (p *Provider) ArchiveEmail(_ context.Context, _ string, uid uint32) error {
jmapID, err := p.lookupJMAPID(uid)
func (p *Provider) ArchiveEmail(_ context.Context, folder string, uid uint32) error {
jmapID, err := p.resolveUID(folder, uid)
if err != nil {
return err
}
Expand All @@ -450,8 +450,8 @@ func (p *Provider) ArchiveEmail(_ context.Context, _ string, uid uint32) error {
return err
}

func (p *Provider) MoveEmail(_ context.Context, uid uint32, _, dstFolder string) error {
jmapID, err := p.lookupJMAPID(uid)
func (p *Provider) MoveEmail(_ context.Context, uid uint32, srcFolder, dstFolder string) error {
jmapID, err := p.resolveUID(srcFolder, uid)
if err != nil {
return err
}
Expand Down Expand Up @@ -678,6 +678,55 @@ func (p *Provider) Close() error {
// Verify interface compliance at compile time.
var _ backend.Provider = (*Provider)(nil)

// resolveUID returns the JMAP ID for the given uint32 UID. It checks the
// in-memory cache first (fast path when FetchEmails ran on the same instance),
// then falls back to querying the mailbox so the backend works correctly even
// when a fresh Provider instance is created per call.
func (p *Provider) resolveUID(folder string, uid uint32) (jmapclient.ID, error) {
if id, err := p.lookupJMAPID(uid); err == nil {
return id, nil
}
return p.resolveUIDByQuery(folder, uid)
}

// resolveUIDByQuery fetches all email IDs in the folder from JMAP via
// Email/query, hashes each one, and returns the ID whose hash matches uid.
// It also warms the local cache as a side effect.
func (p *Provider) resolveUIDByQuery(folder string, uid uint32) (jmapclient.ID, error) {
mboxID, err := p.resolveMailboxID(folder)
if err != nil {
return "", fmt.Errorf("jmap: resolving mailbox for UID lookup: %w", err)
}

req := &jmapclient.Request{}
req.Invoke(&email.Query{
Account: p.accountID,
Filter: &email.FilterCondition{InMailbox: mboxID},
Limit: 10000,
})

resp, err := p.client.Do(req)
if err != nil {
return "", fmt.Errorf("jmap: querying IDs for UID lookup: %w", err)
}

p.mu.Lock()
defer p.mu.Unlock()

for _, inv := range resp.Responses {
if r, ok := inv.Args.(*email.QueryResponse); ok {
for _, id := range r.IDs {
h := jmapIDToUID(id)
p.idToJMAPID[h] = id
if h == uid {
return id, nil
}
}
}
}
return "", fmt.Errorf("jmap: no email found for UID %d in folder %q", uid, folder)
}

// lookupJMAPID resolves a uint32 UID hash back to the JMAP string ID.
func (p *Provider) lookupJMAPID(uid uint32) (jmapclient.ID, error) {
p.mu.Lock()
Expand Down
2 changes: 2 additions & 0 deletions daemonclient/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"time"

"github.com/floatpane/matcha/backend"
_ "github.com/floatpane/matcha/backend/jmap" // register jmap backend for directService
_ "github.com/floatpane/matcha/backend/maildir" // register maildir backend for directService
"github.com/floatpane/matcha/config"
"github.com/floatpane/matcha/daemonrpc"
)
Expand Down
Loading