aboutsummaryrefslogblamecommitdiffstats
path: root/worker/jmap/fetch.go
blob: 3b3a8460ac211337d36b62cd4c6d6be2905b4cd2 (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 {
		jid := jmap.ID(uid)
		m, err := w.cache.GetEmail(jid)
		if err != nil {
			// Message wasn't in cache; fetch it
			emailIdsToFetch = append(emailIdsToFetch, jid)
			continue
		}
		currentEmails = append(currentEmails, m)
		// Get the UI updated immediately
		w.w.PostMessage(&types.MessageInfo{
			Message: types.RespondTo(msg),
			Info:    w.translateMsgInfo(m),
		}, nil)
	}

	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 {
	mail, err := w.cache.GetEmail(jmap.ID(msg.Uid))
	if err != nil {
		return fmt.Errorf("bug: unknown message id %s: %w", msg.Uid, 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 {
		mail, err := w.cache.GetEmail(jmap.ID(uid))
		if err != nil {
			return fmt.Errorf("bug: unknown message id %s: %w", uid, 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)
}