aboutsummaryrefslogblamecommitdiffstats
path: root/worker/jmap/fetch.go
blob: bbef1bb58051a263b86f26253e50b5594ed8675b (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

















                                                  
                   
















                        
                                                                                      

                                                               






                                                                                     
                               


                                                                      
                 

         

                                      
 




                                                         
 


















                                                                                     

                          

         






                                                                 


                 
                  































































































                                                                                        
                                                     






                                                            
package jmap

import (
	"bytes"
	"fmt"
	"io"
	"strings"

	"git.sr.ht/~rjarry/aerc/models"
	"git.sr.ht/~rjarry/aerc/worker/types"
	"git.sr.ht/~rockorager/go-jmap"
	"git.sr.ht/~rockorager/go-jmap/mail/email"
	"github.com/emersion/go-message/charset"
)

var headersProperties = []string{
	"id",
	"blobId",
	"threadId",
	"mailboxIds",
	"keywords",
	"size",
	"receivedAt",
	"headers",
	"messageId",
	"inReplyTo",
	"references",
	"from",
	"to",
	"cc",
	"bcc",
	"replyTo",
	"subject",
	"bodyStructure",
}

func (w *JMAPWorker) handleFetchMessageHeaders(msg *types.FetchMessageHeaders) error {
	emailIdsToFetch := make([]jmap.ID, 0, len(msg.Uids))
	currentEmails := make([]*email.Email, 0, len(msg.Uids))
	for _, uid := range msg.Uids {
		id, ok := w.uidStore.GetKey(uid)
		if !ok {
			return fmt.Errorf("bug: no jmap id for message uid: %v", uid)
		}
		jid := jmap.ID(id)
		m, err := w.cache.GetEmail(jid)
		if err == nil {
			currentEmails = append(currentEmails, m)
		} else {
			emailIdsToFetch = append(emailIdsToFetch, jid)
		}
	}

	if len(emailIdsToFetch) != 0 {
		var req jmap.Request

		req.Invoke(&email.Get{
			Account:    w.AccountId(),
			IDs:        emailIdsToFetch,
			Properties: []string{"threadId"},
		})

		resp, err := w.Do(&req)
		if err != nil {
			return err
		}

		for _, inv := range resp.Responses {
			switch r := inv.Args.(type) {
			case *email.GetResponse:
				if err = w.cache.PutEmailState(r.State); err != nil {
					w.w.Warnf("PutEmailState: %s", err)
				}
				currentEmails = append(currentEmails, r.List...)
			case *jmap.MethodError:
				return wrapMethodError(r)
			}
		}
	}

	allEmails, err := w.fetchEntireThreads(currentEmails)
	if err != nil {
		return err
	}

	for _, m := range allEmails {
		w.w.PostMessage(&types.MessageInfo{
			Message: types.RespondTo(msg),
			Info:    w.translateMsgInfo(m),
		}, nil)
		if err := w.cache.PutEmail(m.ID, m); err != nil {
			w.w.Warnf("PutEmail: %s", err)
		}
	}

	return nil
}

func (w *JMAPWorker) handleFetchMessageBodyPart(msg *types.FetchMessageBodyPart) error {
	id, ok := w.uidStore.GetKey(msg.Uid)
	if !ok {
		return fmt.Errorf("bug: unknown message uid %d", msg.Uid)
	}
	mail, err := w.cache.GetEmail(jmap.ID(id))
	if err != nil {
		return fmt.Errorf("bug: unknown message id %s: %w", id, err)
	}

	part := mail.BodyStructure
	for i, index := range msg.Part {
		index -= 1 // convert to zero based offset
		if index < len(part.SubParts) {
			part = part.SubParts[index]
		} else {
			return fmt.Errorf(
				"bug: invalid part index[%d]: %v", i, msg.Part)
		}
	}

	buf, err := w.cache.GetBlob(part.BlobID)
	if err != nil {
		rd, err := w.Download(part.BlobID)
		if err != nil {
			return w.wrapDownloadError("part", part.BlobID, err)
		}
		buf, err = io.ReadAll(rd)
		rd.Close()
		if err != nil {
			return err
		}
		if err = w.cache.PutBlob(part.BlobID, buf); err != nil {
			w.w.Warnf("PutBlob: %s", err)
		}
	}
	var reader io.Reader = bytes.NewReader(buf)
	if strings.HasPrefix(part.Type, "text/") && part.Charset != "" {
		r, err := charset.Reader(part.Charset, reader)
		if err != nil {
			return fmt.Errorf("charset.Reader: %w", err)
		}
		reader = r
	}
	w.w.PostMessage(&types.MessageBodyPart{
		Message: types.RespondTo(msg),
		Part: &models.MessageBodyPart{
			Reader: reader,
			Uid:    msg.Uid,
		},
	}, nil)

	return nil
}

func (w *JMAPWorker) handleFetchFullMessages(msg *types.FetchFullMessages) error {
	for _, uid := range msg.Uids {
		id, ok := w.uidStore.GetKey(uid)
		if !ok {
			return fmt.Errorf("bug: unknown message uid %d", uid)
		}
		mail, err := w.cache.GetEmail(jmap.ID(id))
		if err != nil {
			return fmt.Errorf("bug: unknown message id %s: %w", id, err)
		}
		buf, err := w.cache.GetBlob(mail.BlobID)
		if err != nil {
			rd, err := w.Download(mail.BlobID)
			if err != nil {
				return w.wrapDownloadError("full", mail.BlobID, err)
			}
			buf, err = io.ReadAll(rd)
			rd.Close()
			if err != nil {
				return err
			}
			if err = w.cache.PutBlob(mail.BlobID, buf); err != nil {
				w.w.Warnf("PutBlob: %s", err)
			}
		}
		w.w.PostMessage(&types.FullMessage{
			Message: types.RespondTo(msg),
			Content: &models.FullMessage{
				Reader: bytes.NewReader(buf),
				Uid:    uid,
			},
		}, nil)
	}

	return nil
}

func (w *JMAPWorker) wrapDownloadError(prefix string, blobId jmap.ID, err error) error {
	urlRepl := strings.NewReplacer(
		"{accountId}", string(w.AccountId()),
		"{blobId}", string(blobId),
		"{type}", "application/octet-stream",
		"{name}", "filename",
	)
	url := urlRepl.Replace(w.client.Session.DownloadURL)
	return fmt.Errorf("%s: %q %w", prefix, url, err)
}