forked from Silverfish/proton-bridge
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 (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
@ -76,6 +75,76 @@ func (su *smtpUser) client() pmapi.Client {
|
|||||||
return su.user.GetTemporaryPMAPIClient()
|
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.
|
// 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]
|
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.
|
// 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)
|
htmlAddressMap := make(map[string]*pmapi.MessageAddress)
|
||||||
mimeAddressMap := 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 plainKey, htmlKey, mimeKey *crypto.SessionKey
|
||||||
var plainData, htmlData, mimeData []byte
|
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.`)
|
return errors.New(`"` + email + `" is not a valid recipient.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PMEL 1.
|
sendPreferences, err := su.getSendPreferences(email, message.MIMEType)
|
||||||
contactEmails, err := su.client().GetContactEmailByEmail(email, 0, 1000)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
var signature int
|
||||||
if sendingInfo.Sign {
|
if sendPreferences.Sign {
|
||||||
signature = pmapi.YesSignature
|
signature = pmapi.YesSignature
|
||||||
} else {
|
} else {
|
||||||
signature = pmapi.NoSignature
|
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 == nil {
|
||||||
if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil {
|
if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sendingInfo.Scheme == pmapi.PGPMIMEPackage {
|
if sendPreferences.Scheme == pmapi.PGPMIMEPackage {
|
||||||
mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*crypto.SessionKey{})
|
mimeBodyPacket, _, err := createPackets(sendPreferences.PublicKey, mimeKey, map[string]*crypto.SessionKey{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
} 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 {
|
} else {
|
||||||
switch sendingInfo.MIMEType {
|
switch sendPreferences.MIMEType {
|
||||||
case pmapi.ContentTypePlainText:
|
case pmapi.ContentTypePlainText:
|
||||||
if plainKey == nil {
|
if plainKey == nil {
|
||||||
if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil {
|
if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
|
newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
|
||||||
if sendingInfo.Encrypt && sendingInfo.PublicKey != nil {
|
if sendPreferences.Encrypt && sendPreferences.PublicKey != nil {
|
||||||
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, plainKey, attkeys)
|
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, plainKey, attkeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
plainAddressMap[email] = newAddress
|
plainAddressMap[email] = newAddress
|
||||||
plainSharedScheme |= sendingInfo.Scheme
|
plainSharedScheme |= sendPreferences.Scheme
|
||||||
case pmapi.ContentTypeHTML:
|
case pmapi.ContentTypeHTML:
|
||||||
if htmlKey == nil {
|
if htmlKey == nil {
|
||||||
if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil {
|
if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
|
newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
|
||||||
if sendingInfo.Encrypt && sendingInfo.PublicKey != nil {
|
if sendPreferences.Encrypt && sendPreferences.PublicKey != nil {
|
||||||
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, htmlKey, attkeys)
|
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, htmlKey, attkeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
htmlAddressMap[email] = newAddress
|
htmlAddressMap[email] = newAddress
|
||||||
htmlSharedScheme |= sendingInfo.Scheme
|
htmlSharedScheme |= sendPreferences.Scheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,6 +97,11 @@ type MailSettings struct {
|
|||||||
// AutoResponder string
|
// AutoResponder string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ComposerModeNormal = 0
|
||||||
|
ComposerModePlain = 1
|
||||||
|
)
|
||||||
|
|
||||||
// GetMailSettings gets contact details specified by contact ID.
|
// GetMailSettings gets contact details specified by contact ID.
|
||||||
func (c *client) GetMailSettings() (settings MailSettings, err error) {
|
func (c *client) GetMailSettings() (settings MailSettings, err error) {
|
||||||
req, err := c.NewRequest("GET", "/settings/mail", nil)
|
req, err := c.NewRequest("GET", "/settings/mail", nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user