diff --git a/internal/smtp/keys_test.go b/internal/smtp/keys_test.go new file mode 100644 index 00000000..48e7866e --- /dev/null +++ b/internal/smtp/keys_test.go @@ -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 . + +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-----` diff --git a/internal/smtp/preferences.go b/internal/smtp/preferences.go new file mode 100644 index 00000000..bb1f9ae8 --- /dev/null +++ b/internal/smtp/preferences.go @@ -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 . + +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") + } +} diff --git a/internal/smtp/preferences_test.go b/internal/smtp/preferences_test.go new file mode 100644 index 00000000..d08f3400 --- /dev/null +++ b/internal/smtp/preferences_test.go @@ -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 . + +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) +} diff --git a/internal/smtp/sending_info.go b/internal/smtp/sending_info.go deleted file mode 100644 index 122319e9..00000000 --- a/internal/smtp/sending_info.go +++ /dev/null @@ -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 . - -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 -} diff --git a/internal/smtp/sending_info_test.go b/internal/smtp/sending_info_test.go deleted file mode 100644 index 1cbf86ac..00000000 --- a/internal/smtp/sending_info_test.go +++ /dev/null @@ -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 . - -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-----` diff --git a/internal/smtp/user.go b/internal/smtp/user.go index b91e8f0b..adce4290 100644 --- a/internal/smtp/user.go +++ b/internal/smtp/user.go @@ -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 } } } diff --git a/pkg/pmapi/settings.go b/pkg/pmapi/settings.go index eefe3907..b06aeffd 100644 --- a/pkg/pmapi/settings.go +++ b/pkg/pmapi/settings.go @@ -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)