We build too many walls and not enough bridges

This commit is contained in:
Jakub
2020-04-08 12:59:16 +02:00
commit 17f4d6097a
494 changed files with 62753 additions and 0 deletions

109
internal/smtp/backend.go Normal file
View File

@ -0,0 +1,109 @@
// 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 smtp
import (
"strings"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
goSMTPBackend "github.com/emersion/go-smtp"
)
type panicHandler interface {
HandlePanic()
}
type smtpBackend struct {
panicHandler panicHandler
eventListener listener.Listener
preferences *config.Preferences
bridge bridger
shouldSendNoEncChannels map[string]chan bool
sendRecorder *sendRecorder
}
// NewSMTPBackend returns struct implementing go-smtp/backend interface.
func NewSMTPBackend(
panicHandler panicHandler,
eventListener listener.Listener,
preferences *config.Preferences,
bridge *bridge.Bridge,
) *smtpBackend { //nolint[golint]
return newSMTPBackend(panicHandler, eventListener, preferences, newBridgeWrap(bridge))
}
func newSMTPBackend(
panicHandler panicHandler,
eventListener listener.Listener,
preferences *config.Preferences,
bridge bridger,
) *smtpBackend {
return &smtpBackend{
panicHandler: panicHandler,
eventListener: eventListener,
preferences: preferences,
bridge: bridge,
shouldSendNoEncChannels: make(map[string]chan bool),
sendRecorder: newSendRecorder(),
}
}
// Login authenticates a user.
func (sb *smtpBackend) Login(username, password string) (goSMTPBackend.User, error) {
// Called from go-smtp in goroutines - we need to handle panics for each function.
defer sb.panicHandler.HandlePanic()
username = strings.ToLower(username)
user, err := sb.bridge.GetUser(username)
if err != nil {
log.Warn("Cannot get user: ", err)
return nil, err
}
if err := user.CheckBridgeLogin(password); err != nil {
log.WithError(err).Error("Could not check bridge password")
// Apple Mail sometimes generates a lot of requests very quickly. It's good practice
// to have a timeout after bad logins so that we can slow those requests down a little bit.
time.Sleep(10 * time.Second)
return nil, err
}
// Client can log in only using address so we can properly close all SMTP connections.
addressID, err := user.GetAddressID(username)
if err != nil {
log.Error("Cannot get addressID: ", err)
return nil, err
}
// AddressID is only for split mode--it has to be empty for combined mode.
if user.IsCombinedAddressMode() {
addressID = ""
}
return newSMTPUser(sb.panicHandler, sb.eventListener, sb, user, addressID)
}
func (sb *smtpBackend) shouldReportOutgoingNoEnc() bool {
return sb.preferences.GetBool(preferences.ReportOutgoingNoEncKey)
}
func (sb *smtpBackend) ConfirmNoEncryption(messageID string, shouldSend bool) {
if ch, ok := sb.shouldSendNoEncChannels[messageID]; ok {
ch <- shouldSend
}
}

65
internal/smtp/bridge.go Normal file
View File

@ -0,0 +1,65 @@
// 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 smtp
import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
)
type bridger interface {
GetUser(query string) (bridgeUser, error)
}
type bridgeUser interface {
CheckBridgeLogin(password string) error
IsCombinedAddressMode() bool
GetAddressID(address string) (string, error)
GetTemporaryPMAPIClient() bridge.PMAPIProvider
GetStore() storeUserProvider
}
type bridgeWrap struct {
*bridge.Bridge
}
// newBridgeWrap wraps bridge struct into local bridgeWrap to implement local
// interface. The problem is that bridge returns package bridge's User type, so
// every method that returns User has to be overridden to fulfill the interface.
func newBridgeWrap(bridge *bridge.Bridge) *bridgeWrap {
return &bridgeWrap{Bridge: bridge}
}
func (b *bridgeWrap) GetUser(query string) (bridgeUser, error) {
user, err := b.Bridge.GetUser(query)
if err != nil {
return nil, err
}
return newBridgeUserWrap(user), nil
}
type bridgeUserWrap struct {
*bridge.User
}
func newBridgeUserWrap(bridgeUser *bridge.User) *bridgeUserWrap {
return &bridgeUserWrap{User: bridgeUser}
}
func (u *bridgeUserWrap) GetStore() storeUserProvider {
return u.User.GetStore()
}

View File

@ -0,0 +1,124 @@
// 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 smtp
import (
"crypto/sha256"
"fmt"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type messageGetter interface {
GetMessage(string) (*pmapi.Message, error)
}
type sendRecorderValue struct {
messageID string
time time.Time
}
type sendRecorder struct {
lock *sync.RWMutex
hashes map[string]sendRecorderValue
}
func newSendRecorder() *sendRecorder {
return &sendRecorder{
lock: &sync.RWMutex{},
hashes: map[string]sendRecorderValue{},
}
}
func (q *sendRecorder) getMessageHash(message *pmapi.Message) string {
h := sha256.New()
_, _ = h.Write([]byte(message.AddressID + message.Subject))
if message.Sender != nil {
_, _ = h.Write([]byte(message.Sender.Address))
}
for _, to := range message.ToList {
_, _ = h.Write([]byte(to.Address))
}
for _, to := range message.CCList {
_, _ = h.Write([]byte(to.Address))
}
for _, to := range message.BCCList {
_, _ = h.Write([]byte(to.Address))
}
_, _ = h.Write([]byte(message.Body))
for _, att := range message.Attachments {
_, _ = h.Write([]byte(att.Name + att.MIMEType + fmt.Sprintf("%d", att.Size)))
}
return fmt.Sprintf("%x", h.Sum(nil))
}
func (q *sendRecorder) addMessage(hash, messageID string) {
q.lock.Lock()
defer q.lock.Unlock()
q.deleteExpiredKeys()
q.hashes[hash] = sendRecorderValue{
messageID: messageID,
time: time.Now(),
}
}
func (q *sendRecorder) isSendingOrSent(client messageGetter, hash string) (isSending bool, wasSent bool) {
q.lock.Lock()
defer q.lock.Unlock()
q.deleteExpiredKeys()
value, ok := q.hashes[hash]
if !ok {
return
}
message, err := client.GetMessage(value.messageID)
// Message could be deleted or there could be an internet issue or whatever,
// so let's assume the message was not sent.
if err != nil {
return
}
if message.Type == pmapi.MessageTypeDraft {
// If message is in draft for a long time, let's assume there is
// some problem and message will not be sent anymore.
if time.Since(time.Unix(message.Time, 0)).Minutes() > 10 {
return
}
isSending = true
}
// MessageTypeInboxAndSent can be when message was sent to myself.
if message.Type == pmapi.MessageTypeSent || message.Type == pmapi.MessageTypeInboxAndSent {
wasSent = true
}
return
}
func (q *sendRecorder) deleteExpiredKeys() {
for key, value := range q.hashes {
// It's hard to find a good expiration time.
// On the one hand, a user could set up some cron job sending the same message over and over again (heartbeat).
// On the the other, a user could put the device into sleep mode while sending.
// Changing the expiration time will always make one of the edge cases worse.
// But both edge cases are something we don't care much about. Important thing is we don't send the same message many times.
if time.Since(value.time) > 30*time.Minute {
delete(q.hashes, key)
}
}
}

View File

@ -0,0 +1,414 @@
// 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 smtp
import (
"errors"
"fmt"
"net/mail"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/stretchr/testify/assert"
)
type testSendRecorderGetMessageMock struct {
message *pmapi.Message
err error
}
func (m *testSendRecorderGetMessageMock) GetMessage(messageID string) (*pmapi.Message, error) {
return m.message, m.err
}
func TestSendRecorder_getMessageHash(t *testing.T) {
q := newSendRecorder()
message := &pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
}
hash := q.getMessageHash(message)
testCases := []struct {
message *pmapi.Message
expectEqual bool
}{
{
message,
true,
},
{
&pmapi.Message{},
false,
},
{ // Different AddressID
&pmapi.Message{
AddressID: "...",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different subject
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1.",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different sender
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "sender@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different ToList - changed address
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "other@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different ToList - more addresses
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
{Address: "another@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different CCList
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{
{Address: "to@pm.me"},
},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different BCCList
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{
{Address: "to@pm.me"},
},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different body
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body.",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different attachment - no attachment
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{},
},
false,
},
{ // Different attachment - name
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "...",
MIMEType: "image/png",
Size: 12345,
},
},
},
false,
},
{ // Different attachment - MIMEType
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/jpeg",
Size: 12345,
},
},
},
false,
},
{ // Different attachment - Size
&pmapi.Message{
AddressID: "address123",
Subject: "Subject #1",
Sender: &mail.Address{
Address: "from@pm.me",
},
ToList: []*mail.Address{
{Address: "to@pm.me"},
},
CCList: []*mail.Address{},
BCCList: []*mail.Address{},
Body: "body",
Attachments: []*pmapi.Attachment{
{
Name: "att1",
MIMEType: "image/png",
Size: 42,
},
},
},
false,
},
}
for i, tc := range testCases {
tc := tc // bind
t.Run(fmt.Sprintf("%d / %v", i, tc.message), func(t *testing.T) {
newHash := q.getMessageHash(tc.message)
if tc.expectEqual {
assert.Equal(t, hash, newHash)
} else {
assert.NotEqual(t, hash, newHash)
}
})
}
}
func TestSendRecorder_isSendingOrSent(t *testing.T) {
q := newSendRecorder()
q.addMessage("hash", "messageID")
testCases := []struct {
hash string
message *pmapi.Message
err error
wantIsSending bool
wantWasSent bool
}{
{"badhash", &pmapi.Message{Type: pmapi.MessageTypeDraft}, nil, false, false},
{"hash", nil, errors.New("message not found"), false, false},
{"hash", &pmapi.Message{Type: pmapi.MessageTypeInbox}, nil, false, false},
{"hash", &pmapi.Message{Type: pmapi.MessageTypeDraft, Time: time.Now().Add(-20 * time.Minute).Unix()}, nil, false, false},
{"hash", &pmapi.Message{Type: pmapi.MessageTypeDraft, Time: time.Now().Unix()}, nil, true, false},
{"hash", &pmapi.Message{Type: pmapi.MessageTypeSent}, nil, false, true},
{"hash", &pmapi.Message{Type: pmapi.MessageTypeInboxAndSent}, nil, false, true},
}
for i, tc := range testCases {
tc := tc // bind
t.Run(fmt.Sprintf("%d / %v / %v / %v", i, tc.hash, tc.message, tc.err), func(t *testing.T) {
messageGetter := &testSendRecorderGetMessageMock{message: tc.message, err: tc.err}
isSending, wasSent := q.isSendingOrSent(messageGetter, "hash")
assert.Equal(t, tc.wantIsSending, isSending, "isSending does not match")
assert.Equal(t, tc.wantWasSent, wasSent, "wasSent does not match")
})
}
}
func TestSendRecorder_deleteExpiredKeys(t *testing.T) {
q := newSendRecorder()
q.hashes["hash1"] = sendRecorderValue{
messageID: "msg1",
time: time.Now(),
}
q.hashes["hash2"] = sendRecorderValue{
messageID: "msg2",
time: time.Now().Add(-31 * time.Minute),
}
q.deleteExpiredKeys()
_, ok := q.hashes["hash1"]
assert.True(t, ok)
_, ok = q.hashes["hash2"]
assert.False(t, ok)
}

View File

@ -0,0 +1,244 @@
// 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 smtp
import (
"errors"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/algo"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
const (
pgpInline = "pgp-inline"
pgpMime = "pgp-mime"
)
type SendingInfo struct {
Encrypt bool
Sign bool
Scheme int
MIMEType string
PublicKey *pmcrypto.KeyRing
}
func generateSendingInfo(
eventListener listener.Listener,
contactMeta *ContactMetadata,
isInternal bool,
composeMode string,
apiKeys,
contactKeys []*pmcrypto.KeyRing,
settingsSign bool,
settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
contactKeys, err = pmcrypto.FilterExpiredKeys(contactKeys)
if err != nil {
return
}
if isInternal {
sendingInfo, err = generateInternalSendingInfo(eventListener, contactMeta, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme)
} else {
sendingInfo, err = generateExternalSendingInfo(contactMeta, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme)
}
if (sendingInfo.Scheme == pmapi.PGPInlinePackage || sendingInfo.Scheme == pmapi.PGPMIMEPackage) && sendingInfo.PublicKey == nil {
return sendingInfo, errors.New("public key nil during attempt to encrypt")
}
return
}
func generateInternalSendingInfo(
eventListener listener.Listener,
contactMeta *ContactMetadata,
composeMode string,
apiKeys,
contactKeys []*pmcrypto.KeyRing,
settingsSign bool, //nolint[unparam]
settingsPgpScheme int) (sendingInfo SendingInfo, err error) { //nolint[unparam]
// If sending internally, there should always be a public key; if not, there's an error.
if len(apiKeys) == 0 {
err = errors.New("no valid public keys found for contact")
return
}
// The default settings, unless overridden by presence of a saved contact.
sendingInfo = SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.InternalPackage,
MIMEType: composeMode,
PublicKey: apiKeys[0],
}
// If there is no saved contact, our work here is done.
if contactMeta == nil {
return
}
// If contact has a pinned key, prefer that over the api key (if it's not expired).
checkedContactKeys, err := checkContactKeysAgainstAPI(contactKeys, apiKeys)
if err != nil {
return
}
// If we find no matching keys with the api but the contact still has pinned keys
// it means the pinned keys are out of date (e.g. the contact has since changed their protonmail
// keys and so the keys returned via the api don't match the keys pinned in the contact).
if len(checkedContactKeys) == 0 && len(contactKeys) != 0 {
eventListener.Emit(events.NoActiveKeyForRecipientEvent, contactMeta.Email)
return sendingInfo, errors.New("found no active key for recipient " + contactMeta.Email + ", please check contact settings")
}
if len(checkedContactKeys) > 0 {
sendingInfo.PublicKey = checkedContactKeys[0]
}
// If contact has a saved mime type preference, prefer that over the default.
if len(contactMeta.MIMEType) > 0 {
sendingInfo.MIMEType = contactMeta.MIMEType
}
return sendingInfo, nil
}
func generateExternalSendingInfo(
contactMeta *ContactMetadata,
composeMode string,
apiKeys,
contactKeys []*pmcrypto.KeyRing,
settingsSign bool,
settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
// The default settings, unless overridden by presence of a saved contact.
sendingInfo = SendingInfo{
Encrypt: false,
Sign: settingsSign,
PublicKey: nil,
}
if contactMeta != nil && len(contactKeys) > 0 {
// If the contact has a key, use it. And if the contact metadata says to encryt, do so.
sendingInfo.PublicKey = contactKeys[0]
sendingInfo.Encrypt = contactMeta.Encrypt
} else if len(apiKeys) > 0 {
// If the api returned a key (via WKD), use it. In this case we always encrypt.
sendingInfo.PublicKey = apiKeys[0]
sendingInfo.Encrypt = true
}
// - If we are encrypting, we always sign
// - else if the contact has a preference, we follow that
// - otherwise, we fall back to the mailbox default signing settings
if sendingInfo.Encrypt { //nolint[gocritic]
sendingInfo.Sign = true
} else if contactMeta != nil && !contactMeta.SignMissing {
sendingInfo.Sign = contactMeta.Sign
} else {
sendingInfo.Sign = settingsSign
}
sendingInfo.Scheme, sendingInfo.MIMEType, err = schemeAndMIME(contactMeta,
settingsPgpScheme,
composeMode,
sendingInfo.Encrypt,
sendingInfo.Sign)
return sendingInfo, err
}
func schemeAndMIME(contact *ContactMetadata, settingsScheme int, settingsMIMEType string, encrypted, signed bool) (scheme int, mime string, err error) {
if encrypted && signed {
// Prefer contact settings.
if contact != nil && contact.Scheme == pgpInline {
return pmapi.PGPInlinePackage, pmapi.ContentTypePlainText, nil
} else if contact != nil && contact.Scheme == pgpMime {
return pmapi.PGPMIMEPackage, pmapi.ContentTypeMultipartMixed, nil
}
// If no contact settings, follow mailbox defaults.
scheme = settingsScheme
if scheme == pmapi.PGPMIMEPackage {
return scheme, pmapi.ContentTypeMultipartMixed, nil
} else if scheme == pmapi.PGPInlinePackage {
return scheme, pmapi.ContentTypePlainText, nil
}
}
if !encrypted && signed {
// Prefer contact settings but send unencrypted (PGP-->Clear).
if contact != nil && contact.Scheme == pgpMime {
return pmapi.ClearMIMEPackage, pmapi.ContentTypeMultipartMixed, nil
} else if contact != nil && contact.Scheme == pgpInline {
return pmapi.ClearPackage, pmapi.ContentTypePlainText, nil
}
// If no contact settings, follow mailbox defaults but send unencrypted (PGP-->Clear).
if settingsScheme == pmapi.PGPMIMEPackage {
return pmapi.ClearMIMEPackage, pmapi.ContentTypeMultipartMixed, nil
} else if settingsScheme == pmapi.PGPInlinePackage {
return pmapi.ClearPackage, pmapi.ContentTypePlainText, nil
}
}
if !encrypted && !signed {
// Always send as clear package if we are neither encrypting nor signing.
scheme = pmapi.ClearPackage
// If the contact is nil, no further modifications can be made.
if contact == nil {
return scheme, settingsMIMEType, nil
}
// Prefer contact mime settings.
if contact.Scheme == pgpMime {
return scheme, pmapi.ContentTypeMultipartMixed, nil
} else if contact.Scheme == pgpInline {
return scheme, pmapi.ContentTypePlainText, nil
}
// If contact has a preferred mime type, use that, otherwise follow mailbox default.
if len(contact.MIMEType) > 0 {
return scheme, contact.MIMEType, nil
}
return scheme, settingsMIMEType, nil
}
// If we end up here, something went wrong.
err = errors.New("could not determine correct PGP Scheme and MIME Type to use to send mail")
return scheme, mime, err
}
// checkContactKeysAgainstAPI keeps only those contact keys which are up to date and have
// an ID that matches an API key's ID.
func checkContactKeysAgainstAPI(contactKeys, apiKeys []*pmcrypto.KeyRing) (filteredKeys []*pmcrypto.KeyRing, err error) { //nolint[unparam]
keyIDsAreEqual := func(a, b interface{}) bool {
aKey, bKey := a.(*pmcrypto.KeyRing), b.(*pmcrypto.KeyRing)
return aKey.GetEntities()[0].PrimaryKey.KeyId == bKey.GetEntities()[0].PrimaryKey.KeyId
}
for _, v := range algo.SetIntersection(contactKeys, apiKeys, keyIDsAreEqual) {
filteredKeys = append(filteredKeys, v.(*pmcrypto.KeyRing))
}
return
}

View File

@ -0,0 +1,604 @@
// 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 smtp
import (
"strings"
"testing"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
type mocks struct {
t *testing.T
eventListener *bridge.MockListener
}
func initMocks(t *testing.T) mocks {
mockCtrl := gomock.NewController(t)
return mocks{
t: t,
eventListener: bridge.NewMockListener(mockCtrl),
}
}
type args struct {
eventListener listener.Listener
contactMeta *ContactMetadata
apiKeys []*pmcrypto.KeyRing
contactKeys []*pmcrypto.KeyRing
composeMode string
settingsPgpScheme int
settingsSign bool
isInternal bool
}
type testData struct {
name string
args args
wantSendingInfo SendingInfo
wantErr bool
}
func (tt *testData) runTest(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
gotSendingInfo, err := generateSendingInfo(tt.args.eventListener, tt.args.contactMeta, tt.args.isInternal, tt.args.composeMode, tt.args.apiKeys, tt.args.contactKeys, tt.args.settingsSign, tt.args.settingsPgpScheme)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, gotSendingInfo, tt.wantSendingInfo)
}
})
}
func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
m := initMocks(t)
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey))
if err != nil {
panic(err)
}
tests := []testData{
{
name: "internal, PGP_MIME",
args: args{
contactMeta: nil,
isInternal: true,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.InternalPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: pubKey,
},
},
{
name: "internal, PGP_INLINE",
args: args{
contactMeta: nil,
isInternal: true,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.InternalPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: pubKey,
},
},
{
name: "external, PGP_MIME",
args: args{
contactMeta: nil,
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: false,
Sign: true,
Scheme: pmapi.ClearMIMEPackage,
MIMEType: pmapi.ContentTypeMultipartMixed,
PublicKey: nil,
},
},
{
name: "external, PGP_INLINE",
args: args{
contactMeta: nil,
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage,
},
wantSendingInfo: SendingInfo{
Encrypt: false,
Sign: true,
Scheme: pmapi.ClearPackage,
MIMEType: pmapi.ContentTypePlainText,
PublicKey: nil,
},
},
{
name: "external, PGP_MIME, Unsigned",
args: args{
contactMeta: nil,
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: false,
settingsPgpScheme: pmapi.PGPInlinePackage,
},
wantSendingInfo: SendingInfo{
Encrypt: false,
Sign: false,
Scheme: pmapi.ClearPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: nil,
},
},
{
name: "internal, error no valid public key",
args: args{
eventListener: m.eventListener,
contactMeta: nil,
isInternal: true,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey},
},
wantSendingInfo: SendingInfo{},
wantErr: true,
},
{
name: "external, no pinned key but receive one via WKD",
args: args{
contactMeta: nil,
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.PGPMIMEPackage,
MIMEType: pmapi.ContentTypeMultipartMixed,
PublicKey: pubKey,
},
},
}
for _, tt := range tests {
tt.runTest(t)
}
}
func TestGenerateSendingInfo_Contact_Internal(t *testing.T) {
m := initMocks(t)
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey))
if err != nil {
panic(err)
}
preferredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey))
if err != nil {
panic(err)
}
differentPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testDifferentPublicKey))
if err != nil {
panic(err)
}
m.eventListener.EXPECT().Emit(events.NoActiveKeyForRecipientEvent, "badkey@email.com")
tests := []testData{
{
name: "PGP_MIME, contact wants pgp-mime, no pinned key",
args: args{
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.InternalPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: pubKey,
},
},
{
name: "PGP_MIME, contact wants pgp-mime, pinned key",
args: args{
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{pubKey},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.InternalPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: pubKey,
},
},
{
name: "PGP_MIME, contact wants pgp-mime, pinned key but prefer api key",
args: args{
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: true,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{preferredPubKey},
contactKeys: []*pmcrypto.KeyRing{pubKey},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.InternalPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: preferredPubKey,
},
},
{
name: "internal, found no active key for recipient",
args: args{
eventListener: m.eventListener,
contactMeta: &ContactMetadata{Email: "badkey@email.com", Encrypt: true, Scheme: "pgp-mime"},
isInternal: true,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{differentPubKey},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{},
wantErr: true,
},
{
name: "external, contact saved, no pinned key but receive one via WKD",
args: args{
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.PGPMIMEPackage,
MIMEType: pmapi.ContentTypeMultipartMixed,
PublicKey: pubKey,
},
},
{
name: "external, contact saved, pinned key but receive different one via WKD",
args: args{
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{pubKey},
contactKeys: []*pmcrypto.KeyRing{differentPubKey},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.PGPMIMEPackage,
MIMEType: pmapi.ContentTypeMultipartMixed,
PublicKey: differentPubKey,
},
},
}
for _, tt := range tests {
tt.runTest(t)
}
}
func TestGenerateSendingInfo_Contact_External(t *testing.T) {
pubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testPublicKey))
if err != nil {
panic(err)
}
expiredPubKey, err := pmcrypto.ReadArmoredKeyRing(strings.NewReader(testExpiredPublicKey))
if err != nil {
panic(err)
}
tests := []testData{
{
name: "PGP_MIME, no pinned key",
args: args{
contactMeta: &ContactMetadata{},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: false,
Sign: false,
Scheme: pmapi.ClearPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: nil,
},
},
{
name: "PGP_MIME, pinned key but it's expired",
args: args{
contactMeta: &ContactMetadata{},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{expiredPubKey},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: false,
Sign: false,
Scheme: pmapi.ClearPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: nil,
},
},
{
name: "PGP_MIME, contact wants pgp-mime, pinned key",
args: args{
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-mime"},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.PGPMIMEPackage,
MIMEType: pmapi.ContentTypeMultipartMixed,
PublicKey: pubKey,
},
},
{
name: "PGP_MIME, contact wants pgp-inline, pinned key",
args: args{
contactMeta: &ContactMetadata{Encrypt: true, Scheme: "pgp-inline"},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.PGPInlinePackage,
MIMEType: pmapi.ContentTypePlainText,
PublicKey: pubKey,
},
},
{
name: "PGP_MIME, contact wants default scheme, pinned key",
args: args{
contactMeta: &ContactMetadata{Encrypt: true},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{pubKey},
settingsSign: true,
settingsPgpScheme: pmapi.PGPMIMEPackage,
},
wantSendingInfo: SendingInfo{
Encrypt: true,
Sign: true,
Scheme: pmapi.PGPMIMEPackage,
MIMEType: pmapi.ContentTypeMultipartMixed,
PublicKey: pubKey,
},
},
{
name: "PGP_INLINE, contact wants default scheme, no pinned key",
args: args{
contactMeta: &ContactMetadata{},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage,
},
wantSendingInfo: SendingInfo{
Encrypt: false,
Sign: false,
Scheme: pmapi.ClearPackage,
MIMEType: pmapi.ContentTypeHTML,
PublicKey: nil,
},
},
{
name: "PGP_INLINE, contact wants plain text, no pinned key",
args: args{
contactMeta: &ContactMetadata{MIMEType: pmapi.ContentTypePlainText},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage,
},
wantSendingInfo: SendingInfo{
Encrypt: false,
Sign: false,
Scheme: pmapi.ClearPackage,
MIMEType: pmapi.ContentTypePlainText,
PublicKey: nil,
},
},
{
name: "PGP_INLINE, contact sign missing, no pinned key",
args: args{
contactMeta: &ContactMetadata{SignMissing: true},
isInternal: false,
composeMode: pmapi.ContentTypeHTML,
apiKeys: []*pmcrypto.KeyRing{},
contactKeys: []*pmcrypto.KeyRing{},
settingsSign: true,
settingsPgpScheme: pmapi.PGPInlinePackage,
},
wantSendingInfo: SendingInfo{
Encrypt: false,
Sign: true,
Scheme: pmapi.ClearPackage,
MIMEType: pmapi.ContentTypePlainText,
PublicKey: nil,
},
},
}
for _, tt := range tests {
tt.runTest(t)
}
}
const testPublicKey = `
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: OpenPGP.js v0.7.1
Comment: http://openpgpjs.org
xsBNBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE
WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39
vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi
MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5
c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb
DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB
AAHNBlVzZXJJRMLAcgQQAQgAJgUCVEltzwYLCQgHAwIJED62JZ7fId8kBBUI
AgoDFgIBAhsDAh4BAAD0nQf9EtH9TC0JqSs8q194Zo244jjlJFM3EzxOSULq
0zbywlLORfyoo/O8jU/HIuGz+LT98JDtnltTqfjWgu6pS3ZL2/L4AGUKEoB7
OI6oIdRwzMc61sqI+Qpbzxo7rzufH4CiXZc6cxORUgL550xSCcqnq0q1mds7
h5roKDzxMW6WLiEsc1dN8IQKzC7Ec5wA7U4oNGsJ3TyI8jkIs0IhXrRCd26K
0TW8Xp6GCsfblWXosR13y89WVNgC+xrrJKTZEisc0tRlneIgjcwEUvwfIg2n
9cDUFA/5BsfzTW5IurxqDEziIVP0L44PXjtJrBQaGMPlEbtP5i2oi3OADVX2
XbvsRc7ATQRUSW3PAQgAkPnu5fps5zhOB/e618v/iF3KiogxUeRhA68TbvA+
xnFfTxCx2Vo14aOL0CnaJ8gO5yRSqfomL2O1kMq07N1MGbqucbmc+aSfoElc
+Gd5xBE/w3RcEhKcAaYTi35vG22zlZup4x3ElioyIarOssFEkQgNNyDf5AXZ
jdHLA6qVxeqAb/Ff74+y9HUmLPSsRU9NwFzvK3Jv8C/ubHVLzTYdFgYkc4W1
Uug9Ou08K+/4NEMrwnPFBbZdJAuUjQz2zW2ZiEKiBggiorH2o5N3mYUnWEmU
vqL3EOS8TbWo8UBIW3DDm2JiZR8VrEgvBtc9mVDUj/x+5pR07Fy1D6DjRmAc
9wARAQABwsBfBBgBCAATBQJUSW3SCRA+tiWe3yHfJAIbDAAA/iwH/ik9RKZM
B9Ir0x5mGpKPuqhugwrc3d04m1sOdXJm2NtD4ddzSEvzHwaPNvEvUl5v7FVM
zf6+6mYGWHyNP4+e7RtwYLlRpud6smuGyDSsotUYyumiqP6680ZIeWVQ+a1T
ThNs878mAJy1FhvQFdTmA8XIC616hDFpamQKPlpoO1a0wZnQhrPwT77HDYEE
a+hqY4Jr/a7ui40S+7xYRHKL/7ZAS4/grWllhU3dbNrwSzrOKwrA/U0/9t73
8Ap6JL71YymDeaL4sutcoaahda1pTrMWePtrCltz6uySwbZs7GXoEzjX3EAH
+6qhkUJtzMaE3YEFEoQMGzcDTUEfXCJ3zJw=
=yT9U
-----END PGP PUBLIC KEY BLOCK-----
`
const testDifferentPublicKey = `
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBF2Ix1EBCACwkUAn/5+sO1feDcS+aQ9BskESOyf1tBS8EDyz4deHFqnzoVCx
pJNvF7jb5J0AFCO/I5Mg7ddJb1udd/Eq+aZKfNYgjlvpdnW2Lo6Y0a5I5sm8R+vW
6EPQGxdgT7QG0VbeekGuy+F4o0KrgvJ4Sl3020q/Vix5B8ovtS6LGB22NWn5FGbL
+ssmq3tr3o2Q2jmHEIMTN4LOk1C4oHCljwrl7UP2MrER/if+czva3dB2jQgto6ia
o0+myIHkIjEKz5q7EGaGn9b7TEWk6+qNFRlKSa3GEFy4DXuQuysb+imjuP8uFxwb
/ib4QoOd/lAkrAVrcUHoWWhtBinsGEBXlG0LABEBAAG0GmphbWVzLXRlc3RAcHJv
dG9ubWFpbC5ibHVliQFUBBMBCAA+FiEEIwbxzW52iRgG0YMKojP3Zu/mCXIFAl2I
x1ECGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQojP3Zu/mCXJu
iQf+PiGA0sLEHx0gn2TRoYe7NOn9cnbi+KMLPIFJLGG4mAdVnEVNgaEvMGsNnC14
3FNIVaSdIR5/4ebtplZIlJWb8zxyaNTFkOJexnzwLw2p2cMF78Vsc4sAVLL5Y068
0v6KUzSK2cI1D4kvCyVK57jZL5dURCyISQrekYN/qhQb/TXbbUuznIJURTnLIq6k
v3E6SPB0hKksPgYlQaRocICw7ybbFur7gavyYlyZwD22JSGjwkJBSBi9dj14OD5Q
Egrd7E0qMd6BPzdlV9bctRabyUQLVjWFq8Nw4cC8AW7j7ENq6QIsuM2iKPf9M/HR
5U+Q9hUxcaG/Sv72QI7M4Qc4DrkBDQRdiMdRAQgA7Qufpv+RrZzcxYyfRf4SWZu5
Geo4Zke/AzlkTsw3MgMJHxiSXxEZdU4u/NRQeK53sEQ9J5iIuuzdjLbs5ECT4PjI
G8Lw6LtsCQ6WW9Gc7RUQNsXErIYidfk+v2zsJTHkP9aGkAgEe92bu87SSGXKO1In
w3e04wPjXeZ3ZYw2NovtPFNKVqBrglmN2WMTUXqOXNtcHCn/x5hQfuyo41wTol1m
YrZCiWu+Nxt6nEWQHA3hw0Dp8byCd/9yhIbn21cCZbX2aITYZL4pFbemMGfeteZF
eDVDxAXPFtat9pzgFe8wmF1kDrvnEsjvbb5UjmtlWZr0EWGoBkiioVh4/pyVMwAR
AQABiQE2BBgBCAAgFiEEIwbxzW52iRgG0YMKojP3Zu/mCXIFAl2Ix1ECGwwACgkQ
ojP3Zu/mCXLJZAf9Hbfu7FraFdl2DwYO815XFukMCAIUzhIMrLhUFO1WWg/m44bm
6OZ8NockPl8Mx3CjSG5Kjuk9h5AOG/doOVQL+i8ktQ7VsF4G9tBEgcxjacoGvNZH
VP1gFScmnI4rSfduhHf8JKToTJvK/KOFnko4/2fzM2WH3VLu7qZgT3RufuUn5LLn
C7eju/gf4WQZUtMTJODzs/EaHOkFevrJ7c6IIAUWD12sA6WHEC3l/mQuc9iXlyJw
HyMl6JQldr4XCcdTu73uSvVJ/1IkvLiHPuPP9ma9+FClaUGOmUws7rNQ3ODX52tx
bIYA5I4XbBMze46izlbEAKt6wHhQWTGlSpts0A==
=cOfs
-----END PGP PUBLIC KEY BLOCK-----`
const testExpiredPublicKey = `
-----BEGIN PGP PRIVATE KEY BLOCK-----
xcA4BAAAAAEBAgCgONc0J8rfO6cJw5YTP38x1ze2tAYIO7EcmRCNYwMkXngb
0Qdzg34Q5RW0rNiR56VB6KElPUhePRPVklLFiIvHABEBAAEAAf9qabYMzsz/
/LeRVZSsTgTljmJTdzd2ambUbpi+vt8MXJsbaWh71vjoLMWSXajaKSPDjVU5
waFNt9kLqwGGGLqpAQD5ZdMH2XzTq6GU9Ka69iZs6Pbnzwdz59Vc3i8hXlUj
zQEApHargCTsrtvSrm+hK/pN51/BHAy9lxCAw9f2etx+AeMA/RGrijkFZtYt
jeWdv/usXL3mgHvEcJv63N5zcEvDX5X4W1bND3Rlc3QxIDxhQGIuY29tPsJ7
BBABCAAvBQIAAAABBQMAAAU5BgsJBwgDAgkQzcF99nGrkAkEFQgKAgMWAgEC
GQECGwMCHgEAABAlAfwPehmLZs+gOhOTTaSslqQ50bl/REjmv42Nyr1ZBlQS
DECl1Qu4QyeXin29uEXWiekMpNlZVsEuc8icCw6ABhIZ
=/7PI
-----END PGP PRIVATE KEY BLOCK-----`

112
internal/smtp/server.go Normal file
View File

@ -0,0 +1,112 @@
// 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 smtp
import (
"crypto/tls"
"fmt"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/emersion/go-sasl"
goSMTP "github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
type smtpServer struct {
server *goSMTP.Server
eventListener listener.Listener
useSSL bool
}
// NewSMTPServer returns an SMTP server configured with the given options.
func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBackend goSMTP.Backend, eventListener listener.Listener) *smtpServer { //nolint[golint]
s := goSMTP.NewServer(smtpBackend)
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
s.TLSConfig = tls
s.Domain = bridge.Host
s.AllowInsecureAuth = true
if debug {
s.Debug = logrus.
WithField("pkg", "smtp/server").
WriterLevel(logrus.DebugLevel)
}
s.EnableAuth(sasl.Login, func(conn *goSMTP.Conn) sasl.Server {
return sasl.NewLoginServer(func(address, password string) error {
user, err := conn.Server().Backend.Login(address, password)
if err != nil {
return err
}
conn.SetUser(user)
return nil
})
})
return &smtpServer{
server: s,
eventListener: eventListener,
useSSL: useSSL,
}
}
// Starts the server.
func (s *smtpServer) ListenAndServe() {
go s.monitorDisconnectedUsers()
l := log.WithField("useSSL", s.useSSL).WithField("address", s.server.Addr)
l.Info("SMTP server is starting")
var err error
if s.useSSL {
err = s.server.ListenAndServeTLS()
} else {
err = s.server.ListenAndServe()
}
if err != nil {
s.eventListener.Emit(events.ErrorEvent, "SMTP failed: "+err.Error())
l.Error("SMTP failed: ", err)
return
}
defer s.server.Close()
l.Info("SMTP server stopped")
}
// Stops the server.
func (s *smtpServer) Close() {
s.server.Close()
}
func (s *smtpServer) monitorDisconnectedUsers() {
ch := make(chan string)
s.eventListener.Add(events.CloseConnectionEvent, ch)
for address := range ch {
log.Info("Disconnecting all open SMTP connections for ", address)
disconnectUser := func(conn *goSMTP.Conn) {
connUser := conn.User()
if connUser != nil {
_ = conn.Close()
}
}
s.server.ForEachConn(disconnectUser)
}
}

25
internal/smtp/smtp.go Normal file
View File

@ -0,0 +1,25 @@
// 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 smtp provides SMTP server of the Bridge.
package smtp
import "github.com/ProtonMail/proton-bridge/pkg/config"
var (
log = config.GetLogEntry("smtp") //nolint[gochecknoglobals]
)

36
internal/smtp/store.go Normal file
View File

@ -0,0 +1,36 @@
// 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 smtp
import (
"io"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type storeUserProvider interface {
CreateDraft(
kr *pmcrypto.KeyRing,
message *pmapi.Message,
attachmentReaders []io.Reader,
attachedPublicKey,
attachedPublicKeyName string,
parentID string) (*pmapi.Message, []*pmapi.Attachment, error)
SendMessage(messageID string, req *pmapi.SendMessageReq) error
}

513
internal/smtp/user.go Normal file
View File

@ -0,0 +1,513 @@
// 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/>.
// NOTE: Comments in this file refer to a specification in a document called "ProtonMail Encryption logic". It will be referred to via abbreviation PMEL.
package smtp
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
"mime"
"net/mail"
"regexp"
"strconv"
"strings"
"time"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
goSMTPBackend "github.com/emersion/go-smtp"
"github.com/pkg/errors"
)
type smtpUser struct {
panicHandler panicHandler
eventListener listener.Listener
backend *smtpBackend
user bridgeUser
client bridge.PMAPIProvider
storeUser storeUserProvider
addressID string
}
// newSMTPUser returns struct implementing go-smtp/session interface.
func newSMTPUser(
panicHandler panicHandler,
eventListener listener.Listener,
smtpBackend *smtpBackend,
user bridgeUser,
addressID string,
) (goSMTPBackend.User, error) {
// Using client directly is deprecated. Code should be moved to store.
client := user.GetTemporaryPMAPIClient()
storeUser := user.GetStore()
if storeUser == nil {
return nil, errors.New("user database is not initialized")
}
return &smtpUser{
panicHandler: panicHandler,
eventListener: eventListener,
backend: smtpBackend,
user: user,
client: client,
storeUser: storeUser,
addressID: addressID,
}, nil
}
// Send sends an email from the given address to the given addresses with the given body.
func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err error) { //nolint[funlen]
// Called from go-smtp in goroutines - we need to handle panics for each function.
defer su.panicHandler.HandlePanic()
mailSettings, err := su.client.GetMailSettings()
if err != nil {
return err
}
var addr *pmapi.Address = su.client.Addresses().ByEmail(from)
if addr == nil {
err = errors.New("backend: invalid email address: not owned by user")
return
}
kr := addr.KeyRing()
var attachedPublicKey string
var attachedPublicKeyName string
if mailSettings.AttachPublicKey > 0 {
attachedPublicKey, err = kr.GetArmoredPublicKey()
if err != nil {
return err
}
attachedPublicKeyName = "publickey - " + kr.Identities()[0].Name
}
message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName)
if err != nil {
return
}
clearBody := message.Body
externalID := message.Header.Get("Message-Id")
externalID = strings.Trim(externalID, "<>")
draftID, parentID := su.handleReferencesHeader(message)
if err = su.handleSenderAndRecipients(message, addr, from, to); err != nil {
return err
}
message.AddressID = addr.ID
// Apple Mail Message-Id has to be stored to avoid recovered message after each send.
// Before it was done only for Apple Mail, but it should work for any client. Also, the client
// is set up from IMAP and no one can be sure that the same client is used for SMTP as well.
// Also, user can use more than one client which could break the condition as well.
// If there is any problem, condition to Apple Mail only should be returned.
// Note: for that, we would need to refactor a little bit and pass the last client name from
// the IMAP through the bridge user.
message.ExternalID = externalID
// If Outlook does not get a response quickly, it will try to send the message again, leading
// to sending the same message multiple times. In case we detect the same message is in the
// sending queue, we wait a minute to finish the first request. If the message is still being
// sent after the timeout, we return an error back to the client. The UX is not the best,
// but it's better than sending the message many times. If the message was sent, we simply return
// nil to indicate it's OK.
sendRecorderMessageHash := su.backend.sendRecorder.getMessageHash(message)
isSending, wasSent := su.backend.sendRecorder.isSendingOrSent(su.client, sendRecorderMessageHash)
if isSending {
log.Debug("Message is in send queue, waiting")
time.Sleep(60 * time.Second)
isSending, wasSent = su.backend.sendRecorder.isSendingOrSent(su.client, sendRecorderMessageHash)
}
if isSending {
log.Debug("Message is still in send queue, returning error")
return errors.New("message is sending")
}
if wasSent {
log.Debug("Message was already sent")
return nil
}
message, atts, err := su.storeUser.CreateDraft(kr, message, attReaders, attachedPublicKey, attachedPublicKeyName, parentID)
if err != nil {
return
}
su.backend.sendRecorder.addMessage(sendRecorderMessageHash, message.ID)
// We always have to create a new draft even if there already is one,
// because clients don't necessarily save the draft before sending, which
// can lead to sending the wrong message. Also clients do not necessarily
// delete the old draft.
if draftID != "" {
if err := su.client.DeleteMessages([]string{draftID}); err != nil {
log.WithError(err).WithField("draftID", draftID).Warn("Original draft cannot be deleted")
}
}
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]*pmcrypto.SymmetricKey)
attkeysEncoded := make(map[string]pmapi.AlgoKey)
for _, att := range atts {
var keyPackets []byte
if keyPackets, err = base64.StdEncoding.DecodeString(att.KeyPackets); err != nil {
return errors.Wrap(err, "decoding attachment key packets")
}
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)
// PMEL 2.
settingsPgpScheme := mailSettings.PGPScheme
settingsSign := (mailSettings.Sign > 0)
// PMEL 3.
composeMode := message.MIMEType
var plainKey, htmlKey, mimeKey *pmcrypto.SymmetricKey
var plainData, htmlData, mimeData []byte
containsUnencryptedRecipients := false
for _, email := range to {
// PMEL 1.
contactEmails, err := su.client.GetContactEmailByEmail(email, 0, 1000)
if err != nil {
return err
}
var contactMeta *ContactMetadata
var contactKeys []*pmcrypto.KeyRing
for _, contactEmail := range contactEmails {
if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_
continue
}
contact, err := su.client.GetContactByID(contactEmail.ContactID)
if err != nil {
return err
}
decryptedCards, err := su.client.DecryptAndVerifyCards(contact.Cards)
if err != nil {
return err
}
contactMeta, err = GetContactMetadataFromVCards(decryptedCards, email)
if err != nil {
return err
}
for _, contactRawKey := range contactMeta.Keys {
contactKey, err := pmcrypto.ReadKeyRing(bytes.NewBufferString(contactRawKey))
if err != nil {
return err
}
contactKeys = append(contactKeys, contactKey)
}
break // We take the first hit where Defaults == 0, see "How to find the right contact" of PMEL
}
// PMEL 4.
apiRawKeyList, isInternal, err := su.client.GetPublicKeysForEmail(email)
if err != nil {
err = fmt.Errorf("backend: cannot get recipients' public keys: %v", err)
return err
}
var apiKeys []*pmcrypto.KeyRing
for _, apiRawKey := range apiRawKeyList {
var kr *pmcrypto.KeyRing
if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(apiRawKey.PublicKey)); err != nil {
return err
}
apiKeys = append(apiKeys, kr)
}
sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme)
if !sendingInfo.Encrypt {
containsUnencryptedRecipients = true
}
if err != nil {
return errors.New("error sending to user " + email + ": " + err.Error())
}
var signature int
if sendingInfo.Sign {
signature = pmapi.YesSignature
} else {
signature = pmapi.NoSignature
}
if sendingInfo.Scheme == pmapi.PGPMIMEPackage || sendingInfo.Scheme == pmapi.ClearMIMEPackage {
if mimeKey == nil {
if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil {
return err
}
}
if sendingInfo.Scheme == pmapi.PGPMIMEPackage {
mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*pmcrypto.SymmetricKey{})
if err != nil {
return err
}
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendingInfo.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature}
} else {
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
}
mimeSharedType |= sendingInfo.Scheme
} else {
switch sendingInfo.MIMEType {
case pmapi.ContentTypePlainText:
if plainKey == nil {
if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil {
return err
}
}
newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
if sendingInfo.Encrypt && sendingInfo.PublicKey != nil {
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, plainKey, attkeys)
if err != nil {
return err
}
}
plainAddressMap[email] = newAddress
plainSharedScheme |= sendingInfo.Scheme
case pmapi.ContentTypeHTML:
if htmlKey == nil {
if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil {
return err
}
}
newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
if sendingInfo.Encrypt && sendingInfo.PublicKey != nil {
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, htmlKey, attkeys)
if err != nil {
return err
}
}
htmlAddressMap[email] = newAddress
htmlSharedScheme |= sendingInfo.Scheme
}
}
}
if containsUnencryptedRecipients {
dec := new(mime.WordDecoder)
subject, err := dec.DecodeHeader(message.Header.Get("Subject"))
if err != nil {
return errors.New("error decoding subject message " + message.Header.Get("Subject"))
}
if !su.continueSendingUnencryptedMail(subject) {
_ = su.client.DeleteMessages([]string{message.ID})
return errors.New("sending was canceled by user")
}
}
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)
}
return su.storeUser.SendMessage(message.ID, req)
}
func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID string) {
// Remove the internal IDs from the references header before sending to avoid confusion.
references := m.Header.Get("References")
newReferences := []string{}
for _, reference := range strings.Fields(references) {
if !strings.Contains(reference, "@protonmail.internalid") {
newReferences = append(newReferences, reference)
} else { // internalid is the parentID.
idMatch := regexp.MustCompile("[a-zA-Z0-9-_=]*@protonmail.internalid").FindString(reference)
if idMatch != "" {
lastID := idMatch[0 : len(idMatch)-len("@protonmail.internalid")]
filter := &pmapi.MessagesFilter{ID: []string{lastID}}
if su.addressID != "" {
filter.AddressID = su.addressID
}
metadata, _, _ := su.client.ListMessages(filter)
for _, m := range metadata {
if isDraft(m) {
draftID = m.ID
} else {
parentID = m.ID
}
}
}
}
}
m.Header["References"] = newReferences
if parentID == "" && len(newReferences) > 0 {
externalID := strings.Trim(newReferences[len(newReferences)-1], "<>")
filter := &pmapi.MessagesFilter{ExternalID: externalID}
if su.addressID != "" {
filter.AddressID = su.addressID
}
metadata, _, _ := su.client.ListMessages(filter)
// There can be two or messages with the same external ID and then we cannot
// be sure which message should be parent. Better to not choose any.
if len(metadata) == 1 {
parentID = metadata[0].ID
}
}
return draftID, parentID
}
func isDraft(m *pmapi.Message) bool {
for _, labelID := range m.LabelIDs {
if labelID == pmapi.DraftLabel {
return true
}
}
return false
}
func (su *smtpUser) handleSenderAndRecipients(m *pmapi.Message, addr *pmapi.Address, from string, to []string) (err error) {
from = pmapi.ConstructAddress(from, addr.Email)
// Check sender.
if m.Sender == nil {
m.Sender = &mail.Address{Address: from}
} else {
m.Sender.Address = from
}
// Check recipients.
if len(to) == 0 {
err = errors.New("backend: no recipient specified")
return
}
// Sanitize ToList because some clients add *Sender* in the *ToList* when only Bcc is filled.
i := 0
for _, keep := range m.ToList {
keepThis := false
for _, addr := range to {
if addr == keep.Address {
keepThis = true
break
}
}
if keepThis {
m.ToList[i] = keep
i++
}
}
m.ToList = m.ToList[:i]
// Build a map of recipients visible to all.
// Bcc should be empty when sending a message.
var recipients []*mail.Address
recipients = append(recipients, m.ToList...)
recipients = append(recipients, m.CCList...)
recipients = append(recipients, m.BCCList...)
rm := map[string]bool{}
for _, r := range recipients {
rm[r.Address] = true
}
for _, r := range to {
if !rm[r] {
// Recipient is not known, add it to Bcc.
m.BCCList = append(m.BCCList, &mail.Address{Address: r})
}
}
return nil
}
func (su *smtpUser) continueSendingUnencryptedMail(subject string) bool {
if !su.backend.shouldReportOutgoingNoEnc() {
return true
}
messageID := strconv.Itoa(rand.Int()) //nolint[gosec]
ch := make(chan bool)
su.backend.shouldSendNoEncChannels[messageID] = ch
su.eventListener.Emit(events.OutgoingNoEncEvent, messageID+":"+subject)
log.Debug("Waiting for sendingUnencrypted confirmation for ", messageID)
var res bool
select {
case res = <-ch:
// GUI should always respond in 10 seconds, but let's have safety timeout
// in case GUI will not respond properly. If GUI didn't respond, we cannot
// be sure if user even saw the notice: better to not send the e-mail.
log.Debug("Got sendingUnencrypted for ", messageID, ": ", res)
case <-time.After(15 * time.Second):
log.Debug("sendingUnencrypted timeout, not sending ", messageID)
res = false
}
delete(su.backend.shouldSendNoEncChannels, messageID)
close(ch)
return res
}
// Logout is called when this User will no longer be used.
func (su *smtpUser) Logout() error {
log.Debug("SMTP client logged out user ", su.addressID)
return nil
}

96
internal/smtp/utils.go Normal file
View File

@ -0,0 +1,96 @@
// 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 smtp
import (
"encoding/base64"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func createPackets(
pubkey *pmcrypto.KeyRing,
bodyKey *pmcrypto.SymmetricKey,
attkeys map[string]*pmcrypto.SymmetricKey,
) (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 *pmcrypto.KeyRing,
textToEncrypt string,
canonicalizeText bool, // nolint[unparam]
) (key *pmcrypto.SymmetricKey, symEncryptedData []byte, err error) {
// We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones).
pgpMessage, err := kr.FirstKey().Encrypt(pmcrypto.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 *pmcrypto.SymmetricKey,
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
}

View File

@ -0,0 +1,94 @@
// 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 smtp
import (
"encoding/base64"
"strconv"
"strings"
"github.com/ProtonMail/go-vcard"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type ContactMetadata struct {
Email string
Keys []string
Scheme string
Sign bool
SignMissing bool
Encrypt bool
MIMEType string
}
const (
FieldPMScheme = "X-PM-SCHEME"
FieldPMEncrypt = "X-PM-ENCRYPT"
FieldPMSign = "X-PM-SIGN"
FieldPMMIMEType = "X-PM-MIMETYPE"
)
func GetContactMetadataFromVCards(cards []pmapi.Card, email string) (contactMeta *ContactMetadata, err error) {
for _, card := range cards {
dec := vcard.NewDecoder(strings.NewReader(card.Data))
parsedCard, err := dec.Decode()
if err != nil {
return nil, err
}
group := parsedCard.GetGroupByValue(vcard.FieldEmail, email)
if len(group) == 0 {
continue
}
keys := []string{}
for _, key := range parsedCard.GetAllValueByGroup(vcard.FieldKey, group) {
keybyte, err := base64.StdEncoding.DecodeString(strings.Split(key, "base64,")[1])
if err != nil {
return nil, err
}
// It would be better to always have correct data on the server, but mistakes
// can happen -- we had an issue where KEY was included in VCARD, but was empty.
// It's valid and we need to handle it by not including it in the keys, which would fail later.
if len(keybyte) > 0 {
keys = append(keys, string(keybyte))
}
}
scheme := parsedCard.GetValueByGroup(FieldPMScheme, group)
// Warn: ParseBool treats 1, T, True, true as true and 0, F, Fale, false as false.
// However PMEL declares 'true' is true, 'false' is false. every other string is true
encrypt, _ := strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMEncrypt, group))
var sign, signMissing bool
if len(parsedCard[FieldPMSign]) == 0 {
signMissing = true
} else {
sign, _ = strconv.ParseBool(parsedCard.GetValueByGroup(FieldPMSign, group))
signMissing = false
}
mimeType := parsedCard.GetValueByGroup(FieldPMMIMEType, group)
return &ContactMetadata{
Email: email,
Keys: keys,
Scheme: scheme,
Sign: sign,
SignMissing: signMissing,
Encrypt: encrypt,
MIMEType: mimeType,
}, nil
}
return &ContactMetadata{}, nil
}