hydroxide-push/protonmail/messages.go

606 lines
14 KiB
Go
Raw Normal View History

package protonmail
import (
"bytes"
"encoding/base64"
"errors"
"io"
"net/http"
2018-01-08 00:38:13 +02:00
"net/url"
"strconv"
"strings"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
type MessageType int
const (
MessageInbox MessageType = iota
MessageDraft
MessageSent
MessageInboxAndSent
)
type MessageEncryption int
const (
MessageUnencrypted MessageEncryption = iota
MessageEncryptedInternal
MessageEncryptedExternal
MessageEncryptedOutside
_
_
_
MessageEncryptedInlinePGP
MessageEncryptedPGPMIME
_
_
)
type MessageAction int
const (
MessageReply MessageAction = iota
MessageReplyAll
MessageForward
)
type MessageAddress struct {
Address string
2017-09-19 15:54:47 +03:00
Name string
}
type Message struct {
ID string `json:",omitempty"`
2017-09-19 15:54:47 +03:00
Order int64
ConversationID string `json:",omitempty"`
2017-09-19 15:54:47 +03:00
Subject string
Unread int
2017-09-19 15:54:47 +03:00
Type MessageType
Sender *MessageAddress
ToList []*MessageAddress
2020-09-14 13:09:50 +03:00
Time Timestamp
2017-09-19 15:54:47 +03:00
Size int64
2018-01-08 11:30:45 +02:00
NumAttachments int
2017-09-19 15:54:47 +03:00
IsEncrypted MessageEncryption
2020-09-14 13:09:50 +03:00
ExpirationTime Timestamp
2017-09-19 15:54:47 +03:00
IsReplied int
IsRepliedAll int
IsForwarded int
SpamScore int
AddressID string
Body string
2017-12-03 12:55:59 +02:00
MIMEType string `json:",omitempty"`
2017-09-19 15:54:47 +03:00
CCList []*MessageAddress
BCCList []*MessageAddress
ReplyTos []*MessageAddress
Header string `json:",omitempty"`
2017-09-19 15:54:47 +03:00
Attachments []*Attachment
LabelIDs []string
ExternalID string `json:",omitempty"`
}
func (msg *Message) Read(keyring openpgp.KeyRing, prompt openpgp.PromptFunction) (*openpgp.MessageDetails, error) {
switch msg.IsEncrypted {
case MessageUnencrypted:
return &openpgp.MessageDetails{
IsEncrypted: false,
IsSigned: false,
UnverifiedBody: strings.NewReader(msg.Body),
}, nil
default:
block, err := armor.Decode(strings.NewReader(msg.Body))
if err != nil {
return nil, err
}
return openpgp.ReadMessage(block.Body, keyring, prompt, nil)
}
}
type messageWriter struct {
plaintext io.WriteCloser
ciphertext io.WriteCloser
b *bytes.Buffer
msg *Message
}
func (w *messageWriter) Write(p []byte) (n int, err error) {
return w.plaintext.Write(p)
}
func (w *messageWriter) Close() error {
if err := w.plaintext.Close(); err != nil {
return err
}
if err := w.ciphertext.Close(); err != nil {
return err
}
w.msg.Body = w.b.String()
return nil
}
func (msg *Message) Encrypt(to []*openpgp.Entity, signed *openpgp.Entity) (plaintext io.WriteCloser, err error) {
var b bytes.Buffer
ciphertext, err := armor.Encode(&b, "PGP MESSAGE", nil)
if err != nil {
return nil, err
}
plaintext, err = openpgp.Encrypt(ciphertext, to, signed, nil, nil)
if err != nil {
return nil, err
}
return &messageWriter{
plaintext: plaintext,
ciphertext: ciphertext,
b: &b,
msg: msg,
}, nil
}
2018-01-08 00:38:13 +02:00
type MessageFilter struct {
2018-10-21 13:15:20 +03:00
Page int
2018-01-08 00:38:13 +02:00
PageSize int
2018-10-21 13:15:20 +03:00
Limit int
Label string
Sort string
Asc bool
Begin int64
End int64
Keyword string
To string
From string
Subject string
Attachments *bool
Starred *bool
Unread *bool
2018-01-08 00:38:13 +02:00
Conversation string
2018-10-21 13:15:20 +03:00
AddressID string
ID []string
ExternalID string
2018-01-08 00:38:13 +02:00
}
func (c *Client) ListMessages(filter *MessageFilter) (total int, messages []*Message, err error) {
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.Label != "" {
v.Set("Label", filter.Label)
}
if filter.Sort != "" {
v.Set("Sort", filter.Sort)
}
if filter.Asc {
v.Set("Desc", "0")
}
if filter.Conversation != "" {
v.Set("Conversation", filter.Conversation)
}
2018-07-11 13:48:45 +03:00
if filter.AddressID != "" {
v.Set("AddressID", filter.AddressID)
2018-01-08 00:38:13 +02:00
}
if filter.ExternalID != "" {
v.Set("ExternalID", filter.ExternalID)
}
req, err := c.newRequest(http.MethodGet, "/messages?"+v.Encode(), nil)
if err != nil {
return 0, nil, err
}
var respData struct {
resp
2018-10-21 13:15:20 +03:00
Total int
2018-01-08 00:38:13 +02:00
Messages []*Message
}
if err := c.doJSON(req, &respData); err != nil {
return 0, nil, err
}
return respData.Total, respData.Messages, nil
}
type MessageCount struct {
LabelID string
2018-10-21 13:15:20 +03:00
Total int
Unread int
2018-01-08 00:38:13 +02:00
}
func (c *Client) CountMessages(address string) ([]*MessageCount, error) {
v := url.Values{}
if address != "" {
v.Set("Address", address)
}
req, err := c.newRequest(http.MethodGet, "/messages/count?"+v.Encode(), nil)
if err != nil {
return nil, err
}
var respData struct {
resp
Counts []*MessageCount
}
if err := c.doJSON(req, &respData); err != nil {
return nil, err
}
return respData.Counts, nil
}
func (c *Client) GetMessage(id string) (*Message, error) {
req, err := c.newRequest(http.MethodGet, "/messages/"+id, nil)
if err != nil {
return nil, err
}
var respData struct {
resp
Message *Message
}
if err := c.doJSON(req, &respData); err != nil {
return nil, err
}
return respData.Message, nil
}
// CreateDraftMessage creates a new draft message. ToList, CCList, BCCList,
// Subject, Body and AddressID are required in msg.
func (c *Client) CreateDraftMessage(msg *Message, parentID string) (*Message, error) {
var actionPtr *MessageAction
if parentID != "" {
// TODO: support other actions
action := MessageReply
actionPtr = &action
}
reqData := struct {
2017-09-19 15:54:47 +03:00
Message *Message
ParentID string `json:",omitempty"`
Action *MessageAction `json:",omitempty"`
}{msg, parentID, actionPtr}
req, err := c.newJSONRequest(http.MethodPost, "/messages", &reqData)
if err != nil {
return nil, err
}
var respData struct {
resp
Message *Message
}
if err := c.doJSON(req, &respData); err != nil {
return nil, err
}
return respData.Message, nil
}
func (c *Client) UpdateDraftMessage(msg *Message) (*Message, error) {
reqData := struct {
Message *Message
}{msg}
req, err := c.newJSONRequest(http.MethodPut, "/messages/"+msg.ID, &reqData)
if err != nil {
return nil, err
}
var respData struct {
resp
Message *Message
}
if err := c.doJSON(req, &respData); err != nil {
return nil, err
}
return respData.Message, nil
}
2018-01-12 20:28:54 +02:00
func (c *Client) doMessages(action string, ids []string) error {
reqData := struct {
IDs []string
}{ids}
req, err := c.newJSONRequest(http.MethodPut, "/messages/"+action, &reqData)
if err != nil {
return err
}
// TODO: the response contains one response per message
return c.doJSON(req, nil)
}
func (c *Client) MarkMessagesRead(ids []string) error {
return c.doMessages("read", ids)
}
func (c *Client) MarkMessagesUnread(ids []string) error {
return c.doMessages("unread", ids)
}
func (c *Client) DeleteMessages(ids []string) error {
return c.doMessages("delete", ids)
}
func (c *Client) UndeleteMessages(ids []string) error {
return c.doMessages("undelete", ids)
}
func (c *Client) LabelMessages(labelID string, ids []string) error {
reqData := struct {
LabelID string
2018-10-21 13:15:20 +03:00
IDs []string
2018-01-12 20:28:54 +02:00
}{labelID, ids}
req, err := c.newJSONRequest(http.MethodPut, "/messages/label", &reqData)
if err != nil {
return err
}
// TODO: the response contains one response per message
return c.doJSON(req, nil)
}
func (c *Client) UnlabelMessages(labelID string, ids []string) error {
reqData := struct {
LabelID string
2018-10-21 13:15:20 +03:00
IDs []string
2018-01-12 20:28:54 +02:00
}{labelID, ids}
req, err := c.newJSONRequest(http.MethodPut, "/messages/unlabel", &reqData)
if err != nil {
return err
}
// TODO: the response contains one response per message
return c.doJSON(req, nil)
}
type MessageKeyPacket struct {
2017-09-19 15:54:47 +03:00
ID string
KeyPackets string
}
type MessagePackageType int
2017-09-21 21:02:54 +03:00
const (
MessagePackageInternal MessagePackageType = 1
MessagePackageEncryptedOutside = 2
MessagePackageCleartext = 4
2017-09-21 21:02:54 +03:00
MessagePackageInlinePGP = 8
MessagePackagePGPMIME = 16
MessagePackageMIME = 32
)
// From https://github.com/ProtonMail/WebClient/blob/public/src/app/composer/services/encryptMessage.js
type MessagePackage struct {
Type MessagePackageType
2017-12-03 12:55:59 +02:00
BodyKeyPacket string
AttachmentKeyPackets map[string]string
2017-12-03 12:55:59 +02:00
Signature int
// Only if encrypted for outside
PasswordHint string
2017-12-03 12:55:59 +02:00
Auth interface{} // TODO
Token string
EncToken string `json:",omitempty"`
}
type MessagePackageSet struct {
2017-12-03 12:55:59 +02:00
Type MessagePackageType // OR of each Type
Addresses map[string]*MessagePackage
2017-12-03 12:55:59 +02:00
MIMEType string
Body string // Encrypted body data packet
// Only if cleartext is sent
BodyKey *PackedKey `json:",omitempty"`
AttachmentKeys map[string]*PackedKey `json:",omitempty"`
2017-12-03 12:55:59 +02:00
bodyKey *packet.EncryptedKey
attachmentKeys map[string]*packet.EncryptedKey
signature int
}
type PackedKey struct {
2018-10-21 13:15:20 +03:00
Algorithm string
Key string
}
func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *MessagePackageSet {
return &MessagePackageSet{
2017-12-03 12:55:59 +02:00
Addresses: make(map[string]*MessagePackage),
attachmentKeys: attachmentKeys,
}
}
type outgoingMessageWriter struct {
2017-12-03 12:55:59 +02:00
cleartext io.WriteCloser
ciphertext io.WriteCloser
2017-12-03 12:55:59 +02:00
encoded *bytes.Buffer
set *MessagePackageSet
}
func (w *outgoingMessageWriter) Write(p []byte) (int, error) {
return w.cleartext.Write(p)
}
func (w *outgoingMessageWriter) Close() error {
if err := w.cleartext.Close(); err != nil {
return err
}
if err := w.ciphertext.Close(); err != nil {
return err
}
2017-12-02 17:23:06 +02:00
w.set.Body = w.encoded.String()
w.encoded = nil
return nil
}
2017-12-03 12:55:59 +02:00
// Encrypt encrypts the data that will be written to the returned
// io.WriteCloser, and optionally signs it.
2017-12-03 12:55:59 +02:00
func (set *MessagePackageSet) Encrypt(mimeType string, signed *openpgp.Entity) (io.WriteCloser, error) {
2017-12-02 17:23:06 +02:00
set.MIMEType = mimeType
config := &packet.Config{}
2017-12-03 12:55:59 +02:00
key, err := generateUnencryptedKey(packet.CipherAES256, config)
if err != nil {
return nil, err
}
2017-12-03 12:55:59 +02:00
set.bodyKey = key
var signer *packet.PrivateKey
if signed != nil {
signKey, ok := signingKey(signed, config.Now())
if !ok {
return nil, errors.New("no valid signing keys")
}
signer = signKey.PrivateKey
if signer == nil {
return nil, errors.New("no private key in signing key")
}
if signer.Encrypted {
return nil, errors.New("signing key must be decrypted")
}
set.signature = 1
}
encoded := new(bytes.Buffer)
ciphertext := base64.NewEncoder(base64.StdEncoding, encoded)
cleartext, err := symetricallyEncrypt(ciphertext, key, signer, nil, config)
if err != nil {
return nil, err
}
return &outgoingMessageWriter{
cleartext: cleartext,
ciphertext: ciphertext,
encoded: encoded,
2017-12-03 12:55:59 +02:00
set: set,
}, nil
}
2018-10-21 15:05:26 +03:00
func cipherFunctionString(cipherFunc packet.CipherFunction) string {
switch cipherFunc {
case packet.CipherAES128:
return "aes128"
case packet.CipherAES192:
return "aes192"
case packet.CipherAES256:
return "aes256"
default:
panic("protonmail: unsupported cipher function")
}
}
func (set *MessagePackageSet) AddCleartext(addr string) (*MessagePackage, error) {
pkg := &MessagePackage{
2018-10-21 13:15:20 +03:00
Type: MessagePackageCleartext,
Signature: set.signature,
}
set.Addresses[addr] = pkg
2017-12-02 17:23:06 +02:00
set.Type |= MessagePackageCleartext
if set.BodyKey == nil || set.AttachmentKeys == nil {
set.BodyKey = &PackedKey{
2018-10-21 15:05:26 +03:00
Algorithm: cipherFunctionString(set.bodyKey.CipherFunc),
2018-10-21 13:15:20 +03:00
Key: base64.StdEncoding.EncodeToString(set.bodyKey.Key),
}
set.AttachmentKeys = make(map[string]*PackedKey, len(set.attachmentKeys))
for att, key := range set.attachmentKeys {
set.AttachmentKeys[att] = &PackedKey{
Algorithm: cipherFunctionString(key.CipherFunc),
Key: base64.StdEncoding.EncodeToString(key.Key),
}
}
}
return pkg, nil
}
func serializeEncryptedKey(symKey *packet.EncryptedKey, pub *packet.PublicKey, config *packet.Config) (string, error) {
2017-12-03 11:20:45 +02:00
var encoded bytes.Buffer
ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded)
2017-12-03 11:20:45 +02:00
err := packet.SerializeEncryptedKey(ciphertext, pub, symKey.CipherFunc, symKey.Key, config)
if err != nil {
return "", err
}
2017-12-03 11:20:45 +02:00
ciphertext.Close()
return encoded.String(), nil
}
func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) (*MessagePackage, error) {
config := &packet.Config{}
encKey, ok := encryptionKey(pub, config.Now())
if !ok {
return nil, errors.New("cannot encrypt a message to key id " + strconv.FormatUint(pub.PrimaryKey.KeyId, 16) + " because it has no encryption keys")
}
bodyKey, err := serializeEncryptedKey(set.bodyKey, encKey.PublicKey, config)
if err != nil {
return nil, err
}
attachmentKeys := make(map[string]string, len(set.attachmentKeys))
for att, key := range set.attachmentKeys {
attKey, err := serializeEncryptedKey(key, encKey.PublicKey, config)
if err != nil {
return nil, err
}
attachmentKeys[att] = attKey
}
2017-12-02 17:23:06 +02:00
set.Type |= MessagePackageInternal
pkg := &MessagePackage{
2017-12-03 12:55:59 +02:00
Type: MessagePackageInternal,
BodyKeyPacket: bodyKey,
AttachmentKeyPackets: attachmentKeys,
Signature: set.signature,
}
set.Addresses[addr] = pkg
return pkg, nil
}
type OutgoingMessage struct {
ID string
2017-09-21 21:02:54 +03:00
// Only if message expires
ExpirationTime int // Duration in seconds
2017-09-21 21:02:54 +03:00
Packages []*MessagePackageSet
}
func (c *Client) SendMessage(msg *OutgoingMessage) (sent, parent *Message, err error) {
req, err := c.newJSONRequest(http.MethodPost, "/messages/"+msg.ID, msg)
if err != nil {
return nil, nil, err
}
var respData struct {
resp
Sent, Parent *Message
}
if err := c.doJSON(req, &respData); err != nil {
return nil, nil, err
}
return respData.Sent, respData.Parent, nil
}