refactor smtp sending

* [x] move package creation logic to `pmapi.SendMessageReq`
* [ ] write test of package creation logic
    * [x] internal
    * [x] plain
    * [x] external encrypted
    * [ ] signature ???
    * [x] attachments
This commit is contained in:
Jakub
2020-11-16 15:14:47 +01:00
committed by Jakub Cuth
parent a0fbed5859
commit 152046bf97
8 changed files with 1245 additions and 288 deletions

View File

@ -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

43
pkg/pmapi/debug.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View File

@ -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
}

331
pkg/pmapi/message_send.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}
}

View File

@ -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
}