diff --git a/internal/smtp/user.go b/internal/smtp/user.go index 37957249..3f4bf5de 100644 --- a/internal/smtp/user.go +++ b/internal/smtp/user.go @@ -187,7 +187,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err log.WithError(err).Error("Failed to parse message") return } - clearBody := message.Body + richBody := message.Body externalID := message.Header.Get("Message-Id") externalID = strings.Trim(externalID, "<>") @@ -256,7 +256,6 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err atts = append(atts, message.Attachments...) // Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys. attkeys := make(map[string]*crypto.SessionKey) - attkeysEncoded := make(map[string]pmapi.AlgoKey) for _, att := range atts { var keyPackets []byte @@ -266,23 +265,9 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err if attkeys[att.ID], err = kr.DecryptSessionKey(keyPackets); err != nil { return errors.Wrap(err, "decrypting attachment session key") } - attkeysEncoded[att.ID] = pmapi.AlgoKey{ - Key: attkeys[att.ID].GetBase64Key(), - Algorithm: attkeys[att.ID].Algo, - } } - plainSharedScheme := 0 - htmlSharedScheme := 0 - mimeSharedType := 0 - - plainAddressMap := make(map[string]*pmapi.MessageAddress) - htmlAddressMap := make(map[string]*pmapi.MessageAddress) - mimeAddressMap := make(map[string]*pmapi.MessageAddress) - - var plainKey, htmlKey, mimeKey *crypto.SessionKey - var plainData, htmlData, mimeData []byte - + req := pmapi.NewSendMessageReq(kr, mimeBody, plainBody, richBody, attkeys) containsUnencryptedRecipients := false for _, email := range to { @@ -300,59 +285,13 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err var signature int if sendPreferences.Sign { - signature = pmapi.YesSignature + signature = pmapi.SignatureDetached } else { - signature = pmapi.NoSignature + signature = pmapi.SignatureNone } - if sendPreferences.Scheme == pmapi.PGPMIMEPackage || sendPreferences.Scheme == pmapi.ClearMIMEPackage { - if mimeKey == nil { - if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil { - return err - } - } - if sendPreferences.Scheme == pmapi.PGPMIMEPackage { - mimeBodyPacket, _, err := createPackets(sendPreferences.PublicKey, mimeKey, map[string]*crypto.SessionKey{}) - if err != nil { - return err - } - mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature} - } else { - mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature} - } - mimeSharedType |= sendPreferences.Scheme - } else { - switch sendPreferences.MIMEType { - case pmapi.ContentTypePlainText: - if plainKey == nil { - if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil { - return err - } - } - newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature} - if sendPreferences.Encrypt && sendPreferences.PublicKey != nil { - newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, plainKey, attkeys) - if err != nil { - return err - } - } - plainAddressMap[email] = newAddress - plainSharedScheme |= sendPreferences.Scheme - case pmapi.ContentTypeHTML: - if htmlKey == nil { - if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil { - return err - } - } - newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature} - if sendPreferences.Encrypt && sendPreferences.PublicKey != nil { - newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, htmlKey, attkeys) - if err != nil { - return err - } - } - htmlAddressMap[email] = newAddress - htmlSharedScheme |= sendPreferences.Scheme - } + + if err := req.AddRecipient(email, sendPreferences.Scheme, sendPreferences.PublicKey, signature, sendPreferences.MIMEType, sendPreferences.Encrypt); err != nil { + return errors.Wrap(err, "failed to add recipient") } } @@ -370,31 +309,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err } } - req := &pmapi.SendMessageReq{} - - plainPkg := buildPackage(plainAddressMap, plainSharedScheme, pmapi.ContentTypePlainText, plainData, plainKey, attkeysEncoded) - if plainPkg != nil { - req.Packages = append(req.Packages, plainPkg) - } - - htmlPkg := buildPackage(htmlAddressMap, htmlSharedScheme, pmapi.ContentTypeHTML, htmlData, htmlKey, attkeysEncoded) - if htmlPkg != nil { - req.Packages = append(req.Packages, htmlPkg) - } - - if len(mimeAddressMap) > 0 { - pkg := &pmapi.MessagePackage{ - Body: base64.StdEncoding.EncodeToString(mimeData), - Addresses: mimeAddressMap, - MIMEType: pmapi.ContentTypeMultipartMixed, - Type: mimeSharedType, - BodyKey: pmapi.AlgoKey{ - Key: mimeKey.GetBase64Key(), - Algorithm: mimeKey.Algo, - }, - } - req.Packages = append(req.Packages, pkg) - } + req.PreparePackages() return su.storeUser.SendMessage(message.ID, req) } diff --git a/internal/smtp/utils.go b/internal/smtp/utils.go index 36b98171..745ff2dc 100644 --- a/internal/smtp/utils.go +++ b/internal/smtp/utils.go @@ -18,11 +18,7 @@ package smtp import ( - "encoding/base64" "regexp" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) //nolint:gochecknoglobals // Used like a constant @@ -35,85 +31,3 @@ var mailFormat = regexp.MustCompile(`.+@.+\..+`) func looksLikeEmail(e string) bool { return mailFormat.MatchString(e) } - -func createPackets( - pubkey *crypto.KeyRing, - bodyKey *crypto.SessionKey, - attkeys map[string]*crypto.SessionKey, -) (bodyPacket string, attachmentPackets map[string]string, err error) { - // Encrypt message body keys. - packetBytes, err := pubkey.EncryptSessionKey(bodyKey) - if err != nil { - return - } - bodyPacket = base64.StdEncoding.EncodeToString(packetBytes) - - // Encrypt attachment keys. - attachmentPackets = make(map[string]string) - for id, attkey := range attkeys { - var packets []byte - if packets, err = pubkey.EncryptSessionKey(attkey); err != nil { - return - } - attachmentPackets[id] = base64.StdEncoding.EncodeToString(packets) - } - return -} - -func encryptSymmetric( - kr *crypto.KeyRing, - textToEncrypt string, - canonicalizeText bool, // nolint[unparam] -) (key *crypto.SessionKey, symEncryptedData []byte, err error) { - // We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones). - firstKey, err := kr.FirstKey() - if err != nil { - return - } - - pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr) - if err != nil { - return - } - - pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0) - if err != nil { - return - } - - key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket()) - if err != nil { - return - } - - symEncryptedData = pgpSplitMessage.GetBinaryDataPacket() - - return -} - -func buildPackage( - addressMap map[string]*pmapi.MessageAddress, - sharedScheme int, - mimeType string, - bodyData []byte, - bodyKey *crypto.SessionKey, - attKeys map[string]pmapi.AlgoKey, -) (pkg *pmapi.MessagePackage) { - if len(addressMap) == 0 { - return nil - } - - pkg = &pmapi.MessagePackage{ - Body: base64.StdEncoding.EncodeToString(bodyData), - Addresses: addressMap, - MIMEType: mimeType, - Type: sharedScheme, - } - - if sharedScheme|pmapi.ClearPackage > 0 { - pkg.BodyKey.Key = bodyKey.GetBase64Key() - pkg.BodyKey.Algorithm = bodyKey.Algo - pkg.AttachmentKeys = attKeys - } - return pkg -} diff --git a/pkg/pmapi/client.go b/pkg/pmapi/client.go index 3e1c9cea..3f513b08 100644 --- a/pkg/pmapi/client.go +++ b/pkg/pmapi/client.go @@ -254,7 +254,7 @@ func (c *client) doBuffered(req *http.Request, bodyBuffer []byte, retryUnauthori head += "\n" } c.log.Tracef("REQHEAD \n%s", head) - c.log.Tracef("REQBODY '%s'", string(bodyBuffer)) + c.log.Tracef("REQBODY '%s'", printBytes(bodyBuffer)) } hasBody := len(bodyBuffer) > 0 diff --git a/pkg/pmapi/debug.go b/pkg/pmapi/debug.go new file mode 100644 index 00000000..6f74bc59 --- /dev/null +++ b/pkg/pmapi/debug.go @@ -0,0 +1,43 @@ +// 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 . + +package pmapi + +import "unicode/utf8" + +func printBytes(body []byte) string { + if utf8.Valid(body) { + return string(body) + } + enc := []rune{} + for _, b := range body { + switch { + case b == 9: + enc = append(enc, rune('⟼')) + case b == 13: + enc = append(enc, rune('↵')) + case b < 32, b == 127: + enc = append(enc, '◡') + case b > 31 && b < 127, b == 10: + enc = append(enc, rune(b)) + default: + enc = append(enc, 9728+rune(b)) + } + } + + return string(enc) +} diff --git a/pkg/pmapi/keyring.go b/pkg/pmapi/keyring.go index 978ac6e7..23169b7e 100644 --- a/pkg/pmapi/keyring.go +++ b/pkg/pmapi/keyring.go @@ -19,6 +19,7 @@ package pmapi import ( "bytes" + "encoding/base64" "encoding/json" "io" "io/ioutil" @@ -289,3 +290,57 @@ func signAttachment(encrypter *crypto.KeyRing, data io.Reader) (signature io.Rea } return bytes.NewReader(sig.GetBinary()), nil } + +func createPackets( + pubkey *crypto.KeyRing, + bodyKey *crypto.SessionKey, + attkeys map[string]*crypto.SessionKey, +) (bodyPacket string, attachmentPackets map[string]string, err error) { + // Encrypt message body keys. + packetBytes, err := pubkey.EncryptSessionKey(bodyKey) + if err != nil { + return + } + bodyPacket = base64.StdEncoding.EncodeToString(packetBytes) + + // Encrypt attachment keys. + attachmentPackets = make(map[string]string) + for id, attkey := range attkeys { + var packets []byte + if packets, err = pubkey.EncryptSessionKey(attkey); err != nil { + return + } + attachmentPackets[id] = base64.StdEncoding.EncodeToString(packets) + } + return +} + +func encryptSymmetric( + kr *crypto.KeyRing, + textToEncrypt string, +) (decryptedKey *crypto.SessionKey, symEncryptedData []byte, err error) { + // We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones). + firstKey, err := kr.FirstKey() + if err != nil { + return + } + + pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr) + if err != nil { + return + } + + pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0) + if err != nil { + return + } + + decryptedKey, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket()) + if err != nil { + return + } + + symEncryptedData = pgpSplitMessage.GetBinaryDataPacket() + + return +} diff --git a/pkg/pmapi/message_send.go b/pkg/pmapi/message_send.go new file mode 100644 index 00000000..f3fa6413 --- /dev/null +++ b/pkg/pmapi/message_send.go @@ -0,0 +1,331 @@ +// 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 . + +package pmapi + +import ( + "encoding/base64" + "errors" + + "github.com/ProtonMail/gopenpgp/v2/crypto" +) + +const ( + DraftActionReply = 0 + DraftActionReplyAll = 1 + DraftActionForward = 2 +) + +// Message package types. +const ( + InternalPackage = 1 + EncryptedOutsidePackage = 2 + ClearPackage = 4 + PGPInlinePackage = 8 + PGPMIMEPackage = 16 + ClearMIMEPackage = 32 +) + +// Signature types. +const ( + SignatureNone = 0 + SignatureDetached = 1 + SignatureAttachedArmored = 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", "/mail/v4/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 AlgoKey struct { + Key string + Algorithm string +} + +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 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. +} + +func newMessagePackage( + send sendData, + attKeys map[string]AlgoKey, +) (pkg *MessagePackage) { + pkg = &MessagePackage{ + Body: base64.StdEncoding.EncodeToString(send.data), + Addresses: send.addressMap, + MIMEType: send.contentType, + Type: send.sharedScheme, + } + + if send.sharedScheme&ClearPackage == ClearPackage || + send.sharedScheme&ClearMIMEPackage == ClearMIMEPackage { + pkg.BodyKey.Key = send.key.GetBase64Key() + pkg.BodyKey.Algorithm = send.key.Algo + } + + if attKeys != nil && send.sharedScheme&ClearPackage == ClearPackage { + pkg.AttachmentKeys = attKeys + } + + return pkg +} + +type sendData struct { + key *crypto.SessionKey //body session key + addressMap map[string]*MessageAddress + sharedScheme int + data []byte // ciphertext + body string // cleartext + contentType string +} + +type SendMessageReq struct { + ExpirationTime int64 `json:",omitempty"` + // AutoSaveContacts int `json:",omitempty"` + + // Data for encrypted recipients. + Packages []*MessagePackage + + mime, plain, rich sendData + attKeys map[string]*crypto.SessionKey + kr *crypto.KeyRing +} + +func NewSendMessageReq( + kr *crypto.KeyRing, + mimeBody, plainBody, richBody string, + attKeys map[string]*crypto.SessionKey, +) *SendMessageReq { + req := &SendMessageReq{} + + req.mime.addressMap = make(map[string]*MessageAddress) + req.plain.addressMap = make(map[string]*MessageAddress) + req.rich.addressMap = make(map[string]*MessageAddress) + + req.mime.body = mimeBody + req.plain.body = plainBody + req.rich.body = richBody + + req.attKeys = attKeys + req.kr = kr + + return req +} + +var ( + errMultipartInNonMIME = errors.New("multipart mixed not allowed in this scheme") + errAttSignNotSupported = errors.New("attached signature not supported") + errEncryptMustSign = errors.New("encrypted package must be signed") + errEONotSupported = errors.New("encrypted outside is not supported") + errWrongSendScheme = errors.New("wrong send scheme") + errInternalMustEncrypt = errors.New("internal package must be encrypted") + errInlinelMustEncrypt = errors.New("PGP Inline package must be encrypted") + errMisingPubkey = errors.New("cannot encrypt body key packet: missing pubkey") + errSignMustBeMultipart = errors.New("clear singed packet must be multipart") + errMIMEMustBeMultipart = errors.New("MIME packet must be multipart") +) + +func (req *SendMessageReq) AddRecipient( + email string, sendScheme int, + pubkey *crypto.KeyRing, signature int, + contentType string, doEncrypt bool, +) (err error) { + if signature == SignatureAttachedArmored { + return errAttSignNotSupported + } + + if doEncrypt && signature != SignatureDetached { + return errEncryptMustSign + } + + switch sendScheme { + case PGPMIMEPackage, ClearMIMEPackage: + if contentType != ContentTypeMultipartMixed { + return errMIMEMustBeMultipart + } + return req.addMIMERecipient(email, sendScheme, pubkey, signature) + case InternalPackage, ClearPackage, PGPInlinePackage: + return req.addNonMIMERecipient(email, sendScheme, pubkey, signature, contentType, doEncrypt) + case EncryptedOutsidePackage: + return errEONotSupported + } + return errWrongSendScheme +} + +func (req *SendMessageReq) addNonMIMERecipient( + email string, sendScheme int, + pubkey *crypto.KeyRing, signature int, + contentType string, doEncrypt bool, +) (err error) { + if sendScheme == ClearPackage && signature == SignatureDetached { + return errSignMustBeMultipart + } + + var send *sendData + switch contentType { + case ContentTypePlainText: + send = &req.plain + send.contentType = contentType + case ContentTypeHTML: + send = &req.rich + send.contentType = contentType + case ContentTypeMultipartMixed: + return errMultipartInNonMIME + } + + if send.key == nil { + if send.key, send.data, err = encryptSymmetric(req.kr, send.body); err != nil { + return err + } + } + newAddress := &MessageAddress{Type: sendScheme, Signature: signature} + + if sendScheme == PGPInlinePackage && !doEncrypt { + return errInlinelMustEncrypt + } + if sendScheme == InternalPackage && !doEncrypt { + return errInternalMustEncrypt + } + if doEncrypt && pubkey == nil { + return errMisingPubkey + } + + if doEncrypt { + newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(pubkey, send.key, req.attKeys) + if err != nil { + return err + } + } + send.addressMap[email] = newAddress + send.sharedScheme |= sendScheme + + return nil +} + +func (req *SendMessageReq) addMIMERecipient( + email string, sendScheme int, + pubkey *crypto.KeyRing, signature int, +) (err error) { + + req.mime.contentType = ContentTypeMultipartMixed + if req.mime.key == nil { + if req.mime.key, req.mime.data, err = encryptSymmetric(req.kr, req.mime.body); err != nil { + return err + } + } + + if sendScheme == PGPMIMEPackage { + if pubkey == nil { + return errMisingPubkey + } + // Attachment keys are not needed because attachments are part + // of MIME body and therefore attachments are encrypted with + // body session key. + mimeBodyPacket, _, err := createPackets(pubkey, req.mime.key, map[string]*crypto.SessionKey{}) + if err != nil { + return err + } + req.mime.addressMap[email] = &MessageAddress{Type: sendScheme, BodyKeyPacket: mimeBodyPacket, Signature: signature} + } else { + req.mime.addressMap[email] = &MessageAddress{Type: sendScheme, Signature: signature} + } + req.mime.sharedScheme |= sendScheme + + return nil +} + +func (req *SendMessageReq) PreparePackages() { + attkeysEncoded := make(map[string]AlgoKey) + for attID, attkey := range req.attKeys { + attkeysEncoded[attID] = AlgoKey{ + Key: attkey.GetBase64Key(), + Algorithm: attkey.Algo, + } + } + + for _, send := range []sendData{req.mime, req.plain, req.rich} { + if len(send.addressMap) == 0 { + continue + } + req.Packages = append(req.Packages, newMessagePackage(send, attkeysEncoded)) + } +} + +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", "/mail/v4/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 +} diff --git a/pkg/pmapi/message_send_test.go b/pkg/pmapi/message_send_test.go new file mode 100644 index 00000000..feed8630 --- /dev/null +++ b/pkg/pmapi/message_send_test.go @@ -0,0 +1,807 @@ +// 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 . + +package pmapi + +import ( + "encoding/base64" + "testing" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/stretchr/testify/require" +) + +type recipient struct { + email string + sendScheme int + pubkey *crypto.KeyRing + signature int + contentType string + doEncrypt bool + wantError error +} + +type testData struct { + recipients []recipient + wantPackages []*MessagePackage + + attKeys map[string]*crypto.SessionKey + mimeBody, plainBody, richBody string +} + +func (td *testData) prepareAndCheck(t *testing.T) { + r := require.New(t) + + shouldBeEmpty := func(want string) require.ValueAssertionFunc { + if len(want) == 0 { + return require.Empty + } + return require.NotEmpty + } + + have := NewSendMessageReq(testPrivateKeyRing, td.mimeBody, td.plainBody, td.richBody, td.attKeys) + for _, rec := range td.recipients { + err := have.AddRecipient(rec.email, rec.sendScheme, rec.pubkey, rec.signature, rec.contentType, rec.doEncrypt) + + if rec.wantError == nil { + r.NoError(err, "email %s", rec.email) + } else { + r.EqualError(err, rec.wantError.Error(), "email %s", rec.email) + } + } + have.PreparePackages() + + r.Equal(len(td.wantPackages), len(have.Packages)) + + for i, wantPackage := range td.wantPackages { + havePackage := have.Packages[i] + + r.Equal(len(havePackage.Addresses), len(wantPackage.Addresses)) + for email, wantAddress := range wantPackage.Addresses { + haveAddress, ok := havePackage.Addresses[email] + r.True(ok, "pkg %d email %s", i, email) + + r.Equal(wantAddress.Type, haveAddress.Type, "pkg %d email %s", i, email) + shouldBeEmpty(wantAddress.BodyKeyPacket)(t, haveAddress.BodyKeyPacket, "pkg %d email %s", i, email) + r.Equal(wantAddress.Signature, haveAddress.Signature, "pkg %d email %s", i, email) + + if len(td.attKeys) == 0 { + r.Len(haveAddress.AttachmentKeyPackets, 0) + } else { + r.Equal( + len(wantAddress.AttachmentKeyPackets), + len(haveAddress.AttachmentKeyPackets), + "pkg %d email %s", i, email, + ) + for attID, wantAttKey := range wantAddress.AttachmentKeyPackets { + haveAttKey, ok := haveAddress.AttachmentKeyPackets[attID] + r.True(ok, "pkg %d email %s att %s", i, email, attID) + shouldBeEmpty(wantAttKey)(t, haveAttKey, "pkg %d email %s att %s", i, email, attID) + } + } + } + + r.Equal(wantPackage.Type, havePackage.Type, "pkg %d", i) + r.Equal(wantPackage.MIMEType, havePackage.MIMEType, "pkg %d", i) + + shouldBeEmpty(wantPackage.Body)(t, havePackage.Body, "pkg %d", i) + + wantBodyKey := wantPackage.BodyKey + haveBodyKey := havePackage.BodyKey + + shouldBeEmpty(wantBodyKey.Algorithm)(t, haveBodyKey.Algorithm, "pkg %d", i) + shouldBeEmpty(wantBodyKey.Key)(t, haveBodyKey.Key, "pkg %d", i) + + if len(td.attKeys) == 0 { + r.Len(havePackage.AttachmentKeys, 0) + } else { + r.Equal( + len(wantPackage.AttachmentKeys), + len(havePackage.AttachmentKeys), + "pkg %d", i, + ) + for attID, wantAttKey := range wantPackage.AttachmentKeys { + haveAttKey, ok := havePackage.AttachmentKeys[attID] + r.True(ok, "pkg %d att %s", i, attID) + shouldBeEmpty(wantAttKey.Key)(t, haveAttKey.Key, "pkg %d att %s", i, attID) + shouldBeEmpty(wantAttKey.Algorithm)(t, haveAttKey.Algorithm, "pkg %d att %s", i, attID) + } + } + } +} + +func TestSendReq(t *testing.T) { + attKeyB64 := "EvjO/2RIJNn6HdoU6ACqFdZglzJhpjQ/PpjsvL3mB5Q=" + token, err := base64.StdEncoding.DecodeString(attKeyB64) + require.NoError(t, err) + + attKey := crypto.NewSessionKeyFromToken(token, "aes256") + attKeyPackets := map[string]string{"attID": "not-empty"} + attAlgoKeys := map[string]AlgoKey{"attID": {"not-empty", "not-empty"}} + + // NOTE naming + // Single: there should be one packet + // Multiple: there should be more than one packet + // Internal: there should be internal package + // Clear: there should be non-encrypted package + // Encrypted: there should be encrypted package + // NotAllowed: combination of inputs which are not allowed + tests := map[string]testData{ + "SingleInternalHTML": { + recipients: []recipient{ + {"html@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "html@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: InternalPackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + }, + }, + }, + "SingleInternalPlain": { + recipients: []recipient{ + {"plain@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "plain@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: InternalPackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + }, + }, + }, + "InternalNotAllowed": { + recipients: []recipient{ + {"multipart@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, errMultipartInNonMIME}, + {"noencrypt@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, false, errInternalMustEncrypt}, + {"no-pubkey@pm.me", InternalPackage, nil, SignatureDetached, ContentTypeHTML, true, errMisingPubkey}, + {"nosigning@pm.me", InternalPackage, testPublicKeyRing, SignatureNone, ContentTypeHTML, true, errEncryptMustSign}, + }, + }, + "MultipleInternal": { + recipients: []recipient{ + {"internal1@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + {"internal2@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + {"internal3@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + {"internal4@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "internal1@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "internal3@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: InternalPackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + }, + { + Addresses: map[string]*MessageAddress{ + "internal2@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "internal4@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: InternalPackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + }, + }, + }, + + "SingleClearHTML": { + recipients: []recipient{ + {"html@email.com", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "html@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + }, + Type: ClearPackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + }, + }, + "SingleClearPlain": { + recipients: []recipient{ + {"plain@email.com", ClearPackage, nil, SignatureNone, ContentTypePlainText, false, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "plain@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + }, + Type: ClearPackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + }, + }, + "SingleClearMIME": { + recipients: []recipient{ + {"mime@email.com", ClearMIMEPackage, nil, SignatureNone, ContentTypeMultipartMixed, false, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "mime@email.com": { + Type: ClearMIMEPackage, + Signature: SignatureNone, + }, + }, + Type: ClearMIMEPackage, + MIMEType: ContentTypeMultipartMixed, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + }, + }, + }, + "SingleClearSign": { + recipients: []recipient{ + {"signed@email.com", ClearMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, false, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "signed@email.com": { + Type: ClearMIMEPackage, + Signature: SignatureDetached, + }, + }, + Type: ClearMIMEPackage, + MIMEType: ContentTypeMultipartMixed, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + }, + }, + }, + "ClearNotAllowed": { + recipients: []recipient{ + {"plain@email.com", ClearPackage, nil, SignatureDetached, ContentTypePlainText, false, errSignMustBeMultipart}, + {"html-1@email.com", ClearPackage, nil, SignatureDetached, ContentTypeHTML, false, errSignMustBeMultipart}, + {"plain@email.com", ClearMIMEPackage, nil, SignatureDetached, ContentTypePlainText, false, errMIMEMustBeMultipart}, + {"html-@email.com", ClearMIMEPackage, nil, SignatureDetached, ContentTypeHTML, false, errMIMEMustBeMultipart}, + }, + }, + "MultipleClear": { + recipients: []recipient{ + {"html@email.com", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil}, + {"sign@email.com", ClearMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, false, nil}, + {"mime@email.com", ClearMIMEPackage, nil, SignatureNone, ContentTypeMultipartMixed, false, nil}, + {"plain@email.com", ClearPackage, nil, SignatureNone, ContentTypePlainText, false, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ // TODO can this two be combined + "sign@email.com": { + Type: ClearMIMEPackage, + Signature: SignatureDetached, + }, + "mime@email.com": { + Type: ClearMIMEPackage, + Signature: SignatureNone, + }, + }, + Type: ClearMIMEPackage, + MIMEType: ContentTypeMultipartMixed, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + }, + { + Addresses: map[string]*MessageAddress{ + "plain@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + }, + Type: ClearPackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + { + Addresses: map[string]*MessageAddress{ + "html@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + }, + Type: ClearPackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + }, + }, + + "SingleEncryptedMIME": { + recipients: []recipient{ + {"mime@gpg.com", PGPMIMEPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "mime@gpg.com": { + Type: PGPMIMEPackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + }, + }, + Type: PGPMIMEPackage, + MIMEType: ContentTypeMultipartMixed, + Body: "non-empty", + }, + }, + }, + "SingleEncryptedInlinePlain": { + recipients: []recipient{ + {"inline-plain@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "inline-plain@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: PGPInlinePackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + }, + }, + }, + "SingleEncryptedInlineHTML": { + recipients: []recipient{ + {"inline-html@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "inline-html@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: PGPInlinePackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + }, + }, + }, + "EncryptedNotAllowed": { + recipients: []recipient{ + {"inline-mixed@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, errMultipartInNonMIME}, + {"inline-clear@gpg.com", PGPInlinePackage, nil, SignatureDetached, ContentTypePlainText, false, errInlinelMustEncrypt}, + {"mime-plain@gpg.com", PGPMIMEPackage, nil, SignatureDetached, ContentTypePlainText, true, errMIMEMustBeMultipart}, + {"mime-html@gpg.com", PGPMIMEPackage, nil, SignatureDetached, ContentTypeHTML, true, errMIMEMustBeMultipart}, + {"no-pubkey@gpg.com", PGPMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, true, errMisingPubkey}, + {"not-signed@gpg.com", PGPMIMEPackage, testPublicKeyRing, SignatureNone, ContentTypeMultipartMixed, true, errEncryptMustSign}, + }, + }, + "MultipleEncrypted": { + recipients: []recipient{ + {"mime@gpg.com", PGPMIMEPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, nil}, + {"inline-plain@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + {"inline-html@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "mime@gpg.com": { + Type: PGPMIMEPackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + }, + }, + Type: PGPMIMEPackage, + MIMEType: ContentTypeMultipartMixed, + Body: "non-empty", + }, + { + Addresses: map[string]*MessageAddress{ + "inline-plain@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: PGPInlinePackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + }, + { + Addresses: map[string]*MessageAddress{ + "inline-html@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: PGPInlinePackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + }, + }, + }, + + "SingleInternalEncryptedHTML": { + recipients: []recipient{ + {"inline-html@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + {"internal@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "inline-html@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "internal@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: PGPInlinePackage | InternalPackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + }, + }, + }, + "SingleInternalEncryptedPlain": { + recipients: []recipient{ + {"inline-plain@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + {"internal@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "inline-plain@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "internal@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: PGPInlinePackage | InternalPackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + }, + }, + }, + "SingleInternalClearHTML": { + recipients: []recipient{ + {"internal@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + {"html@email.com", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "internal@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "html@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + }, + Type: InternalPackage | ClearPackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + }, + }, + "SingleInternalClearPlain": { + recipients: []recipient{ + {"internal@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + {"html@email.com", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "internal@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "html@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + }, + Type: InternalPackage | ClearPackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + }, + }, + "SingleClearEncryptedHTML": { + recipients: []recipient{ + {"html@email.com", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil}, + {"inline-html@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "inline-html@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "html@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + }, + Type: PGPInlinePackage | ClearPackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + }, + }, + "SingleClearEncryptedPlain": { + recipients: []recipient{ + {"plain@email.com", ClearPackage, nil, SignatureNone, ContentTypePlainText, false, nil}, + {"inline-plain@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "plain@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + "inline-plain@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: PGPInlinePackage | ClearPackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + }, + }, + "SingleClearEncryptedMIME": { + recipients: []recipient{ + {"signed@email.com", ClearMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, false, nil}, + {"mime@gpg.com", PGPMIMEPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "mime@gpg.com": { + Type: PGPMIMEPackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + }, + "signed@email.com": { + Type: ClearMIMEPackage, + Signature: SignatureDetached, + }, + }, + Type: ClearMIMEPackage | PGPMIMEPackage, + MIMEType: ContentTypeMultipartMixed, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + }, + }, + }, + "SingleClearEncryptedMIMENoSign": { + recipients: []recipient{ + {"mime@email.com", ClearMIMEPackage, nil, SignatureNone, ContentTypeMultipartMixed, false, nil}, + {"mime@gpg.com", PGPMIMEPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ + "mime@gpg.com": { + Type: PGPMIMEPackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + }, + "mime@email.com": { // can this be combined ? + Type: ClearMIMEPackage, + Signature: SignatureNone, + }, + }, + Type: ClearMIMEPackage | PGPMIMEPackage, + MIMEType: ContentTypeMultipartMixed, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + }, + }, + }, + "MultipleCombo": { + recipients: []recipient{ + {"mime@email.com", ClearMIMEPackage, nil, SignatureNone, ContentTypeMultipartMixed, false, nil}, + {"signed@email.com", ClearMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, false, nil}, + {"mime@gpg.com", PGPMIMEPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, nil}, + + {"plain@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + {"plain@email.com", ClearPackage, nil, SignatureNone, ContentTypePlainText, false, nil}, + {"inline-plain@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil}, + + {"html@pm.me", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + {"html@email.com", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil}, + {"inline-html@gpg.com", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil}, + }, + wantPackages: []*MessagePackage{ + { + Addresses: map[string]*MessageAddress{ // TODO can this three be combined + "mime@gpg.com": { + Type: PGPMIMEPackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + }, + "mime@email.com": { + Type: ClearMIMEPackage, + Signature: SignatureNone, + }, + "signed@email.com": { + Type: ClearMIMEPackage, + Signature: SignatureDetached, + }, + }, + Type: ClearMIMEPackage | PGPMIMEPackage, + MIMEType: ContentTypeMultipartMixed, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + }, + { + Addresses: map[string]*MessageAddress{ + "plain@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "plain@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + "inline-plain@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: InternalPackage | ClearPackage | PGPInlinePackage, + MIMEType: ContentTypePlainText, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + { + Addresses: map[string]*MessageAddress{ + "html@pm.me": { + Type: InternalPackage, + Signature: SignatureDetached, + BodyKeyPacket: "not-empty", + AttachmentKeyPackets: attKeyPackets, + }, + "html@email.com": { + Type: ClearPackage, + Signature: SignatureNone, + }, + "inline-html@gpg.com": { + Type: PGPInlinePackage, + Signature: SignatureDetached, + BodyKeyPacket: "non-empty", + AttachmentKeyPackets: attKeyPackets, + }, + }, + Type: InternalPackage | ClearPackage | PGPInlinePackage, + MIMEType: ContentTypeHTML, + Body: "non-empty", + BodyKey: AlgoKey{"non-empty", "non-empty"}, + AttachmentKeys: attAlgoKeys, + }, + }, + }, + } + + for name, test := range tests { + test.mimeBody = "Mime body" + test.plainBody = "Plain body" + test.richBody = "HTML body" + t.Run("NoAtt"+name, test.prepareAndCheck) + test.attKeys = map[string]*crypto.SessionKey{"attID": attKey} + t.Run("Att"+name, test.prepareAndCheck) + } +} diff --git a/pkg/pmapi/messages.go b/pkg/pmapi/messages.go index 623bd07b..32ee60d2 100644 --- a/pkg/pmapi/messages.go +++ b/pkg/pmapi/messages.go @@ -569,114 +569,6 @@ func (c *client) GetMessage(id string) (msg *Message, err error) { 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", "/mail/v4/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", "/mail/v4/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 }