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", "mailboxIds", "keywords", "size", "receivedAt", "headers", "messageId", "inReplyTo", "references", "from", "to", "cc", "bcc", "replyTo", "subject", "bodyStructure", } func (w *JMAPWorker) handleFetchMessageHeaders(msg *types.FetchMessageHeaders) error { var req jmap.Request ids := make([]jmap.ID, 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 { w.w.PostMessage(&types.MessageInfo{ Message: types.RespondTo(msg), Info: w.translateMsgInfo(m), }, nil) continue } ids = append(ids, jid) } if len(ids) == 0 { return nil } req.Invoke(&email.Get{ Account: w.accountId, IDs: ids, Properties: headersProperties, }) resp, err := w.Do(&req) if err != nil { return err } for _, inv := range resp.Responses { switch r := inv.Args.(type) { case *email.GetResponse: for _, m := range r.List { 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) } } if err = w.cache.PutEmailState(r.State); err != nil { w.w.Warnf("PutEmailState: %s", err) } case *jmap.MethodError: return wrapMethodError(r) } } 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) }