refactor: builder pattern for generateSendingInfo

This commit is contained in:
James Houlahan
2020-06-29 15:33:42 +02:00
parent 29978b7014
commit 61a841ced7
7 changed files with 1051 additions and 980 deletions

View 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-----`

View 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")
}
}

View 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)
}

View File

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

View File

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

View File

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

View File

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