mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 20:56:51 +00:00
refactor: builder pattern for generateSendingInfo
This commit is contained in:
78
internal/smtp/keys_test.go
Normal file
78
internal/smtp/keys_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
// 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
|
||||
|
||||
const testPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefEWSHl
|
||||
CjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39vPiLJXUq
|
||||
Zs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKiMeVa+GLEHhgZ
|
||||
2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5c8CmpqJuASIJNrSX
|
||||
M/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrbDEVRA2/BCJonw7aASiNC
|
||||
rSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEBAAHNBlVzZXJJRMLAcgQQAQgA
|
||||
JgUCVEltzwYLCQgHAwIJED62JZ7fId8kBBUIAgoDFgIBAhsDAh4BAAD0nQf9EtH9
|
||||
TC0JqSs8q194Zo244jjlJFM3EzxOSULq0zbywlLORfyoo/O8jU/HIuGz+LT98JDt
|
||||
nltTqfjWgu6pS3ZL2/L4AGUKEoB7OI6oIdRwzMc61sqI+Qpbzxo7rzufH4CiXZc6
|
||||
cxORUgL550xSCcqnq0q1mds7h5roKDzxMW6WLiEsc1dN8IQKzC7Ec5wA7U4oNGsJ
|
||||
3TyI8jkIs0IhXrRCd26K0TW8Xp6GCsfblWXosR13y89WVNgC+xrrJKTZEisc0tRl
|
||||
neIgjcwEUvwfIg2n9cDUFA/5BsfzTW5IurxqDEziIVP0L44PXjtJrBQaGMPlEbtP
|
||||
5i2oi3OADVX2XbvsRc7ATQRUSW3PAQgAkPnu5fps5zhOB/e618v/iF3KiogxUeRh
|
||||
A68TbvA+xnFfTxCx2Vo14aOL0CnaJ8gO5yRSqfomL2O1kMq07N1MGbqucbmc+aSf
|
||||
oElc+Gd5xBE/w3RcEhKcAaYTi35vG22zlZup4x3ElioyIarOssFEkQgNNyDf5AXZ
|
||||
jdHLA6qVxeqAb/Ff74+y9HUmLPSsRU9NwFzvK3Jv8C/ubHVLzTYdFgYkc4W1Uug9
|
||||
Ou08K+/4NEMrwnPFBbZdJAuUjQz2zW2ZiEKiBggiorH2o5N3mYUnWEmUvqL3EOS8
|
||||
TbWo8UBIW3DDm2JiZR8VrEgvBtc9mVDUj/x+5pR07Fy1D6DjRmAc9wARAQABwsBf
|
||||
BBgBCAATBQJUSW3SCRA+tiWe3yHfJAIbDAAA/iwH/ik9RKZMB9Ir0x5mGpKPuqhu
|
||||
gwrc3d04m1sOdXJm2NtD4ddzSEvzHwaPNvEvUl5v7FVMzf6+6mYGWHyNP4+e7Rtw
|
||||
YLlRpud6smuGyDSsotUYyumiqP6680ZIeWVQ+a1TThNs878mAJy1FhvQFdTmA8XI
|
||||
C616hDFpamQKPlpoO1a0wZnQhrPwT77HDYEEa+hqY4Jr/a7ui40S+7xYRHKL/7ZA
|
||||
S4/grWllhU3dbNrwSzrOKwrA/U0/9t738Ap6JL71YymDeaL4sutcoaahda1pTrMW
|
||||
ePtrCltz6uySwbZs7GXoEzjX3EAH+6qhkUJtzMaE3YEFEoQMGzcDTUEfXCJ3zJw=
|
||||
=yT9U
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
|
||||
const testOtherPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBF8Rmj4BCACgXXxRqLsmEUWZGd0f88BteXBfi9zL+9GysOTk4n9EgINLN2PU
|
||||
5rYSmWvVocO8IAfl/z9zpTJQesQjGe5lHbygUWFmjadox2ZeecZw0PWCSRdAjk6w
|
||||
Q4UX0JiCo3IuICZk1t53WWRtGnhA2Q21J4b2DJg4T5ZFKgKDzDhWoGF1ZStbI5X1
|
||||
0rKTGFNHgreV5PqxUjxHVtx3rgT9Mx+13QTffqKR9oaYC6mNs4TNJdhyqfaYxqGw
|
||||
ElxfdS9Wz6ODXrUNuSHETfgvAmo1Qep7GkefrC1isrmXA2+a+mXzFn4L0FCG073w
|
||||
Vi/lEw6R/vKfN6QukHPxwoSguow4wTyhRRmfABEBAAG0GVRlc3RUZXN0IDx0ZXN0
|
||||
dGVzdEBwbS5tZT6JAU4EEwEIADgWIQTsXZU1AxlWCPT02+BKdWAu4Q1jXQUCXxGa
|
||||
PgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBKdWAu4Q1jXQw+B/0ZudN+
|
||||
W9EqJtL/elm7Qla47zNsFmB+pHObdGoKtp3mNc97CQoW1yQ/i/V0heBFTAioP00g
|
||||
FgEk1ZUJfO++EtI8esNFdDZqY99826/Cl0FlJwubn/XYxi4XyaGTY1nhhyEJ2HWI
|
||||
/mZ+Jfm9ojbHSLwO5/AHiQt5t+LPDsKLXZw1BDJTgf1xD6e36CwAZgrPGWDqCXJ9
|
||||
BjlQn5hje7p0F8vYWBnnfSPkMHwibz9FlFqDh5v3XTgGpFIWDVkPVgAs8erM9AM2
|
||||
TjdpGcdW8xfcymo3j/o2QUBGYGJwPTsGEO5IkFRre9c/3REa7MKIi17Y479ub0A6
|
||||
2J3xgnqgI4sxmgmOuQENBF8Rmj4BCADX3BamNZsjC3I0knVIwjbz//1r8WOfNwGh
|
||||
gg5LsvpfLkrsNUZy+deSwb+hS9Auyr1xsMmtVyiTPGUXTjU4uUzY2zyTYWgYfSEi
|
||||
CojlXmYYLsjyPzR7KhVP6QIYZqYkOQXaCQDRlprRoFIEe4FzTCuqDHatJNwSesGy
|
||||
5pPJrjiAeb47m9KaoEIacoe9D3w1z4FCKN3A8cjiWT8NRfhYTBoE/T34oXVUj8l+
|
||||
jLIgVUQgGoBos160Z1Cnxd2PKWFVh/Br3QtIPTbNVDWhh5T1+N2ypbwsXCawy6fj
|
||||
cbOaTLz/vF9g+RJKC0MtxdL5qUtv3d3Zn07Sg+9H6wjsboAdAvirABEBAAGJATYE
|
||||
GAEIACAWIQTsXZU1AxlWCPT02+BKdWAu4Q1jXQUCXxGaPgIbDAAKCRBKdWAu4Q1j
|
||||
Xc4WB/9+aTGMMTlIdAFs9rf0i7i83pUOOxuLl34YQ0t5WGsjteQ4IK+gfuFvp37W
|
||||
ktv98ShOxAexbfqzGyGcYLLgaCxCbbB85fvSeX0xK/C2UbiH3Gv1z8GTelailCxt
|
||||
vyx642TwpcLXW1obHaHTSIi5L35Tce9gbug9sKCRSlAH76dANYBbMLa2Bl0LSrF8
|
||||
mcie9jJaPRXGOeHOyZmPZwwGhVYgadjptWqXnFz3ua8vxgqG0sefWF23F36iVz2q
|
||||
UjxSE+nKLaPFLlEDLgxG4SwHkcR9fi7zaQVnXg4rEjr0uz5MSUqZC4MNB4rkhU3g
|
||||
/rUMQyZupw+xJ+ayQNVBEtYZd/9u
|
||||
=TNX4
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
525
internal/smtp/preferences.go
Normal file
525
internal/smtp/preferences.go
Normal file
@ -0,0 +1,525 @@
|
||||
// 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/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
pgpInline = "pgp-inline"
|
||||
pgpMIME = "pgp-mime"
|
||||
pmInternal = "internal" // A mix between pgpInline and pgpMime used by PM.
|
||||
)
|
||||
|
||||
// SendPreferences contains information about how to handle a message.
|
||||
// It is derived from contact data, api key data, mail settings and composer preferences.
|
||||
type SendPreferences struct {
|
||||
// Encrypt indicates whether the email should be encrypted or not.
|
||||
// If it's encrypted, we need to know which public key to use.
|
||||
Encrypt bool
|
||||
|
||||
// Sign indicates whether the email should be signed or not.
|
||||
Sign bool
|
||||
|
||||
// Scheme indicates if we should encrypt body and attachments separately and
|
||||
// what MIME format to give the final encrypted email. The two standard PGP
|
||||
// schemes are PGP/MIME and PGP/Inline. However we use a custom scheme for
|
||||
// internal emails (including the so-called encrypted-to-outside emails,
|
||||
// which even though meant for external users, they don't really get out of
|
||||
// our platform). If the email is sent unencrypted, no PGP scheme is needed.
|
||||
Scheme int
|
||||
|
||||
// MIMEType is the MIME type to use for formatting the body of the email
|
||||
// (before encryption/after decryption). The standard possibilities are the
|
||||
// enriched HTML format, text/html, and plain text, text/plain. But it's
|
||||
// also possible to have a multipart/mixed format, which is typically used
|
||||
// for PGP/MIME encrypted emails, where attachments go into the body too.
|
||||
// Because of this, this option is sometimes called MIME format.
|
||||
MIMEType string
|
||||
|
||||
// PublicKey contains an OpenPGP key that can be used for encryption.
|
||||
PublicKey *crypto.KeyRing
|
||||
}
|
||||
|
||||
type sendPreferencesBuilder struct {
|
||||
internal bool
|
||||
encrypt *bool
|
||||
sign *bool
|
||||
scheme *string
|
||||
mimeType *string
|
||||
|
||||
publicKey *crypto.KeyRing
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withInternal() {
|
||||
b.internal = true
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) isInternal() bool {
|
||||
return b.internal
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withEncrypt(v bool) {
|
||||
b.encrypt = &v
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withEncryptDefault(v bool) {
|
||||
if b.encrypt == nil {
|
||||
b.encrypt = &v
|
||||
}
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) shouldEncrypt() bool {
|
||||
if b.encrypt != nil {
|
||||
return *b.encrypt
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withSign(v bool) {
|
||||
b.sign = &v
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withSignDefault(v bool) {
|
||||
if b.sign == nil {
|
||||
b.sign = &v
|
||||
}
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) shouldSign() bool {
|
||||
if b.sign != nil {
|
||||
return *b.sign
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withScheme(v string) {
|
||||
b.scheme = &v
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withSchemeDefault(v string) {
|
||||
if b.scheme == nil {
|
||||
b.scheme = &v
|
||||
}
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) getScheme() string {
|
||||
if b.scheme != nil {
|
||||
return *b.scheme
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withMIMEType(v string) {
|
||||
b.mimeType = &v
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withMIMETypeDefault(v string) {
|
||||
if b.mimeType == nil {
|
||||
b.mimeType = &v
|
||||
}
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) removeMIMEType() {
|
||||
b.mimeType = nil
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) getMIMEType() string {
|
||||
if b.mimeType != nil {
|
||||
return *b.mimeType
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) withPublicKey(v *crypto.KeyRing) {
|
||||
b.publicKey = v
|
||||
}
|
||||
|
||||
// Build converts the PGP scheme with a string value into a number value, and
|
||||
// we may override some of the other encryption preferences with the composer
|
||||
// preferences. Notice that the composer allows to select a sign preference,
|
||||
// an email format preference and an encrypt-to-outside preference. The
|
||||
// object we extract has the following possible value types:
|
||||
// {
|
||||
// encrypt: true | false,
|
||||
// sign: true | false,
|
||||
// pgpScheme: 1 (ProtonMail custom scheme)
|
||||
// | 2 (Protonmail scheme for encrypted-to-outside email)
|
||||
// | 4 (no cryptographic scheme)
|
||||
// | 8 (PGP/INLINE)
|
||||
// | 16 (PGP/MIME),
|
||||
// mimeType: 'text/html' | 'text/plain' | 'multipart/mixed',
|
||||
// publicKey: OpenPGPKey | undefined/null
|
||||
// }
|
||||
func (b *sendPreferencesBuilder) build() (p SendPreferences) {
|
||||
p.Encrypt = b.shouldEncrypt()
|
||||
p.Sign = b.shouldSign()
|
||||
p.MIMEType = b.getMIMEType()
|
||||
p.PublicKey = b.publicKey
|
||||
|
||||
switch {
|
||||
case b.isInternal():
|
||||
p.Scheme = pmapi.InternalPackage
|
||||
|
||||
case b.shouldSign() && b.shouldEncrypt():
|
||||
if b.getScheme() == pgpInline {
|
||||
p.Scheme = pmapi.PGPInlinePackage
|
||||
} else {
|
||||
p.Scheme = pmapi.PGPMIMEPackage
|
||||
}
|
||||
|
||||
case b.shouldSign() && !b.shouldEncrypt():
|
||||
p.Scheme = pmapi.ClearMIMEPackage
|
||||
|
||||
default:
|
||||
p.Scheme = pmapi.ClearPackage
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// setPGPSettings returns a SendPreferences with the following possible values:
|
||||
//
|
||||
// {
|
||||
// encrypt: true | false | undefined/null/'',
|
||||
// sign: true | false | undefined/null/'',
|
||||
// pgpScheme: 'pgp-mime' | 'pgp-inline' | undefined/null/'',
|
||||
// mimeType: 'text/html' | 'text/plain' | undefined/null/'',
|
||||
// publicKey: OpenPGPKey | undefined/null
|
||||
// }
|
||||
//
|
||||
// These settings are simply a reflection of the vCard content plus the public
|
||||
// key info retrieved from the API via the GET KEYS route.
|
||||
func (b *sendPreferencesBuilder) setPGPSettings(
|
||||
vCardData *ContactMetadata,
|
||||
apiKeys []pmapi.PublicKey,
|
||||
isInternal bool,
|
||||
) (err error) {
|
||||
// If there is no contact metadata, we can just use a default constructed one.
|
||||
if vCardData == nil {
|
||||
vCardData = &ContactMetadata{}
|
||||
}
|
||||
|
||||
// Sending internal.
|
||||
// We are guaranteed to always receive API keys.
|
||||
if isInternal {
|
||||
b.withInternal()
|
||||
return b.setInternalPGPSettings(vCardData, apiKeys)
|
||||
}
|
||||
|
||||
// Sending external but with keys supplied by WKD.
|
||||
// Treated pretty much same as internal.
|
||||
if len(apiKeys) > 0 {
|
||||
return b.setExternalPGPSettingsWithWKDKeys(vCardData, apiKeys)
|
||||
}
|
||||
|
||||
// Sending external without any WKD keys.
|
||||
// If we have a contact saved, we can use its settings.
|
||||
return b.setExternalPGPSettingsWithoutWKDKeys(vCardData)
|
||||
}
|
||||
|
||||
// setInternalPGPSettings returns SendPreferences for internal messages.
|
||||
// An internal address can be either an obvious one: abc@protonmail.com,
|
||||
// abc@protonmail.ch or abc@pm.me, or one belonging to a custom domain
|
||||
// registered with proton.
|
||||
func (b *sendPreferencesBuilder) setInternalPGPSettings(
|
||||
vCardData *ContactMetadata,
|
||||
apiKeys []pmapi.PublicKey,
|
||||
) (err error) {
|
||||
// We're guaranteed to get at least one valid (i.e. not expired, revoked or
|
||||
// marked as verification-only) public key from the server.
|
||||
if len(apiKeys) == 0 {
|
||||
return errors.New("an API key is necessary but wasn't provided")
|
||||
}
|
||||
|
||||
// We always encrypt and sign internal mail.
|
||||
b.withEncrypt(true)
|
||||
b.withSign(true)
|
||||
|
||||
// We use a custom scheme for internal messages.
|
||||
b.withScheme(pmInternal)
|
||||
|
||||
// If user has overridden the MIMEType for a contact, we use that.
|
||||
// Otherwise, we take the MIMEType from the composer.
|
||||
if vCardData.MIMEType != "" {
|
||||
b.withMIMEType(vCardData.MIMEType)
|
||||
}
|
||||
|
||||
sendingKey, err := pickSendingKey(vCardData, apiKeys)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.withPublicKey(sendingKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pickSendingKey tries to determine which key to use to encrypt outgoing mail.
|
||||
// It returns a keyring containing the chosen key or an error.
|
||||
//
|
||||
// 1. If there are pinned keys in the vCard, those should be given preference
|
||||
// (assuming the fingerprint matches one of the keys served by the API).
|
||||
// 2. If there are pinned keys in the vCard but no matching keys were served
|
||||
// by the API, we use one of the API keys but first show a modal to the
|
||||
// user to ask them to confirm that they trust the API key.
|
||||
// (Use case: user doesn't trust server, pins the only keys they trust to
|
||||
// the contact, rogue server sends unknown keys, user should have option
|
||||
// to say they don't recognise these keys and abort the mail send.)
|
||||
// 3. If there are no pinned keys, then the client should encrypt with the
|
||||
// first valid key served by the API (in principle the server already
|
||||
// validates the keys and the first one provided should be valid).
|
||||
func pickSendingKey(vCardData *ContactMetadata, rawAPIKeys []pmapi.PublicKey) (kr *crypto.KeyRing, err error) {
|
||||
contactKeys := make([]*crypto.Key, len(vCardData.Keys))
|
||||
apiKeys := make([]*crypto.Key, len(rawAPIKeys))
|
||||
|
||||
for i, key := range vCardData.Keys {
|
||||
var ck *crypto.Key
|
||||
|
||||
// Contact keys are not armored.
|
||||
if ck, err = crypto.NewKey([]byte(key)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
contactKeys[i] = ck
|
||||
}
|
||||
|
||||
for i, key := range rawAPIKeys {
|
||||
var ck *crypto.Key
|
||||
|
||||
// API keys are armored.
|
||||
if ck, err = crypto.NewKeyFromArmored(key.PublicKey); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
apiKeys[i] = ck
|
||||
}
|
||||
|
||||
matchedKeys := matchFingerprints(contactKeys, apiKeys)
|
||||
|
||||
var sendingKey *crypto.Key
|
||||
|
||||
switch {
|
||||
// Case 1.
|
||||
case len(matchedKeys) > 0:
|
||||
sendingKey = matchedKeys[0]
|
||||
|
||||
// Case 2.
|
||||
case len(matchedKeys) == 0 && len(contactKeys) > 0:
|
||||
// NOTE: Here we should ask for trust confirmation.
|
||||
sendingKey = apiKeys[0]
|
||||
|
||||
// Case 3.
|
||||
default:
|
||||
sendingKey = apiKeys[0]
|
||||
}
|
||||
|
||||
return crypto.NewKeyRing(sendingKey)
|
||||
}
|
||||
|
||||
func matchFingerprints(a, b []*crypto.Key) (res []*crypto.Key) {
|
||||
aMap := make(map[string]*crypto.Key)
|
||||
|
||||
for _, el := range a {
|
||||
aMap[el.GetFingerprint()] = el
|
||||
}
|
||||
|
||||
for _, el := range b {
|
||||
if _, inA := aMap[el.GetFingerprint()]; inA {
|
||||
res = append(res, el)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) setExternalPGPSettingsWithWKDKeys(
|
||||
vCardData *ContactMetadata,
|
||||
apiKeys []pmapi.PublicKey,
|
||||
) (err error) {
|
||||
// We're guaranteed to get at least one valid (i.e. not expired, revoked or
|
||||
// marked as verification-only) public key from the server.
|
||||
if len(apiKeys) == 0 {
|
||||
return errors.New("an API key is necessary but wasn't provided")
|
||||
}
|
||||
|
||||
// We always encrypt and sign external mail if WKD keys are present.
|
||||
b.withEncrypt(true)
|
||||
b.withSign(true)
|
||||
|
||||
// If the contact has a specific Scheme preference, we set it (otherwise we
|
||||
// leave it unset to allow it to be filled in with the default value later).
|
||||
if vCardData.Scheme != "" {
|
||||
b.withScheme(vCardData.Scheme)
|
||||
}
|
||||
|
||||
// Because the email is signed, the cryptographic scheme determines the email
|
||||
// format. A PGP/INLINE scheme forces to use plain text. A PGP/MIME scheme
|
||||
// forces the automatic format.
|
||||
switch vCardData.Scheme {
|
||||
case pgpMIME:
|
||||
b.removeMIMEType()
|
||||
case pgpInline:
|
||||
b.withMIMEType("text/plain")
|
||||
}
|
||||
|
||||
sendingKey, err := pickSendingKey(vCardData, apiKeys)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.withPublicKey(sendingKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) setExternalPGPSettingsWithoutWKDKeys(
|
||||
vCardData *ContactMetadata,
|
||||
) (err error) {
|
||||
b.withEncrypt(vCardData.Encrypt)
|
||||
|
||||
// Sign must be enabled whenever encrypt is.
|
||||
b.withSign(vCardData.Sign || vCardData.Encrypt)
|
||||
|
||||
// If the contact has a specific Scheme preference, we set it (otherwise we
|
||||
// leave it unset to allow it to be filled in with the default value later).
|
||||
if vCardData.Scheme != "" {
|
||||
b.withScheme(vCardData.Scheme)
|
||||
}
|
||||
|
||||
// If we are signing the message, the PGP scheme overrides the MIMEType.
|
||||
// Otherwise, we read the MIMEType from the vCard, if set.
|
||||
if vCardData.Sign {
|
||||
switch vCardData.Scheme {
|
||||
case pgpMIME:
|
||||
b.removeMIMEType()
|
||||
case pgpInline:
|
||||
b.withMIMEType("text/plain")
|
||||
}
|
||||
} else if vCardData.MIMEType != "" {
|
||||
b.withMIMEType(vCardData.MIMEType)
|
||||
}
|
||||
|
||||
if len(vCardData.Keys) > 0 {
|
||||
var key *crypto.Key
|
||||
|
||||
// Contact keys are not armored.
|
||||
if key, err = crypto.NewKey([]byte(vCardData.Keys[0])); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var kr *crypto.KeyRing
|
||||
|
||||
if kr, err = crypto.NewKeyRing(key); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.withPublicKey(kr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setEncryptionPreferences sets the undefined values in the SendPreferences
|
||||
// determined thus far using using the (global) user mail settings.
|
||||
// The object we extract has the following possible value types:
|
||||
//
|
||||
// {
|
||||
// encrypt: true | false,
|
||||
// sign: true | false,
|
||||
// pgpScheme: 'pgp-mime' | 'pgp-inline',
|
||||
// mimeType: 'text/html' | 'text/plain',
|
||||
// publicKey: OpenPGPKey | undefined/null
|
||||
// }
|
||||
//
|
||||
// The public key can still be undefined as we do not need it if the outgoing
|
||||
// email is not encrypted.
|
||||
func (b *sendPreferencesBuilder) setEncryptionPreferences(mailSettings pmapi.MailSettings) {
|
||||
// For internal addresses or external ones with WKD keys, this flag should
|
||||
// always be true. For external ones, an undefined flag defaults to false.
|
||||
b.withEncryptDefault(false)
|
||||
|
||||
// For internal addresses or external ones with WKD keys, this flag should
|
||||
// always be true. For external ones, an undefined flag defaults to the user
|
||||
// mail setting "Sign External messages". Otherwise we keep the defined value
|
||||
// unless it conflicts with the encrypt flag (we do not allow to send
|
||||
// encrypted but not signed).
|
||||
b.withSignDefault(mailSettings.Sign > 0)
|
||||
|
||||
if b.shouldEncrypt() {
|
||||
b.withSign(true)
|
||||
}
|
||||
|
||||
// If undefined, default to the user mail setting "Default PGP scheme".
|
||||
// Otherwise keep the defined value.
|
||||
switch mailSettings.PGPScheme {
|
||||
case pmapi.PGPInlinePackage:
|
||||
b.withSchemeDefault(pgpInline)
|
||||
case pmapi.PGPMIMEPackage:
|
||||
b.withSchemeDefault(pgpMIME)
|
||||
}
|
||||
|
||||
// Its value is constrained by the sign flag and the PGP scheme:
|
||||
// - Sign flag = true → For a PGP/Inline scheme, the MIME type must be
|
||||
// 'plain/text'. Otherwise we default to the user mail setting "Composer mode"
|
||||
// - Sign flag = false → If undefined, default to the user mail setting
|
||||
// "Composer mode". Otherwise keep the defined value.
|
||||
if b.shouldSign() && b.getScheme() == pgpInline {
|
||||
b.withMIMEType("text/plain")
|
||||
} else {
|
||||
switch mailSettings.ComposerMode {
|
||||
case pmapi.ComposerModeNormal:
|
||||
b.withMIMETypeDefault("text/html")
|
||||
case pmapi.ComposerModePlain:
|
||||
b.withMIMETypeDefault("text/plain")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *sendPreferencesBuilder) setMIMEPreferences(composerMIMEType string) {
|
||||
// If the sign flag (that we just determined above) is true we use the scheme
|
||||
// in the encryption preferences, unless the plain text format has been
|
||||
// selected in the composer, in which case we must enforce PGP/INLINE.
|
||||
if !b.isInternal() && b.shouldSign() && composerMIMEType == "text/plain" {
|
||||
b.withScheme(pgpInline)
|
||||
}
|
||||
|
||||
// If the sign flag (that we just determined above) is true, then the MIME
|
||||
// type is determined by the PGP scheme (also determined above): we should
|
||||
// use 'text/plain' for a PGP/Inline scheme, and 'multipart/mixed' otherwise.
|
||||
// Otherwise we use the MIME type from the encryption preferences, unless
|
||||
// the plain text option has been selecting in the composer, which should
|
||||
// enforce 'text/plain' and override the encryption preference.
|
||||
if b.shouldSign() {
|
||||
switch b.getScheme() {
|
||||
case pgpInline:
|
||||
b.withMIMEType("text/plain")
|
||||
default:
|
||||
b.withMIMEType("multipart/mixed")
|
||||
}
|
||||
} else if composerMIMEType == "text/plain" {
|
||||
b.withMIMEType("text/plain")
|
||||
}
|
||||
}
|
||||
356
internal/smtp/preferences_test.go
Normal file
356
internal/smtp/preferences_test.go
Normal file
@ -0,0 +1,356 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPreferencesBuilder(t *testing.T) {
|
||||
testContactKey := loadContactKey(t, testPublicKey)
|
||||
testOtherContactKey := loadContactKey(t, testOtherPublicKey)
|
||||
|
||||
tests := []struct { // nolint[maligned]
|
||||
name string
|
||||
|
||||
contactMeta *ContactMetadata
|
||||
receivedKeys []pmapi.PublicKey
|
||||
isInternal bool
|
||||
mailSettings pmapi.MailSettings
|
||||
composerMIMEType string
|
||||
|
||||
wantEncrypt bool
|
||||
wantSign bool
|
||||
wantScheme int
|
||||
wantMIMEType string
|
||||
wantPublicKey string
|
||||
}{
|
||||
{
|
||||
name: "internal",
|
||||
|
||||
contactMeta: &ContactMetadata{},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: true,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.InternalPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "internal with contact-specific email format",
|
||||
|
||||
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: true,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.InternalPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "internal with pinned contact public key",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: true,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.InternalPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
// NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation.
|
||||
name: "internal with conflicting contact public key",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: true,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.InternalPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "wkd-external",
|
||||
|
||||
contactMeta: &ContactMetadata{},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPMIMEPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "wkd-external with contact-specific email format",
|
||||
|
||||
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPMIMEPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "wkd-external with global pgp-inline scheme",
|
||||
|
||||
contactMeta: &ContactMetadata{},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPInlinePackage,
|
||||
wantMIMEType: "text/plain",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "wkd-external with contact-specific pgp-inline scheme overriding global pgp-mime setting",
|
||||
|
||||
contactMeta: &ContactMetadata{Scheme: pgpInline},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPInlinePackage,
|
||||
wantMIMEType: "text/plain",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "wkd-external with contact-specific pgp-mime scheme overriding global pgp-inline setting",
|
||||
|
||||
contactMeta: &ContactMetadata{Scheme: pgpMIME},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPMIMEPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "wkd-external with additional pinned contact public key",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPMIMEPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
// NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation.
|
||||
name: "wkd-external with additional conflicting contact public key",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testOtherContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{{PublicKey: testPublicKey}},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPMIMEPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "external",
|
||||
|
||||
contactMeta: &ContactMetadata{},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: false,
|
||||
wantScheme: pmapi.ClearPackage,
|
||||
wantMIMEType: "text/html",
|
||||
},
|
||||
|
||||
{
|
||||
name: "external with contact-specific email format",
|
||||
|
||||
contactMeta: &ContactMetadata{MIMEType: "text/plain"},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: false,
|
||||
wantScheme: pmapi.ClearPackage,
|
||||
wantMIMEType: "text/plain",
|
||||
},
|
||||
|
||||
{
|
||||
name: "external with sign enabled",
|
||||
|
||||
contactMeta: &ContactMetadata{Sign: true},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.ClearMIMEPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
},
|
||||
|
||||
{
|
||||
name: "external with pinned contact public key but no intention to encrypt/sign",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: false,
|
||||
wantScheme: pmapi.ClearPackage,
|
||||
wantMIMEType: "text/html",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "external with pinned contact public key, encrypted and signed",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPMIMEPackage,
|
||||
wantMIMEType: "multipart/mixed",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "external with pinned contact public key, encrypted and signed using contact-specific pgp-inline",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true, Scheme: pgpInline},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPInlinePackage,
|
||||
wantMIMEType: "text/plain",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
|
||||
{
|
||||
name: "external with pinned contact public key, encrypted and signed using global pgp-inline",
|
||||
|
||||
contactMeta: &ContactMetadata{Keys: []string{testContactKey}, Encrypt: true, Sign: true},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPInlinePackage},
|
||||
|
||||
wantEncrypt: true,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.PGPInlinePackage,
|
||||
wantMIMEType: "text/plain",
|
||||
wantPublicKey: testPublicKey,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test // Avoid using range scope test inside function literal.
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
b := &sendPreferencesBuilder{}
|
||||
|
||||
require.NoError(t, b.setPGPSettings(test.contactMeta, test.receivedKeys, test.isInternal))
|
||||
b.setEncryptionPreferences(test.mailSettings)
|
||||
b.setMIMEPreferences(test.composerMIMEType)
|
||||
|
||||
prefs := b.build()
|
||||
|
||||
assert.Equal(t, test.wantEncrypt, prefs.Encrypt)
|
||||
assert.Equal(t, test.wantSign, prefs.Sign)
|
||||
assert.Equal(t, test.wantScheme, prefs.Scheme)
|
||||
assert.Equal(t, test.wantMIMEType, prefs.MIMEType)
|
||||
assert.Equal(t, test.wantPublicKey, func() string {
|
||||
if prefs.PublicKey == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
k, _ := prefs.PublicKey.GetKey(0)
|
||||
s, _ := k.GetArmoredPublicKey()
|
||||
|
||||
return s
|
||||
}())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadContactKey(t *testing.T, key string) string {
|
||||
ck, err := crypto.NewKeyFromArmored(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
pk, err := ck.GetPublicKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
return string(pk)
|
||||
}
|
||||
@ -1,257 +0,0 @@
|
||||
// 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"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/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 *crypto.KeyRing
|
||||
}
|
||||
|
||||
func generateSendingInfo(
|
||||
eventListener listener.Listener,
|
||||
contactMeta *ContactMetadata,
|
||||
isInternal bool,
|
||||
composeMode string,
|
||||
apiKeys,
|
||||
contactKeys []*crypto.KeyRing,
|
||||
settingsSign bool,
|
||||
settingsPgpScheme int) (sendingInfo SendingInfo, err error) {
|
||||
contactKeys, err = crypto.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 []*crypto.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 []*crypto.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 []*crypto.KeyRing) (filteredKeys []*crypto.KeyRing, err error) { //nolint[unparam]
|
||||
keyIDsAreEqual := func(a, b interface{}) bool {
|
||||
aKey, bKey := a.(*crypto.KeyRing), b.(*crypto.KeyRing)
|
||||
|
||||
aFirst, getKeyErr := aKey.GetKey(0)
|
||||
if getKeyErr != nil {
|
||||
err = errors.New("missing primary key")
|
||||
return false
|
||||
}
|
||||
|
||||
bFirst, getKeyErr := bKey.GetKey(0)
|
||||
if getKeyErr != nil {
|
||||
err = errors.New("missing primary key")
|
||||
return false
|
||||
}
|
||||
|
||||
return aFirst.GetKeyID() == bFirst.GetKeyID()
|
||||
}
|
||||
|
||||
for _, v := range algo.SetIntersection(contactKeys, apiKeys, keyIDsAreEqual) {
|
||||
filteredKeys = append(filteredKeys, v.(*crypto.KeyRing))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@ -1,632 +0,0 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mocks struct {
|
||||
t *testing.T
|
||||
eventListener *users.MockListener
|
||||
}
|
||||
|
||||
func initMocks(t *testing.T) mocks {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
return mocks{
|
||||
t: t,
|
||||
eventListener: users.NewMockListener(mockCtrl),
|
||||
}
|
||||
}
|
||||
|
||||
type args struct {
|
||||
eventListener listener.Listener
|
||||
contactMeta *ContactMetadata
|
||||
apiKeys []*crypto.KeyRing
|
||||
contactKeys []*crypto.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.Encrypt, tt.wantSendingInfo.Encrypt)
|
||||
assert.Equal(t, gotSendingInfo.Sign, tt.wantSendingInfo.Sign)
|
||||
assert.Equal(t, gotSendingInfo.Scheme, tt.wantSendingInfo.Scheme)
|
||||
assert.Equal(t, gotSendingInfo.MIMEType, tt.wantSendingInfo.MIMEType)
|
||||
assert.True(t, keyRingsAreEqual(gotSendingInfo.PublicKey, tt.wantSendingInfo.PublicKey))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func keyRingFromKey(publicKey string) *crypto.KeyRing {
|
||||
key, err := crypto.NewKeyFromArmored(publicKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return kr
|
||||
}
|
||||
|
||||
func keyRingsAreEqual(a, b *crypto.KeyRing) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if a == nil && b != nil || a != nil && b == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
aKeys, bKeys := a.GetKeys(), b.GetKeys()
|
||||
|
||||
if len(aKeys) != len(bKeys) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range aKeys {
|
||||
aFPs := aKeys[i].GetSHA256Fingerprints()
|
||||
bFPs := bKeys[i].GetSHA256Fingerprints()
|
||||
|
||||
if !cmp.Equal(aFPs, bFPs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestGenerateSendingInfo_WithoutContact(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
|
||||
pubKey := keyRingFromKey(testPublicKey)
|
||||
|
||||
tests := []testData{
|
||||
{
|
||||
name: "internal, PGP_MIME",
|
||||
args: args{
|
||||
contactMeta: nil,
|
||||
isInternal: true,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.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 := keyRingFromKey(testPublicKey)
|
||||
|
||||
preferredPubKey := keyRingFromKey(testPublicKey)
|
||||
|
||||
differentPubKey := keyRingFromKey(testDifferentPublicKey)
|
||||
|
||||
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: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{preferredPubKey},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{pubKey},
|
||||
contactKeys: []*crypto.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 := keyRingFromKey(testPublicKey)
|
||||
|
||||
expiredPubKey := keyRingFromKey(testExpiredPublicKey)
|
||||
|
||||
tests := []testData{
|
||||
{
|
||||
name: "PGP_MIME, no pinned key",
|
||||
args: args{
|
||||
contactMeta: &ContactMetadata{},
|
||||
isInternal: false,
|
||||
composeMode: pmapi.ContentTypeHTML,
|
||||
apiKeys: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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: []*crypto.KeyRing{},
|
||||
contactKeys: []*crypto.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-----`
|
||||
@ -21,7 +21,6 @@ package smtp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/mail"
|
||||
@ -76,6 +75,76 @@ func (su *smtpUser) client() pmapi.Client {
|
||||
return su.user.GetTemporaryPMAPIClient()
|
||||
}
|
||||
|
||||
// Send sends an email from the given address to the given addresses with the given body.
|
||||
func (su *smtpUser) getSendPreferences(recipient, messageMIMEType string) (preferences SendPreferences, err error) {
|
||||
b := &sendPreferencesBuilder{}
|
||||
|
||||
// 1. contact vcard data
|
||||
vCardData, err := su.getContactVCardData(recipient)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. api key data
|
||||
apiKeys, isInternal, err := su.getAPIKeyData(recipient)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 1 + 2 -> 3. advanced PGP settings
|
||||
if err = b.setPGPSettings(vCardData, apiKeys, isInternal); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 4. mail settings
|
||||
mailSettings, err := su.client().GetMailSettings()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 3 + 4 -> 5. encryption preferences
|
||||
b.setEncryptionPreferences(mailSettings)
|
||||
|
||||
// 6. composer preferences -- in our case, this comes from the MIME type of the message.
|
||||
|
||||
// 5 + 6 -> 7. send preferences
|
||||
b.setMIMEPreferences(messageMIMEType)
|
||||
|
||||
return b.build(), nil
|
||||
}
|
||||
|
||||
func (su *smtpUser) getContactVCardData(recipient string) (meta *ContactMetadata, err error) {
|
||||
emails, err := su.client().GetContactEmailByEmail(recipient, 0, 1000)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, email := range emails {
|
||||
if email.Defaults == 1 {
|
||||
// NOTE: Can we still ignore this?
|
||||
continue
|
||||
}
|
||||
|
||||
var contact pmapi.Contact
|
||||
if contact, err = su.client().GetContactByID(email.ContactID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var cards []pmapi.Card
|
||||
if cards, err = su.client().DecryptAndVerifyCards(contact.Cards); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return GetContactMetadataFromVCards(cards, recipient)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (su *smtpUser) getAPIKeyData(recipient string) (apiKeys []pmapi.PublicKey, isInternal bool, err error) {
|
||||
return su.client().GetPublicKeysForEmail(recipient)
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -204,13 +273,6 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
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 *crypto.SessionKey
|
||||
var plainData, htmlData, mimeData []byte
|
||||
|
||||
@ -221,131 +283,65 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
return errors.New(`"` + email + `" is not a valid recipient.`)
|
||||
}
|
||||
|
||||
// PMEL 1.
|
||||
contactEmails, err := su.client().GetContactEmailByEmail(email, 0, 1000)
|
||||
sendPreferences, err := su.getSendPreferences(email, message.MIMEType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contactMeta *ContactMetadata
|
||||
var contactKeyRings []*crypto.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
|
||||
}
|
||||
contactKeyRing, err := crypto.NewKeyRing(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, contactRawKey := range contactMeta.Keys {
|
||||
contactKey, err := crypto.NewKey([]byte(contactRawKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := contactKeyRing.AddKey(contactKey); err != nil {
|
||||
return err
|
||||
}
|
||||
contactKeyRings = append(contactKeyRings, contactKeyRing)
|
||||
}
|
||||
|
||||
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 apiKeyRings []*crypto.KeyRing
|
||||
for _, apiRawKey := range apiRawKeyList {
|
||||
key, err := crypto.NewKeyFromArmored(apiRawKey.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kr, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiKeyRings = append(apiKeyRings, kr)
|
||||
}
|
||||
|
||||
sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeyRings, contactKeyRings, 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 {
|
||||
if sendPreferences.Sign {
|
||||
signature = pmapi.YesSignature
|
||||
} else {
|
||||
signature = pmapi.NoSignature
|
||||
}
|
||||
if sendingInfo.Scheme == pmapi.PGPMIMEPackage || sendingInfo.Scheme == pmapi.ClearMIMEPackage {
|
||||
if sendPreferences.Scheme == pmapi.PGPMIMEPackage || sendPreferences.Scheme == pmapi.ClearMIMEPackage {
|
||||
if mimeKey == nil {
|
||||
if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if sendingInfo.Scheme == pmapi.PGPMIMEPackage {
|
||||
mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*crypto.SessionKey{})
|
||||
if sendPreferences.Scheme == pmapi.PGPMIMEPackage {
|
||||
mimeBodyPacket, _, err := createPackets(sendPreferences.PublicKey, mimeKey, map[string]*crypto.SessionKey{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendingInfo.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature}
|
||||
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature}
|
||||
} else {
|
||||
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
|
||||
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
|
||||
}
|
||||
mimeSharedType |= sendingInfo.Scheme
|
||||
mimeSharedType |= sendPreferences.Scheme
|
||||
} else {
|
||||
switch sendingInfo.MIMEType {
|
||||
switch sendPreferences.MIMEType {
|
||||
case pmapi.ContentTypePlainText:
|
||||
if plainKey == nil {
|
||||
if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
|
||||
if sendingInfo.Encrypt && sendingInfo.PublicKey != nil {
|
||||
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, plainKey, attkeys)
|
||||
newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
|
||||
if sendPreferences.Encrypt && sendPreferences.PublicKey != nil {
|
||||
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, plainKey, attkeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
plainAddressMap[email] = newAddress
|
||||
plainSharedScheme |= sendingInfo.Scheme
|
||||
plainSharedScheme |= sendPreferences.Scheme
|
||||
case pmapi.ContentTypeHTML:
|
||||
if htmlKey == nil {
|
||||
if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
|
||||
if sendingInfo.Encrypt && sendingInfo.PublicKey != nil {
|
||||
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, htmlKey, attkeys)
|
||||
newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
|
||||
if sendPreferences.Encrypt && sendPreferences.PublicKey != nil {
|
||||
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, htmlKey, attkeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
htmlAddressMap[email] = newAddress
|
||||
htmlSharedScheme |= sendingInfo.Scheme
|
||||
htmlSharedScheme |= sendPreferences.Scheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,6 +97,11 @@ type MailSettings struct {
|
||||
// AutoResponder string
|
||||
}
|
||||
|
||||
const (
|
||||
ComposerModeNormal = 0
|
||||
ComposerModePlain = 1
|
||||
)
|
||||
|
||||
// GetMailSettings gets contact details specified by contact ID.
|
||||
func (c *client) GetMailSettings() (settings MailSettings, err error) {
|
||||
req, err := c.NewRequest("GET", "/settings/mail", nil)
|
||||
|
||||
Reference in New Issue
Block a user