diff options
Diffstat (limited to 'worker/jmap/fetch.go')
-rw-r--r-- | worker/jmap/fetch.go | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/worker/jmap/fetch.go b/worker/jmap/fetch.go new file mode 100644 index 00000000..17b3fb2f --- /dev/null +++ b/worker/jmap/fetch.go @@ -0,0 +1,196 @@ +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) +} |