forked from Silverfish/proton-bridge
We build too many walls and not enough bridges
This commit is contained in:
109
internal/smtp/backend.go
Normal file
109
internal/smtp/backend.go
Normal 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
65
internal/smtp/bridge.go
Normal 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()
|
||||
}
|
||||
124
internal/smtp/send_recorder.go
Normal file
124
internal/smtp/send_recorder.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
414
internal/smtp/send_recorder_test.go
Normal file
414
internal/smtp/send_recorder_test.go
Normal 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)
|
||||
}
|
||||
244
internal/smtp/sending_info.go
Normal file
244
internal/smtp/sending_info.go
Normal 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
|
||||
}
|
||||
604
internal/smtp/sending_info_test.go
Normal file
604
internal/smtp/sending_info_test.go
Normal 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
112
internal/smtp/server.go
Normal 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
25
internal/smtp/smtp.go
Normal 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
36
internal/smtp/store.go
Normal 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
513
internal/smtp/user.go
Normal 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
96
internal/smtp/utils.go
Normal 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
|
||||
}
|
||||
94
internal/smtp/vcard_tools.go
Normal file
94
internal/smtp/vcard_tools.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user