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)