GODT-35: New pmapi client and manager using resty

This commit is contained in:
James Houlahan
2021-02-22 18:23:51 +01:00
committed by Jakub
parent 1d538e8540
commit 2284e9ede1
163 changed files with 3333 additions and 8124 deletions

View File

@ -18,110 +18,68 @@
package pmapi
import (
"bytes"
"context"
"encoding/json"
"io"
"mime/multipart"
"errors"
"strconv"
"github.com/go-resty/resty/v2"
)
// Import errors.
const (
ImportMessageTooLarge = 36022
)
const MaxImportMessageRequestLength = 10
// ImportReq is an import request.
type ImportReq struct {
// A list of messages that will be imported.
Messages []*ImportMsgReq
}
// WriteTo writes the import request to a multipart writer.
func (req *ImportReq) WriteTo(w *multipart.Writer) (err error) {
// Create Metadata field.
mw, err := w.CreateFormField("Metadata")
if err != nil {
return
}
// Build metadata.
metadata := map[string]*ImportMsgReq{}
for i, msg := range req.Messages {
name := strconv.Itoa(i)
metadata[name] = msg
}
// Write metadata.
if err = json.NewEncoder(mw).Encode(metadata); err != nil {
return
}
// Write messages.
for i, msg := range req.Messages {
name := strconv.Itoa(i)
var fw io.Writer
if fw, err = w.CreateFormFile(name, name+".eml"); err != nil {
return err
}
if _, err = fw.Write(msg.Body); err != nil {
return
}
// Adding new line to properly fetch the whole body on the API side.
// The reason is the bug in PHP: https://bugs.php.net/bug.php?id=75923
// Messages generated by PM already have it but importing already
// encrypted messages might not have it.
if _, err = fw.Write([]byte("\r\n")); err != nil {
return
}
}
return err
}
// ImportMsgReq is a request to import a message. All fields are optional except AddressID and Body.
type ImportMsgReq struct {
// The address where the message will be imported.
AddressID string
// The full MIME message.
Body []byte `json:"-"`
// 0: read, 1: unread.
Unread int
// 1 if the message has been replied.
IsReplied int
// 1 if the message has been replied to all.
IsRepliedAll int
// 1 if the message has been forwarded.
IsForwarded int
// The time when the message was received as a Unix time.
Time int64
// The type of the imported message.
Flags int64
// The labels to apply to the imported message. Must contain at least one system label.
LabelIDs []string
Metadata *ImportMetadata // Metadata about the message to import.
Message []byte // The raw RFC822 message.
}
func (req ImportMsgReq) String() string {
data, _ := json.Marshal(req)
return string(data)
}
type ImportMsgReqs []*ImportMsgReq
// ImportRes is a response to an import request.
type ImportRes struct {
Res
func (reqs ImportMsgReqs) buildMultipartFormData() ([]*resty.MultipartField, error) {
var fields []*resty.MultipartField
Responses []struct {
Name string
Response struct {
Res
MessageID string
}
metadata := make(map[string]*ImportMetadata)
for i, req := range reqs {
name := strconv.Itoa(i)
metadata[name] = req.Metadata
fields = append(fields, &resty.MultipartField{
Param: name,
FileName: name + ".eml",
ContentType: "message/rfc822",
Reader: bytes.NewReader(req.Message),
})
}
b, err := json.Marshal(metadata)
if err != nil {
return nil, err
}
fields = append(fields, &resty.MultipartField{
Param: "Metadata",
ContentType: "application/json",
Reader: bytes.NewReader(b),
})
return fields, nil
}
// TODO: Add other metadata.
type ImportMetadata struct {
AddressID string
Unread Boolean // 0: read, 1: unread.
IsReplied Boolean // 1 if the message has been replied.
IsRepliedAll Boolean // 1 if the message has been replied to all.
IsForwarded Boolean // 1 if the message has been forwarded.
Time int64 // The time when the message was received as a Unix time.
Flags int64 // The type of the imported message.
LabelIDs []string // The labels to apply to the imported message. Must contain at least one system label.
}
// ImportMsgRes is a response to a single message import request.
type ImportMsgRes struct {
// The error encountered while importing the message, if any.
Error error
@ -130,41 +88,46 @@ type ImportMsgRes struct {
}
// Import imports messages to the user's account.
func (c *client) Import(reqs []*ImportMsgReq) (resps []*ImportMsgRes, err error) {
importReq := &ImportReq{Messages: reqs}
func (c *client) Import(ctx context.Context, reqs ImportMsgReqs) ([]*ImportMsgRes, error) {
if len(reqs) > MaxImportMessageRequestLength {
return nil, errors.New("request is too long")
}
req, w, err := c.NewMultipartRequest("POST", "/mail/v4/messages/import")
fields, err := reqs.buildMultipartFormData()
if err != nil {
return
return nil, err
}
// We will write the request as long as it is sent to the API.
var importRes ImportRes
done := make(chan error, 1)
go (func() {
done <- c.DoJSON(req, &importRes)
})()
// Write the request.
if err = importReq.WriteTo(w.Writer); err != nil {
return
}
_ = w.Close()
if err = <-done; err != nil {
return
}
if err = importRes.Err(); err != nil {
return
}
resps = make([]*ImportMsgRes, len(importRes.Responses))
for i, r := range importRes.Responses {
resps[i] = &ImportMsgRes{
Error: r.Response.Err(),
MessageID: r.Response.MessageID,
var res struct {
Responses []struct {
Name string
Response struct {
Error
MessageID string
}
}
}
return resps, err
if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetMultipartFields(fields...).SetResult(&res).Post("/mail/v4/messages/import")
}); err != nil {
return nil, err
}
var resps []*ImportMsgRes
for _, resp := range res.Responses {
var err error
if resp.Response.Code != 1000 {
err = resp.Response.Error
}
resps = append(resps, &ImportMsgRes{
Error: err,
MessageID: resp.Response.MessageID,
})
}
return resps, nil
}