// Copyright (c) 2025 Proton AG // // This file is part of Proton Mail Bridge. // // Proton Mail 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. // // Proton Mail 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 Proton Mail Bridge. If not, see . package smtp import ( "testing" "github.com/ProtonMail/gluon/rfc822" "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/gopenpgp/v2/crypto" "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 *contactSettings receivedKeys []proton.PublicKey isInternal bool mailSettings proton.MailSettings composerMIMEType string wantEncrypt bool wantSign proton.SignatureType wantScheme proton.EncryptionScheme wantMIMEType rfc822.MIMEType wantPublicKey string }{ { name: "internal", contactMeta: &contactSettings{}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: true, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.InternalScheme, wantMIMEType: "text/html", wantPublicKey: testPublicKey, }, { name: "internal with contact-specific email format", contactMeta: &contactSettings{MIMEType: "text/plain"}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: true, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.InternalScheme, wantMIMEType: "text/plain", wantPublicKey: testPublicKey, }, { name: "internal with pinned contact public key", contactMeta: &contactSettings{Keys: []string{testContactKey}}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: true, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.InternalScheme, wantMIMEType: "text/html", 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: &contactSettings{Keys: []string{testOtherContactKey}}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: true, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.InternalScheme, wantMIMEType: "text/html", wantPublicKey: testPublicKey, }, { name: "wkd-external", contactMeta: &contactSettings{EncryptUntrusted: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPMIMEScheme, wantMIMEType: "multipart/mixed", wantPublicKey: testPublicKey, }, { name: "wkd-external", contactMeta: &contactSettings{EncryptUntrusted: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPMIMEScheme, wantMIMEType: "multipart/mixed", wantPublicKey: testPublicKey, }, { name: "wkd-external with contact-specific email format", contactMeta: &contactSettings{MIMEType: "text/plain", EncryptUntrusted: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPMIMEScheme, wantMIMEType: "multipart/mixed", wantPublicKey: testPublicKey, }, { name: "wkd-external with global pgp-inline scheme", contactMeta: &contactSettings{EncryptUntrusted: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPInlineScheme, wantMIMEType: "text/plain", wantPublicKey: testPublicKey, }, { name: "wkd-external with contact-specific pgp-inline scheme overriding global pgp-mime setting", contactMeta: &contactSettings{Scheme: pgpInline, EncryptUntrusted: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPInlineScheme, wantMIMEType: "text/plain", wantPublicKey: testPublicKey, }, { name: "wkd-external with contact-specific pgp-mime scheme overriding global pgp-inline setting", contactMeta: &contactSettings{Scheme: pgpMIME, EncryptUntrusted: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPMIMEScheme, wantMIMEType: "multipart/mixed", wantPublicKey: testPublicKey, }, { name: "wkd-external with additional pinned contact public key", contactMeta: &contactSettings{Keys: []string{testContactKey}, EncryptUntrusted: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPMIMEScheme, 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: &contactSettings{Keys: []string{testOtherContactKey}, EncryptUntrusted: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPMIMEScheme, wantMIMEType: "multipart/mixed", wantPublicKey: testPublicKey, }, { name: "wkd-external-with-encrypt-and-sign-disabled", contactMeta: &contactSettings{EncryptUntrusted: false}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: false, wantSign: proton.NoSignature, wantScheme: proton.ClearScheme, wantMIMEType: "text/html", wantPublicKey: testPublicKey, }, { name: "wkd-external-with-encrypt-and-sign-disabled-plain-text", contactMeta: &contactSettings{EncryptUntrusted: false}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/plain"}, wantEncrypt: false, wantSign: proton.NoSignature, wantScheme: proton.ClearScheme, wantMIMEType: "text/plain", wantPublicKey: testPublicKey, }, { name: "wkd-external-with-encrypt-disabled-sign-enabled", contactMeta: &contactSettings{EncryptUntrusted: false, Sign: true, SignIsSet: true}, receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: false, wantSign: proton.DetachedSignature, wantScheme: proton.ClearMIMEScheme, wantMIMEType: "multipart/mixed", wantPublicKey: testPublicKey, }, { name: "external", contactMeta: &contactSettings{}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: false, wantSign: proton.NoSignature, wantScheme: proton.ClearScheme, wantMIMEType: "text/html", }, { name: "external with contact-specific email format", contactMeta: &contactSettings{MIMEType: "text/plain"}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: false, wantSign: proton.NoSignature, wantScheme: proton.ClearScheme, wantMIMEType: "text/plain", }, { name: "external with sign enabled", contactMeta: &contactSettings{Sign: true, SignIsSet: true}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: false, wantSign: proton.DetachedSignature, wantScheme: proton.ClearMIMEScheme, wantMIMEType: "multipart/mixed", }, { name: "external with contact sign enabled and plain text", contactMeta: &contactSettings{MIMEType: "text/plain", Scheme: pgpInline, Sign: true, SignIsSet: true}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: false, wantSign: proton.DetachedSignature, wantScheme: proton.ClearScheme, wantMIMEType: "text/plain", }, { name: "external with sign enabled, sending plaintext, should still send as ClearMIME", contactMeta: &contactSettings{Sign: true, SignIsSet: true}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/plain"}, wantEncrypt: false, wantSign: proton.DetachedSignature, wantScheme: proton.ClearMIMEScheme, wantMIMEType: "multipart/mixed", }, { name: "external with pinned contact public key but no intention to encrypt/sign", contactMeta: &contactSettings{Keys: []string{testContactKey}}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: false, wantSign: proton.NoSignature, wantScheme: proton.ClearScheme, wantMIMEType: "text/html", wantPublicKey: testPublicKey, }, { name: "external with pinned contact public key, encrypted and signed", contactMeta: &contactSettings{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPMIMEScheme, wantMIMEType: "multipart/mixed", wantPublicKey: testPublicKey, }, { name: "external with pinned contact public key, encrypted and signed using contact-specific pgp-inline", contactMeta: &contactSettings{Keys: []string{testContactKey}, Encrypt: true, Sign: true, Scheme: pgpInline, SignIsSet: true}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPInlineScheme, wantMIMEType: "text/plain", wantPublicKey: testPublicKey, }, { name: "external with pinned contact public key, encrypted and signed using global pgp-inline", contactMeta: &contactSettings{Keys: []string{testContactKey}, Encrypt: true, Sign: true, SignIsSet: true}, receivedKeys: []proton.PublicKey{}, isInternal: false, mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"}, wantEncrypt: true, wantSign: proton.DetachedSignature, wantScheme: proton.PGPInlineScheme, wantMIMEType: "text/plain", wantPublicKey: testPublicKey, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { b := &sendPrefsBuilder{} 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.SignatureType) assert.Equal(t, test.wantScheme, prefs.EncryptionScheme) assert.Equal(t, test.wantMIMEType, prefs.MIMEType) if prefs.PubKey != nil { wantKey, err := crypto.NewKeyFromArmored(test.wantPublicKey) require.NoError(t, err) haveKey, err := prefs.PubKey.GetKey(0) require.NoError(t, err) assert.Equal(t, wantKey.GetFingerprint(), haveKey.GetFingerprint()) } }) } } 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) } 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-----`