You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
825 lines
19 KiB
825 lines
19 KiB
// Copyright (c) 2020 Proton Technologies AG |
|
// |
|
// This file is part of ProtonMail Bridge. |
|
// |
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify |
|
// it under the terms of the GNU General Public License as published by |
|
// the Free Software Foundation, either version 3 of the License, or |
|
// (at your option) any later version. |
|
// |
|
// ProtonMail Bridge is distributed in the hope that it will be useful, |
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
// GNU General Public License for more details. |
|
// |
|
// You should have received a copy of the GNU General Public License |
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
package pmapi |
|
|
|
import ( |
|
"bytes" |
|
"crypto/aes" |
|
"crypto/cipher" |
|
"encoding/base64" |
|
"encoding/json" |
|
"errors" |
|
"io" |
|
"net/http" |
|
"net/mail" |
|
"net/url" |
|
"strconv" |
|
"strings" |
|
|
|
pmcrypto "github.com/ProtonMail/gopenpgp/crypto" |
|
"golang.org/x/crypto/openpgp/packet" |
|
) |
|
|
|
// Header types. |
|
const ( |
|
MessageHeader = "-----BEGIN PGP MESSAGE-----" |
|
MessageTail = "-----END PGP MESSAGE-----" |
|
MessageHeaderLegacy = "---BEGIN ENCRYPTED MESSAGE---" |
|
MessageTailLegacy = "---END ENCRYPTED MESSAGE---" |
|
RandomKeyHeader = "---BEGIN ENCRYPTED RANDOM KEY---" |
|
RandomKeyTail = "---END ENCRYPTED RANDOM KEY---" |
|
) |
|
|
|
// Sort types. |
|
const ( |
|
SortByTo = "To" |
|
SortByFrom = "From" |
|
SortBySubject = "Subject" |
|
SortBySize = "Size" |
|
SortByTime = "Time" |
|
SortByID = "ID" |
|
SortDesc = true |
|
SortAsc = false |
|
) |
|
|
|
// Message actions. |
|
const ( |
|
ActionReply = 0 |
|
ActionReplyAll = 1 |
|
ActionForward = 2 |
|
) |
|
|
|
// Message flag definitions. |
|
const ( |
|
FlagReceived = 1 |
|
FlagSent = 2 |
|
FlagInternal = 4 |
|
FlagE2E = 8 |
|
FlagAuto = 16 |
|
FlagReplied = 32 |
|
FlagRepliedAll = 64 |
|
FlagForwarded = 128 |
|
|
|
FlagAutoreplied = 256 |
|
FlagImported = 512 |
|
FlagOpened = 1024 |
|
FlagReceiptSent = 2048 |
|
) |
|
|
|
// Draft flags. |
|
const ( |
|
FlagReceiptRequest = 1 << 16 |
|
FlagPublicKey = 1 << 17 |
|
FlagSign = 1 << 18 |
|
) |
|
|
|
// Spam flags. |
|
const ( |
|
FlagSpfFail = 1 << 24 |
|
FlagDkimFail = 1 << 25 |
|
FlagDmarcFail = 1 << 26 |
|
FlagHamManual = 1 << 27 |
|
FlagSpamAuto = 1 << 28 |
|
FlagSpamManual = 1 << 29 |
|
FlagPhishingAuto = 1 << 30 |
|
FlagPhishingManual = 1 << 31 |
|
) |
|
|
|
// Message flag masks. |
|
const ( |
|
FlagMaskGeneral = 4095 |
|
FlagMaskDraft = FlagReceiptRequest * 7 |
|
FlagMaskSpam = FlagSpfFail * 255 |
|
FlagMask = FlagMaskGeneral | FlagMaskDraft | FlagMaskSpam |
|
) |
|
|
|
// INTERNAL, AUTO are immutable. E2E is immutable except for drafts on send. |
|
const ( |
|
FlagMaskAdd = 4067 + (16777216 * 168) |
|
) |
|
|
|
// Content types. |
|
const ( |
|
ContentTypeMultipartMixed = "multipart/mixed" |
|
ContentTypeMultipartEncrypted = "multipart/encrypted" |
|
ContentTypePlainText = "text/plain" |
|
ContentTypeHTML = "text/html" |
|
) |
|
|
|
// LabelsOperation is the operation to apply to labels. |
|
type LabelsOperation int |
|
|
|
const ( |
|
KeepLabels LabelsOperation = iota // Do nothing. |
|
ReplaceLabels // Replace current labels with new ones. |
|
AddLabels // Add new labels to current ones. |
|
RemoveLabels // Remove specified labels from current ones. |
|
) |
|
|
|
const ( |
|
MessageTypeInbox int = iota |
|
MessageTypeDraft |
|
MessageTypeSent |
|
MessageTypeInboxAndSent |
|
) |
|
|
|
// Due to API limitations, we shouldn't make requests with more than 100 message IDs at a time. |
|
const messageIDPageSize = 100 |
|
|
|
// ConversationIDDomain is used as a placeholder for conversation reference headers to improve compatibility with various clients |
|
const ConversationIDDomain = `protonmail.conversationid` |
|
|
|
// InternalIDDomain is used as a placeholder for reference/message ID headers to improve compatibility with various clients |
|
const InternalIDDomain = `protonmail.internalid` |
|
|
|
// InternalReferenceFormat describes format of the message ID (as regex) used for parsing reference headers |
|
const InternalReferenceFormat = `(?U)<.*@` + InternalIDDomain + `>` |
|
|
|
// Message structure. |
|
type Message struct { |
|
ID string `json:",omitempty"` |
|
Order int64 `json:",omitempty"` |
|
ConversationID string `json:",omitempty"` // only filter |
|
Subject string |
|
Unread int |
|
Type int |
|
Flags int64 |
|
Sender *mail.Address |
|
ReplyTo *mail.Address `json:",omitempty"` |
|
ReplyTos []*mail.Address `json:",omitempty"` |
|
ToList []*mail.Address |
|
CCList []*mail.Address |
|
BCCList []*mail.Address |
|
Time int64 // Unix time |
|
Size int64 |
|
NumAttachments int |
|
ExpirationTime int64 // Unix time |
|
SpamScore int |
|
AddressID string |
|
Body string `json:",omitempty"` |
|
Attachments []*Attachment |
|
LabelIDs []string |
|
ExternalID string |
|
Header mail.Header |
|
MIMEType string |
|
} |
|
|
|
// NewMessage initializes a new message. |
|
func NewMessage() *Message { |
|
return &Message{ |
|
ToList: []*mail.Address{}, |
|
CCList: []*mail.Address{}, |
|
BCCList: []*mail.Address{}, |
|
Attachments: []*Attachment{}, |
|
LabelIDs: []string{}, |
|
} |
|
} |
|
|
|
// Define a new type to prevent MarshalJSON/UnmarshalJSON infinite loops. |
|
type message Message |
|
|
|
type rawMessage struct { |
|
message |
|
|
|
Header string `json:",omitempty"` |
|
} |
|
|
|
func (m *Message) MarshalJSON() ([]byte, error) { |
|
var raw rawMessage |
|
raw.message = message(*m) |
|
|
|
b := &bytes.Buffer{} |
|
_ = http.Header(m.Header).Write(b) |
|
raw.Header = b.String() |
|
|
|
return json.Marshal(&raw) |
|
} |
|
|
|
func (m *Message) UnmarshalJSON(b []byte) error { |
|
var raw rawMessage |
|
if err := json.Unmarshal(b, &raw); err != nil { |
|
return err |
|
} |
|
|
|
*m = Message(raw.message) |
|
|
|
if raw.Header != "" && raw.Header != "(No Header)" { |
|
msg, err := mail.ReadMessage(strings.NewReader(raw.Header + "\r\n\r\n")) |
|
if err == nil { |
|
m.Header = msg.Header |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// IsDraft returns whether the message should be considered to be a draft. |
|
// A draft is complicated. It might have pmapi.DraftLabel but it might not. |
|
// The real API definition of IsDraft is that it is neither sent nor received -- we should use that here. |
|
func (m *Message) IsDraft() bool { |
|
return (m.Flags & (FlagReceived | FlagSent)) == 0 |
|
} |
|
|
|
func (m *Message) IsBodyEncrypted() bool { |
|
trimmedBody := strings.TrimSpace(m.Body) |
|
return strings.HasPrefix(trimmedBody, MessageHeader) && |
|
strings.HasSuffix(trimmedBody, MessageTail) |
|
} |
|
|
|
func (m *Message) IsLegacyMessage() bool { |
|
return strings.Contains(m.Body, RandomKeyHeader) && |
|
strings.Contains(m.Body, RandomKeyTail) && |
|
strings.Contains(m.Body, MessageHeaderLegacy) && |
|
strings.Contains(m.Body, MessageTailLegacy) && |
|
strings.Contains(m.Body, MessageHeader) && |
|
strings.Contains(m.Body, MessageTail) |
|
} |
|
|
|
func (m *Message) Decrypt(kr *pmcrypto.KeyRing) (err error) { |
|
if m.IsLegacyMessage() { |
|
return m.DecryptLegacy(kr) |
|
} |
|
|
|
if !m.IsBodyEncrypted() { |
|
return |
|
} |
|
|
|
armored := strings.TrimSpace(m.Body) |
|
body, err := decrypt(kr, armored) |
|
if err != nil { |
|
return |
|
} |
|
|
|
m.Body = body |
|
return |
|
} |
|
|
|
func (m *Message) DecryptLegacy(kr *pmcrypto.KeyRing) (err error) { |
|
randomKeyStart := strings.Index(m.Body, RandomKeyHeader) + len(RandomKeyHeader) |
|
randomKeyEnd := strings.Index(m.Body, RandomKeyTail) |
|
randomKey := m.Body[randomKeyStart:randomKeyEnd] |
|
|
|
signedKey, err := decrypt(kr, strings.TrimSpace(randomKey)) |
|
if err != nil { |
|
return |
|
} |
|
bytesKey, err := decodeBase64UTF8(signedKey) |
|
if err != nil { |
|
return |
|
} |
|
|
|
messageStart := strings.Index(m.Body, MessageHeaderLegacy) + len(MessageHeaderLegacy) |
|
messageEnd := strings.Index(m.Body, MessageTailLegacy) |
|
message := m.Body[messageStart:messageEnd] |
|
bytesMessage, err := decodeBase64UTF8(message) |
|
if err != nil { |
|
return |
|
} |
|
|
|
block, err := aes.NewCipher(bytesKey) |
|
if err != nil { |
|
return |
|
} |
|
|
|
prefix := make([]byte, block.BlockSize()+2) |
|
bytesMessageReader := bytes.NewReader(bytesMessage) |
|
|
|
_, err = io.ReadFull(bytesMessageReader, prefix) |
|
if err != nil { |
|
return |
|
} |
|
s := packet.NewOCFBDecrypter(block, prefix, packet.OCFBResync) |
|
if s == nil { |
|
err = errors.New("pmapi: incorrect key for legacy decryption") |
|
return |
|
} |
|
|
|
reader := cipher.StreamReader{S: s, R: bytesMessageReader} |
|
buf := new(bytes.Buffer) |
|
_, _ = buf.ReadFrom(reader) |
|
plaintextBytes := buf.Bytes() |
|
|
|
plaintext := "" |
|
for i := 0; i < len(plaintextBytes); i++ { |
|
plaintext += string(plaintextBytes[i]) |
|
} |
|
bytesPlaintext, err := decodeBase64UTF8(plaintext) |
|
if err != nil { |
|
return |
|
} |
|
|
|
m.Body = string(bytesPlaintext) |
|
return err |
|
} |
|
|
|
func decodeBase64UTF8(input string) (output []byte, err error) { |
|
input = strings.TrimSpace(input) |
|
decodedMessage, err := base64.StdEncoding.DecodeString(input) |
|
if err != nil { |
|
return |
|
} |
|
utf8DecodedMessage := []rune(string(decodedMessage)) |
|
output = make([]byte, len(utf8DecodedMessage)) |
|
for i := 0; i < len(utf8DecodedMessage); i++ { |
|
output[i] = byte(int(utf8DecodedMessage[i])) |
|
} |
|
return |
|
} |
|
|
|
func (m *Message) Encrypt(encrypter, signer *pmcrypto.KeyRing) (err error) { |
|
if m.IsBodyEncrypted() { |
|
err = errors.New("pmapi: trying to encrypt an already encrypted message") |
|
return |
|
} |
|
|
|
m.Body, err = encrypt(encrypter, m.Body, signer) |
|
return |
|
} |
|
|
|
func (m *Message) Has(flag int64) bool { |
|
return (m.Flags & flag) == flag |
|
} |
|
|
|
// MessagesCount contains message counts for one label. |
|
type MessagesCount struct { |
|
LabelID string |
|
Total int |
|
Unread int |
|
} |
|
|
|
// MessagesFilter contains fields to filter messages. |
|
type MessagesFilter struct { |
|
Page int |
|
PageSize int |
|
Limit int |
|
LabelID string |
|
Sort string // Time by default (Time, To, From, Subject, Size). |
|
Desc *bool |
|
Begin int64 // Unix time. |
|
End int64 // Unix time. |
|
BeginID string |
|
EndID string |
|
Keyword string |
|
To string |
|
From string |
|
Subject string |
|
ConversationID string |
|
AddressID string |
|
ID []string |
|
Attachments *bool |
|
Unread *bool |
|
ExternalID string // MIME Message-Id (only valid for messages). |
|
AutoWildcard *bool |
|
} |
|
|
|
func (filter *MessagesFilter) urlValues() url.Values { // nolint[funlen] |
|
v := url.Values{} |
|
|
|
if filter.Page != 0 { |
|
v.Set("Page", strconv.Itoa(filter.Page)) |
|
} |
|
if filter.PageSize != 0 { |
|
v.Set("PageSize", strconv.Itoa(filter.PageSize)) |
|
} |
|
if filter.Limit != 0 { |
|
v.Set("Limit", strconv.Itoa(filter.Limit)) |
|
} |
|
if filter.LabelID != "" { |
|
v.Set("LabelID", filter.LabelID) |
|
} |
|
if filter.Sort != "" { |
|
v.Set("Sort", filter.Sort) |
|
} |
|
if filter.Desc != nil { |
|
if *filter.Desc { |
|
v.Set("Desc", "1") |
|
} else { |
|
v.Set("Desc", "0") |
|
} |
|
} |
|
if filter.Begin != 0 { |
|
v.Set("Begin", strconv.Itoa(int(filter.Begin))) |
|
} |
|
if filter.End != 0 { |
|
v.Set("End", strconv.Itoa(int(filter.End))) |
|
} |
|
if filter.BeginID != "" { |
|
v.Set("BeginID", filter.BeginID) |
|
} |
|
if filter.EndID != "" { |
|
v.Set("EndID", filter.EndID) |
|
} |
|
if filter.Keyword != "" { |
|
v.Set("Keyword", filter.Keyword) |
|
} |
|
if filter.To != "" { |
|
v.Set("To", filter.To) |
|
} |
|
if filter.From != "" { |
|
v.Set("From", filter.From) |
|
} |
|
if filter.Subject != "" { |
|
v.Set("Subject", filter.Subject) |
|
} |
|
if filter.ConversationID != "" { |
|
v.Set("ConversationID", filter.ConversationID) |
|
} |
|
if filter.AddressID != "" { |
|
v.Set("AddressID", filter.AddressID) |
|
} |
|
if len(filter.ID) > 0 { |
|
for _, id := range filter.ID { |
|
v.Add("ID[]", id) |
|
} |
|
} |
|
if filter.Attachments != nil { |
|
if *filter.Attachments { |
|
v.Set("Attachments", "1") |
|
} else { |
|
v.Set("Attachments", "0") |
|
} |
|
} |
|
if filter.Unread != nil { |
|
if *filter.Unread { |
|
v.Set("Unread", "1") |
|
} else { |
|
v.Set("Unread", "0") |
|
} |
|
} |
|
if filter.ExternalID != "" { |
|
v.Set("ExternalID", filter.ExternalID) |
|
} |
|
if filter.AutoWildcard != nil { |
|
if *filter.AutoWildcard { |
|
v.Set("AutoWildcard", "1") |
|
} else { |
|
v.Set("AutoWildcard", "0") |
|
} |
|
} |
|
|
|
return v |
|
} |
|
|
|
type MessagesListRes struct { |
|
Res |
|
|
|
Total int |
|
Messages []*Message |
|
} |
|
|
|
// ListMessages gets message metadata. |
|
func (c *client) ListMessages(filter *MessagesFilter) (msgs []*Message, total int, err error) { |
|
req, err := c.NewRequest("GET", "/messages", nil) |
|
if err != nil { |
|
return |
|
} |
|
|
|
req.URL.RawQuery = filter.urlValues().Encode() |
|
var res MessagesListRes |
|
if err = c.DoJSON(req, &res); err != nil { |
|
// If the URI was too long and we searched with IDs, we will try again without the API IDs. |
|
if strings.Contains(err.Error(), "api returned: 414") && len(filter.ID) > 0 { |
|
filter.ID = []string{} |
|
return c.ListMessages(filter) |
|
} |
|
return |
|
} |
|
|
|
msgs, total, err = res.Messages, res.Total, res.Err() |
|
return |
|
} |
|
|
|
type MessagesCountsRes struct { |
|
Res |
|
|
|
Counts []*MessagesCount |
|
} |
|
|
|
// CountMessages counts messages by label. |
|
func (c *client) CountMessages(addressID string) (counts []*MessagesCount, err error) { |
|
reqURL := "/messages/count" |
|
if addressID != "" { |
|
reqURL += ("?AddressID=" + addressID) |
|
} |
|
req, err := c.NewRequest("GET", reqURL, nil) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var res MessagesCountsRes |
|
if err = c.DoJSON(req, &res); err != nil { |
|
return |
|
} |
|
|
|
counts, err = res.Counts, res.Err() |
|
return |
|
} |
|
|
|
type MessageRes struct { |
|
Res |
|
|
|
Message *Message |
|
} |
|
|
|
// GetMessage retrieves a message. |
|
func (c *client) GetMessage(id string) (msg *Message, err error) { |
|
req, err := c.NewRequest("GET", "/messages/"+id, nil) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var res MessageRes |
|
if err = c.DoJSON(req, &res); err != nil { |
|
return |
|
} |
|
|
|
return res.Message, res.Err() |
|
} |
|
|
|
type SendMessageReq struct { |
|
ExpirationTime int64 `json:",omitempty"` |
|
// AutoSaveContacts int `json:",omitempty"` |
|
|
|
// Data for encrypted recipients. |
|
Packages []*MessagePackage |
|
} |
|
|
|
// Message package types. |
|
const ( |
|
InternalPackage = 1 |
|
EncryptedOutsidePackage = 2 |
|
ClearPackage = 4 |
|
PGPInlinePackage = 8 |
|
PGPMIMEPackage = 16 |
|
ClearMIMEPackage = 32 |
|
) |
|
|
|
// Signature types. |
|
const ( |
|
NoSignature = 0 |
|
YesSignature = 1 |
|
) |
|
|
|
type MessagePackage struct { |
|
Addresses map[string]*MessageAddress |
|
Type int |
|
MIMEType string |
|
Body string // base64-encoded encrypted data packet. |
|
BodyKey AlgoKey // base64-encoded session key (only if cleartext recipients). |
|
AttachmentKeys map[string]AlgoKey // Only include if cleartext & attachments. |
|
} |
|
|
|
type MessageAddress struct { |
|
Type int |
|
BodyKeyPacket string // base64-encoded key packet. |
|
Signature int // 0 = None, 1 = Detached, 2 = Attached/Armored |
|
AttachmentKeyPackets map[string]string |
|
} |
|
|
|
type AlgoKey struct { |
|
Key string |
|
Algorithm string |
|
} |
|
|
|
type SendMessageRes struct { |
|
Res |
|
|
|
Sent *Message |
|
|
|
// Parent is only present if the sent message has a parent (reply/reply all/forward). |
|
Parent *Message |
|
} |
|
|
|
func (c *client) SendMessage(id string, sendReq *SendMessageReq) (sent, parent *Message, err error) { |
|
if id == "" { |
|
err = errors.New("pmapi: cannot send message with an empty id") |
|
return |
|
} |
|
|
|
if sendReq.Packages == nil { |
|
sendReq.Packages = []*MessagePackage{} |
|
} |
|
|
|
req, err := c.NewJSONRequest("POST", "/messages/"+id, sendReq) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var res SendMessageRes |
|
if err = c.DoJSON(req, &res); err != nil { |
|
return |
|
} |
|
|
|
sent, parent, err = res.Sent, res.Parent, res.Err() |
|
return |
|
} |
|
|
|
const ( |
|
DraftActionReply = 0 |
|
DraftActionReplyAll = 1 |
|
DraftActionForward = 2 |
|
) |
|
|
|
type DraftReq struct { |
|
Message *Message |
|
ParentID string `json:",omitempty"` |
|
Action int |
|
AttachmentKeyPackets []string |
|
} |
|
|
|
func (c *client) CreateDraft(m *Message, parent string, action int) (created *Message, err error) { |
|
createReq := &DraftReq{Message: m, ParentID: parent, Action: action, AttachmentKeyPackets: []string{}} |
|
|
|
req, err := c.NewJSONRequest("POST", "/messages", createReq) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var res MessageRes |
|
if err = c.DoJSON(req, &res); err != nil { |
|
return |
|
} |
|
|
|
created, err = res.Message, res.Err() |
|
return |
|
} |
|
|
|
type MessagesActionReq struct { |
|
IDs []string |
|
} |
|
|
|
type MessagesActionRes struct { |
|
Res |
|
|
|
Responses []struct { |
|
ID string |
|
Response Res |
|
} |
|
} |
|
|
|
func (res MessagesActionRes) Err() error { |
|
if err := res.Res.Err(); err != nil { |
|
return err |
|
} |
|
|
|
for _, msgRes := range res.Responses { |
|
if err := msgRes.Response.Err(); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// doMessagesAction performs paged requests to doMessagesActionInner. |
|
// This can eventually be done in parallel though. |
|
func (c *client) doMessagesAction(action string, ids []string) (err error) { |
|
for len(ids) > messageIDPageSize { |
|
var requestIDs []string |
|
requestIDs, ids = ids[:messageIDPageSize], ids[messageIDPageSize:] |
|
if err = c.doMessagesActionInner(action, requestIDs); err != nil { |
|
return |
|
} |
|
} |
|
|
|
return c.doMessagesActionInner(action, ids) |
|
} |
|
|
|
// doMessagesActionInner is the non-paged inner method of doMessagesAction. |
|
// You should not call this directly unless you know what you are doing (it can overload the server). |
|
func (c *client) doMessagesActionInner(action string, ids []string) (err error) { |
|
actionReq := &MessagesActionReq{IDs: ids} |
|
req, err := c.NewJSONRequest("PUT", "/messages/"+action, actionReq) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var res MessagesActionRes |
|
if err = c.DoJSON(req, &res); err != nil { |
|
return |
|
} |
|
|
|
err = res.Err() |
|
|
|
return |
|
} |
|
|
|
func (c *client) MarkMessagesRead(ids []string) error { |
|
return c.doMessagesAction("read", ids) |
|
} |
|
|
|
func (c *client) MarkMessagesUnread(ids []string) error { |
|
return c.doMessagesAction("unread", ids) |
|
} |
|
|
|
func (c *client) DeleteMessages(ids []string) error { |
|
return c.doMessagesAction("delete", ids) |
|
} |
|
|
|
func (c *client) UndeleteMessages(ids []string) error { |
|
return c.doMessagesAction("undelete", ids) |
|
} |
|
|
|
type LabelMessagesReq struct { |
|
LabelID string |
|
IDs []string |
|
} |
|
|
|
// LabelMessages labels the given message IDs with the given label. |
|
// The requests are performed paged; this can eventually be done in parallel. |
|
func (c *client) LabelMessages(ids []string, label string) (err error) { |
|
for len(ids) > messageIDPageSize { |
|
var requestIDs []string |
|
requestIDs, ids = ids[:messageIDPageSize], ids[messageIDPageSize:] |
|
if err = c.labelMessages(requestIDs, label); err != nil { |
|
return |
|
} |
|
} |
|
|
|
return c.labelMessages(ids, label) |
|
} |
|
|
|
func (c *client) labelMessages(ids []string, label string) (err error) { |
|
labelReq := &LabelMessagesReq{LabelID: label, IDs: ids} |
|
req, err := c.NewJSONRequest("PUT", "/messages/label", labelReq) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var res MessagesActionRes |
|
if err = c.DoJSON(req, &res); err != nil { |
|
return |
|
} |
|
|
|
err = res.Err() |
|
return |
|
} |
|
|
|
// UnlabelMessages removes the given label from the given message IDs. |
|
// The requests are performed paged; this can eventually be done in parallel. |
|
func (c *client) UnlabelMessages(ids []string, label string) (err error) { |
|
for len(ids) > messageIDPageSize { |
|
var requestIDs []string |
|
requestIDs, ids = ids[:messageIDPageSize], ids[messageIDPageSize:] |
|
if err = c.unlabelMessages(requestIDs, label); err != nil { |
|
return |
|
} |
|
} |
|
|
|
return c.unlabelMessages(ids, label) |
|
} |
|
|
|
func (c *client) unlabelMessages(ids []string, label string) (err error) { |
|
labelReq := &LabelMessagesReq{LabelID: label, IDs: ids} |
|
req, err := c.NewJSONRequest("PUT", "/messages/unlabel", labelReq) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var res MessagesActionRes |
|
if err = c.DoJSON(req, &res); err != nil { |
|
return |
|
} |
|
|
|
err = res.Err() |
|
return |
|
} |
|
|
|
func (c *client) EmptyFolder(labelID, addressID string) (err error) { |
|
if labelID == "" { |
|
return errors.New("pmapi: labelID parameter is empty string") |
|
} |
|
reqURL := "/messages/empty?LabelID=" + labelID |
|
if addressID != "" { |
|
reqURL += ("&AddressID=" + addressID) |
|
} |
|
|
|
req, err := c.NewRequest("DELETE", reqURL, nil) |
|
|
|
if err != nil { |
|
return |
|
} |
|
|
|
var res Res |
|
if err = c.DoJSON(req, &res); err != nil { |
|
return |
|
} |
|
|
|
err = res.Err() |
|
return |
|
}
|
|
|