diff --git a/Makefile b/Makefile index a24075b3..bd26d15d 100644 --- a/Makefile +++ b/Makefile @@ -294,6 +294,7 @@ LOG?=debug LOG_IMAP?=client # client/server/all, or empty to turn it off LOG_SMTP?=--log-smtp # empty to turn it off RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP} +RUN_FLAGS_IE?=-m -l=${LOG} run: run-nogui-cli @@ -316,11 +317,11 @@ run-ie-qml-preview: $(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview run-ie: - TARGET_CMD=Import-Export $(MAKE) run + TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run run-ie-qt: - TARGET_CMD=Import-Export $(MAKE) run-qt + TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-qt run-ie-nogui: - TARGET_CMD=Import-Export $(MAKE) run-nogui + TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run-nogui clean-frontend-qt: $(MAKE) -C internal/frontend/qt -f Makefile.local clean diff --git a/internal/app/base/base.go b/internal/app/base/base.go index 11a7254d..b0c9cd0e 100644 --- a/internal/app/base/base.go +++ b/internal/app/base/base.go @@ -136,6 +136,7 @@ func New( // nolint[funlen] if err := logging.Init(logsPath); err != nil { return nil, err } + logging.SetLevel("debug") // Proper level is set later in run. crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath)) if err := migrateFiles(configName); err != nil { diff --git a/internal/app/base/migration.go b/internal/app/base/migration.go index 7280d594..203dd7a0 100644 --- a/internal/app/base/migration.go +++ b/internal/app/base/migration.go @@ -29,10 +29,12 @@ import ( // migrateFiles migrates files from their old (pre-refactor) locations to their new locations. // We can remove this eventually. // -// | entity | old location | new location | -// |--------|-------------------------------------------|----------------------------------------| -// | prefs | ~/.cache/protonmail//c11/prefs.json | ~/.config/protonmail//prefs.json | -// | c11 | ~/.cache/protonmail//c11 | ~/.cache/protonmail//cache/c11 | +// | entity | old location | new location | +// |-----------|-------------------------------------------|----------------------------------------| +// | prefs | ~/.cache/protonmail//c11/prefs.json | ~/.config/protonmail//prefs.json | +// | c11 1.5.x | ~/.cache/protonmail//c11 | ~/.cache/protonmail//cache/c11 | +// | c11 1.6.x | ~/.cache/protonmail//cache/c11 | ~/.config/protonmail//cache/c11 | +// | updates | ~/.cache/protonmail//updates | ~/.config/protonmail//updates | func migrateFiles(configName string) error { locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName)) if err != nil { @@ -41,43 +43,81 @@ func migrateFiles(configName string) error { locations := locations.New(locationsProvider, configName) userCacheDir := locationsProvider.UserCache() + + if err := migratePrefsFrom15x(locations, userCacheDir); err != nil { + return err + } + if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil { + return err + } + if err := migrateUpdatesFrom16x(locations); err != nil { + return err + } + return nil +} + +func migratePrefsFrom15x(locations *locations.Locations, userCacheDir string) error { newSettingsDir, err := locations.ProvideSettingsPath() if err != nil { return err } - if err := moveIfExists( + return moveIfExists( filepath.Join(userCacheDir, "c11", "prefs.json"), filepath.Join(newSettingsDir, "prefs.json"), - ); err != nil { - return err - } + ) +} - newCacheDir, err := locations.ProvideCachePath() +func migrateCacheFromBoth15xAnd16x(locations *locations.Locations, userCacheDir string) error { + olderCacheDir := userCacheDir + newerCacheDir := locations.GetOldCachePath() + latestCacheDir, err := locations.ProvideCachePath() if err != nil { return err } + // Migration for versions before 1.6.x. if err := moveIfExists( - filepath.Join(userCacheDir, "c11"), - filepath.Join(newCacheDir, "c11"), + filepath.Join(olderCacheDir, "c11"), + filepath.Join(latestCacheDir, "c11"), ); err != nil { return err } - return nil + // Migration for versions 1.6.x. + return moveIfExists( + filepath.Join(newerCacheDir, "c11"), + filepath.Join(latestCacheDir, "c11"), + ) +} + +func migrateUpdatesFrom16x(locations *locations.Locations) error { + oldUpdatesPath := locations.GetOldUpdatesPath() + // Do not use ProvideUpdatesPath, that creates dir right away. + newUpdatesPath := locations.GetUpdatesPath() + + return moveIfExists(oldUpdatesPath, newUpdatesPath) } func moveIfExists(source, destination string) error { + l := logrus.WithField("source", source).WithField("destination", destination) + if _, err := os.Stat(source); os.IsNotExist(err) { - logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file") + l.Debug("No need to migrate file, source doesn't exist") return nil } if _, err := os.Stat(destination); !os.IsNotExist(err) { - logrus.WithField("source", source).WithField("destination", destination).Debug("No need to migrate file") + // Once migrated, files should not stay in source anymore. Therefore + // if some files are still in source location but target already exist, + // it's suspicious. Could happen by installing new version, then the + // old one because of some reason, and then the new one again. + // Good to see as warning because it could be a reason why Bridge is + // behaving weirdly, like wrong configuration, or db re-sync and so on. + l.Warn("No need to migrate file, target already exists") return nil } + l.Info("Migrating files") return os.Rename(source, destination) } diff --git a/internal/frontend/cli-ie/importexport.go b/internal/frontend/cli-ie/importexport.go index d0f324a2..3463c4c0 100644 --- a/internal/frontend/cli-ie/importexport.go +++ b/internal/frontend/cli-ie/importexport.go @@ -38,7 +38,7 @@ func (f *frontendCLI) importLocalMessages(c *ishell.Context) { return } - t, err := f.ie.GetLocalImporter(user.GetPrimaryAddress(), path) + t, err := f.ie.GetLocalImporter(user.Username(), user.GetPrimaryAddress(), path) f.transfer(t, err, false, true) } @@ -68,7 +68,7 @@ func (f *frontendCLI) importRemoteMessages(c *ishell.Context) { return } - t, err := f.ie.GetRemoteImporter(user.GetPrimaryAddress(), username, password, host, port) + t, err := f.ie.GetRemoteImporter(user.Username(), user.GetPrimaryAddress(), username, password, host, port) f.transfer(t, err, false, true) } @@ -81,7 +81,7 @@ func (f *frontendCLI) exportMessagesToEML(c *ishell.Context) { return } - t, err := f.ie.GetEMLExporter(user.GetPrimaryAddress(), path) + t, err := f.ie.GetEMLExporter(user.Username(), user.GetPrimaryAddress(), path) f.transfer(t, err, true, false) } @@ -94,7 +94,7 @@ func (f *frontendCLI) exportMessagesToMBOX(c *ishell.Context) { return } - t, err := f.ie.GetMBOXExporter(user.GetPrimaryAddress(), path) + t, err := f.ie.GetMBOXExporter(user.Username(), user.GetPrimaryAddress(), path) f.transfer(t, err, true, false) } diff --git a/internal/frontend/qml/ImportExportUI/AccountDelegate.qml b/internal/frontend/qml/ImportExportUI/AccountDelegate.qml index c4d0598e..25dcca75 100644 --- a/internal/frontend/qml/ImportExportUI/AccountDelegate.qml +++ b/internal/frontend/qml/ImportExportUI/AccountDelegate.qml @@ -165,6 +165,7 @@ Column { textColor : Style.main.textBlue onClicked: { dialogExport.currentIndex = 0 + dialogExport.account = account dialogExport.address = account dialogExport.show() } @@ -321,6 +322,7 @@ Column { textBold: true textColor: Style.main.textBlue onClicked: { + dialogExport.account = account dialogExport.address = listalias[index] dialogExport.show() } @@ -339,6 +341,7 @@ Column { textBold: true textColor: enabled ? Style.main.textBlue : Style.main.textDisabled onClicked: { + dialogImport.account = account dialogImport.address = listalias[index] dialogImport.show() } diff --git a/internal/frontend/qml/ImportExportUI/DialogExport.qml b/internal/frontend/qml/ImportExportUI/DialogExport.qml index dfb69131..cf457245 100644 --- a/internal/frontend/qml/ImportExportUI/DialogExport.qml +++ b/internal/frontend/qml/ImportExportUI/DialogExport.qml @@ -35,6 +35,7 @@ Dialog { title : set_title() + property string account property string address property alias finish: finish @@ -428,7 +429,7 @@ Dialog { onTriggered : { switch (currentIndex) { case 0: - go.loadStructureForExport(root.address) + go.loadStructureForExport(root.account, root.address) sourceFoldersInput.hasItems = (transferRules.rowCount() > 0) break case 2: diff --git a/internal/frontend/qml/ImportExportUI/DialogImport.qml b/internal/frontend/qml/ImportExportUI/DialogImport.qml index d8dda071..92b3a0b1 100644 --- a/internal/frontend/qml/ImportExportUI/DialogImport.qml +++ b/internal/frontend/qml/ImportExportUI/DialogImport.qml @@ -34,6 +34,7 @@ Dialog { isDialogBusy: currentIndex==3 || currentIndex==4 + property string account property string address property string inputPath : "" property bool isFromFile : inputEmail.text == "" && root.inputPath != "" @@ -1032,6 +1033,7 @@ Dialog { root.isFromIMAP, root.inputPath, inputEmail.text, inputPassword.text, inputServer.text, inputPort.text, + root.account, root.address ) break diff --git a/internal/frontend/qml/ProtonUI/Dialog.qml b/internal/frontend/qml/ProtonUI/Dialog.qml index 006df527..eaaefcfb 100644 --- a/internal/frontend/qml/ProtonUI/Dialog.qml +++ b/internal/frontend/qml/ProtonUI/Dialog.qml @@ -18,6 +18,7 @@ // Dialog with adding new user import QtQuick 2.8 +import QtQuick.Controls 2.1 import QtQuick.Layouts 1.3 import ProtonUI 1.0 @@ -83,6 +84,9 @@ StackLayout { text : "" color: Style.main.textBlue visible: false + width: root.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap } // prevent any action below diff --git a/internal/frontend/qml/ProtonUI/DialogAddUser.qml b/internal/frontend/qml/ProtonUI/DialogAddUser.qml index f7c2c8a6..ba619540 100644 --- a/internal/frontend/qml/ProtonUI/DialogAddUser.qml +++ b/internal/frontend/qml/ProtonUI/DialogAddUser.qml @@ -70,7 +70,8 @@ Dialog { id: topSep color : "transparent" width : Style.main.dummy - height : root.height/2 - (dialogNameAndPassword.heightInputs)/2 + // Hacky hack: +10 is to make title of Dialog bigger so longer error can fit just fine. + height : root.height/2 + 10 - (dialogNameAndPassword.heightInputs)/2 } InputField { diff --git a/internal/frontend/qt-ie/export.go b/internal/frontend/qt-ie/export.go index ba2ec17c..a91b3ea3 100644 --- a/internal/frontend/qt-ie/export.go +++ b/internal/frontend/qt-ie/export.go @@ -29,7 +29,7 @@ const ( TypeMBOX = "MBOX" ) -func (f *FrontendQt) LoadStructureForExport(addressOrID string) { +func (f *FrontendQt) LoadStructureForExport(username, addressOrID string) { errCode := errUnknownError var err error defer func() { @@ -41,7 +41,7 @@ func (f *FrontendQt) LoadStructureForExport(addressOrID string) { } }() - if f.transfer, err = f.ie.GetEMLExporter(addressOrID, ""); err != nil { + if f.transfer, err = f.ie.GetEMLExporter(username, addressOrID, ""); err != nil { // The only error can be problem to load PM user and address. errCode = errPMLoadFailed return diff --git a/internal/frontend/qt-ie/import.go b/internal/frontend/qt-ie/import.go index e72d38bb..c36b376e 100644 --- a/internal/frontend/qt-ie/import.go +++ b/internal/frontend/qt-ie/import.go @@ -26,7 +26,7 @@ import ( ) // wrapper for QML -func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetAddress string) { +func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetUsername, targetAddress string) { errCode := errUnknownError var err error defer func() { @@ -39,7 +39,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm }() if isFromIMAP { - f.transfer, err = f.ie.GetRemoteImporter(targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort) + f.transfer, err = f.ie.GetRemoteImporter(targetUsername, targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort) if err != nil { switch { case errors.Is(err, &transfer.ErrIMAPConnection{}): @@ -54,7 +54,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm return } } else { - f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath) + f.transfer, err = f.ie.GetLocalImporter(targetUsername, targetAddress, sourcePath) if err != nil { // The only error can be problem to load PM user and address. errCode = errPMLoadFailed diff --git a/internal/frontend/qt-ie/ui.go b/internal/frontend/qt-ie/ui.go index 52903e67..edb4f870 100644 --- a/internal/frontend/qt-ie/ui.go +++ b/internal/frontend/qt-ie/ui.go @@ -77,8 +77,8 @@ type GoQMLInterface struct { _ string `property:"credentialsNotRemoved"` _ string `property:"versionCheckFailed"` // - _ func(isAvailable bool) `signal:"setConnectionStatus"` - _ func() `slot:"checkInternet"` + _ func(isAvailable bool) `signal:"setConnectionStatus"` + _ func() `slot:"checkInternet"` _ func() `slot:"setToRestart"` @@ -108,14 +108,14 @@ type GoQMLInterface struct { _ func(description, client, address string) bool `slot:"sendBug"` _ func(address string) bool `slot:"sendImportReport"` - _ func(address string) `slot:"loadStructureForExport"` + _ func(username, address string) `slot:"loadStructureForExport"` _ func() string `slot:"leastUsedColor"` _ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"` _ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"` _ func(email string, importEncrypted bool) `slot:"startImport"` _ func() `slot:"resetSource"` - _ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetAddress string) `slot:"setupAndLoadForImport"` + _ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetUsername, targetAddress string) `slot:"setupAndLoadForImport"` _ string `property:"progressInit"` diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go index b32e66aa..8f6556fc 100644 --- a/internal/frontend/types/types.go +++ b/internal/frontend/types/types.go @@ -114,10 +114,10 @@ func (b *bridgeWrap) GetUser(query string) (User, error) { type ImportExporter interface { UserManager - GetLocalImporter(string, string) (*transfer.Transfer, error) - GetRemoteImporter(string, string, string, string, string) (*transfer.Transfer, error) - GetEMLExporter(string, string) (*transfer.Transfer, error) - GetMBOXExporter(string, string) (*transfer.Transfer, error) + GetLocalImporter(string, string, string) (*transfer.Transfer, error) + GetRemoteImporter(string, string, string, string, string, string) (*transfer.Transfer, error) + GetEMLExporter(string, string, string) (*transfer.Transfer, error) + GetMBOXExporter(string, string, string) (*transfer.Transfer, error) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error ReportFile(osType, osVersion, accountName, address string, logdata []byte) error } diff --git a/internal/imap/mailbox_message.go b/internal/imap/mailbox_message.go index 84793d25..272181ed 100644 --- a/internal/imap/mailbox_message.go +++ b/internal/imap/mailbox_message.go @@ -564,7 +564,7 @@ func (im *imapMailbox) writeRelatedPart(p io.Writer, m *pmapi.Message, inlines [ return } - h := message.GetAttachmentHeader(inline) + h := message.GetAttachmentHeader(inline, true) if p, err = related.CreatePart(h); err != nil { return } @@ -738,7 +738,7 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) ( defer buf.Reset() att := atts[idx] - attachmentHeader := message.GetAttachmentHeader(att) + attachmentHeader := message.GetAttachmentHeader(att, true) if partWriter, err = mw.CreatePart(attachmentHeader); err != nil { return err } diff --git a/internal/importexport/importexport.go b/internal/importexport/importexport.go index a3799c73..0dc94491 100644 --- a/internal/importexport/importexport.go +++ b/internal/importexport/importexport.go @@ -118,9 +118,9 @@ func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address strin } // GetLocalImporter returns transferrer from local EML or MBOX structure to ProtonMail account. -func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transfer, error) { +func (ie *ImportExport) GetLocalImporter(username, address, path string) (*transfer.Transfer, error) { source := transfer.NewLocalProvider(path) - target, err := ie.getPMAPIProvider(address) + target, err := ie.getPMAPIProvider(username, address) if err != nil { return nil, err } @@ -132,12 +132,12 @@ func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transf } // GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account. -func (ie *ImportExport) GetRemoteImporter(address, username, password, host, port string) (*transfer.Transfer, error) { - source, err := transfer.NewIMAPProvider(username, password, host, port) +func (ie *ImportExport) GetRemoteImporter(username, address, remoteUsername, remotePassword, host, port string) (*transfer.Transfer, error) { + source, err := transfer.NewIMAPProvider(remoteUsername, remotePassword, host, port) if err != nil { return nil, err } - target, err := ie.getPMAPIProvider(address) + target, err := ie.getPMAPIProvider(username, address) if err != nil { return nil, err } @@ -149,8 +149,8 @@ func (ie *ImportExport) GetRemoteImporter(address, username, password, host, por } // GetEMLExporter returns transferrer from ProtonMail account to local EML structure. -func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer, error) { - source, err := ie.getPMAPIProvider(address) +func (ie *ImportExport) GetEMLExporter(username, address, path string) (*transfer.Transfer, error) { + source, err := ie.getPMAPIProvider(username, address) if err != nil { return nil, err } @@ -163,8 +163,8 @@ func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer } // GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure. -func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfer, error) { - source, err := ie.getPMAPIProvider(address) +func (ie *ImportExport) GetMBOXExporter(username, address, path string) (*transfer.Transfer, error) { + source, err := ie.getPMAPIProvider(username, address) if err != nil { return nil, err } @@ -176,8 +176,8 @@ func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfe return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target) } -func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvider, error) { - user, err := ie.Users.GetUser(address) +func (ie *ImportExport) getPMAPIProvider(username, address string) (*transfer.PMAPIProvider, error) { + user, err := ie.Users.GetUser(username) if err != nil { return nil, err } diff --git a/internal/locations/locations.go b/internal/locations/locations.go index bc851a2d..317ecf3b 100644 --- a/internal/locations/locations.go +++ b/internal/locations/locations.go @@ -32,8 +32,8 @@ import ( // On linux: // - settings: ~/.config/protonmail/ // - logs: ~/.cache/protonmail//logs -// - cache: ~/.cache/protonmail//cache -// - updates: ~/.cache/protonmail//updates +// - cache: ~/.config/protonmail//cache +// - updates: ~/.config/protonmail//updates // - lockfile: ~/.cache/protonmail//.lock type Locations struct { userConfig, userCache string @@ -129,7 +129,7 @@ func (l *Locations) ProvideLogsPath() (string, error) { return l.getLogsPath(), nil } -// ProvideCachePath returns a location for user cache dirs (e.g. ~/.cache///cache). +// ProvideCachePath returns a location for user cache dirs (e.g. ~/.config///cache). // It creates it if it doesn't already exist. func (l *Locations) ProvideCachePath() (string, error) { if err := os.MkdirAll(l.getCachePath(), 0700); err != nil { @@ -139,6 +139,11 @@ func (l *Locations) ProvideCachePath() (string, error) { return l.getCachePath(), nil } +// GetOldCachePath returns a former location for user cache dirs used for migration scripts only. +func (l *Locations) GetOldCachePath() string { + return filepath.Join(l.userCache, "cache") +} + // ProvideUpdatesPath returns a location for update files (e.g. ~/.cache///updates). // It creates it if it doesn't already exist. func (l *Locations) ProvideUpdatesPath() (string, error) { @@ -149,6 +154,16 @@ func (l *Locations) ProvideUpdatesPath() (string, error) { return l.getUpdatesPath(), nil } +// GetUpdatesPath returns a new location for update files used for migration scripts only. +func (l *Locations) GetUpdatesPath() string { + return l.getUpdatesPath() +} + +// GetOldUpdatesPath returns a former location for update files used for migration scripts only. +func (l *Locations) GetOldUpdatesPath() string { + return filepath.Join(l.userCache, "updates") +} + func (l *Locations) getSettingsPath() string { return l.userConfig } @@ -158,11 +173,25 @@ func (l *Locations) getLogsPath() string { } func (l *Locations) getCachePath() string { - return filepath.Join(l.userCache, "cache") + // Bridge cache is not a typical cache which can be deleted with only + // downside that the app has to download everything again. + // Cache for bridge is database with IMAP UIDs and UIDVALIDITY, and also + // other IMAP setup. Deleting such data leads to either re-sync of client, + // or mix of headers and bodies. Both is caused because of need of re-sync + // between Bridge and API which will happen in different order than before. + // In the first case, UIDVALIDITY is also changed and causes the better + // outcome to "just" re-sync everything; in the later, UIDVALIDITY stays + // the same, causing the client to not re-sync but UIDs in the client does + // not match UIDs in Bridge. + // Because users might use tools to regularly clear caches, Bridge cache + // cannot be located in a standard cache folder. + return filepath.Join(l.userConfig, "cache") } func (l *Locations) getUpdatesPath() string { - return filepath.Join(l.userCache, "updates") + // Users might use tools to regularly clear caches, which would mean always + // removing updates, therefore Bridge updates have to be somewhere else. + return filepath.Join(l.userConfig, "updates") } // Clear removes everything except the lock and update files. diff --git a/internal/locations/locations_test.go b/internal/locations/locations_test.go index 1340794a..7aaa6947 100644 --- a/internal/locations/locations_test.go +++ b/internal/locations/locations_test.go @@ -45,7 +45,8 @@ func TestClearRemovesEverythingExceptLockAndUpdateFiles(t *testing.T) { assert.NoError(t, l.Clear()) assert.FileExists(t, l.GetLockFile()) - assert.NoDirExists(t, l.getSettingsPath()) + assert.DirExists(t, l.getSettingsPath()) + assert.NoFileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json")) assert.NoDirExists(t, l.getLogsPath()) assert.NoDirExists(t, l.getCachePath()) assert.DirExists(t, l.getUpdatesPath()) @@ -58,6 +59,7 @@ func TestClearUpdateFiles(t *testing.T) { assert.FileExists(t, l.GetLockFile()) assert.DirExists(t, l.getSettingsPath()) + assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json")) assert.DirExists(t, l.getLogsPath()) assert.DirExists(t, l.getCachePath()) assert.NoDirExists(t, l.getUpdatesPath()) @@ -75,6 +77,7 @@ func TestCleanLeavesStandardLocationsUntouched(t *testing.T) { assert.FileExists(t, l.GetLockFile()) assert.DirExists(t, l.getSettingsPath()) + assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json")) assert.DirExists(t, l.getLogsPath()) assert.FileExists(t, filepath.Join(l.getLogsPath(), "log1.txt")) assert.FileExists(t, filepath.Join(l.getLogsPath(), "log2.txt")) @@ -138,6 +141,9 @@ func newTestLocations(t *testing.T) *Locations { require.NoError(t, err) require.DirExists(t, settings) + createFilesInDir(t, settings, "prefs.json") + require.FileExists(t, filepath.Join(settings, "prefs.json")) + logs, err := l.ProvideLogsPath() require.NoError(t, err) require.DirExists(t, logs) diff --git a/internal/store/event_loop.go b/internal/store/event_loop.go index bdecfd91..d23327d0 100644 --- a/internal/store/event_loop.go +++ b/internal/store/event_loop.go @@ -268,8 +268,6 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun return false, errors.New("received empty event") } - l = l.WithField("newEventID", event.EventID) - if err = loop.processEvent(event); err != nil { return false, errors.Wrap(err, "failed to process event") } diff --git a/internal/store/mailbox_message.go b/internal/store/mailbox_message.go index 3913b740..1a8241e5 100644 --- a/internal/store/mailbox_message.go +++ b/internal/store/mailbox_message.go @@ -67,10 +67,14 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe } res, err := storeMailbox.client().Import([]*pmapi.ImportMsgReq{importReqs}) - if err == nil && len(res) > 0 { - msg.ID = res[0].MessageID + if err != nil { + return err } - return err + if len(res) == 0 { + return errors.New("no import response") + } + msg.ID = res[0].MessageID + return res[0].Error } // LabelMessages adds the label by calling an API. diff --git a/pkg/message/body.go b/pkg/message/body.go index 07f059dd..952cc466 100644 --- a/pkg/message/body.go +++ b/pkg/message/body.go @@ -57,19 +57,23 @@ func WriteAttachmentBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message, att att.Name += ".gpg" att.MIMEType = "application/pgp-encrypted" //nolint } else if err != nil && err != openpgperrors.ErrSignatureExpired { - err = fmt.Errorf("cannot decrypt attachment: %v", err) - return + return fmt.Errorf("cannot decrypt attachment: %v", err) + } + + // Don't encode message/rfc822 attachments; they should be embedded and preserved. + if att.MIMEType == rfc822Message { + if n, err := io.Copy(w, dr); err != nil { + return fmt.Errorf("cannot write attached message: %v (wrote %v bytes)", err, n) + } + return nil } // Encode it. ww := textwrapper.NewRFC822(w) bw := base64.NewEncoder(base64.StdEncoding, ww) - var n int64 - if n, err = io.Copy(bw, dr); err != nil { - err = fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n) + if n, err := io.Copy(bw, dr); err != nil { + return fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n) } - - _ = bw.Close() - return + return bw.Close() } diff --git a/pkg/message/build.go b/pkg/message/build.go index 3b9dd2c8..eca16b07 100644 --- a/pkg/message/build.go +++ b/pkg/message/build.go @@ -124,7 +124,7 @@ func (bld *Builder) writeRelatedPart(p io.Writer, inlines []*pmapi.Attachment) e return err } - h := GetAttachmentHeader(inline) + h := GetAttachmentHeader(inline, false) if p, err = related.CreatePart(h); err != nil { return err } @@ -194,7 +194,7 @@ func (bld *Builder) BuildMessage() (structure *BodyStructure, message []byte, er return nil, nil, err } - attachmentHeader := GetAttachmentHeader(att) + attachmentHeader := GetAttachmentHeader(att, false) if partWriter, err = mw.CreatePart(attachmentHeader); err != nil { return nil, nil, err } @@ -311,16 +311,11 @@ func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ( for i := 0; i < len(m.Attachments); i++ { att := m.Attachments[i] r := readers[i] - h := GetAttachmentHeader(att) + h := GetAttachmentHeader(att, false) p, err := mw.CreatePart(h) if err != nil { return nil, err } - // Create line wrapper writer. - ww := textwrapper.NewRFC822(p) - - // Create base64 writer. - bw := base64.NewEncoder(base64.StdEncoding, ww) data, err := ioutil.ReadAll(r) if err != nil { @@ -332,6 +327,9 @@ func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ( if err != nil { return nil, err } + + ww := textwrapper.NewRFC822(p) + bw := base64.NewEncoder(base64.StdEncoding, ww) if _, err := bw.Write(pgpMessage.GetBinary()); err != nil { return nil, err } diff --git a/pkg/message/header.go b/pkg/message/header.go index e48f4065..36ecff70 100644 --- a/pkg/message/header.go +++ b/pkg/message/header.go @@ -107,12 +107,17 @@ func GetRelatedHeader(m *pmapi.Message) textproto.MIMEHeader { return h } -func GetAttachmentHeader(att *pmapi.Attachment) textproto.MIMEHeader { +func GetAttachmentHeader(att *pmapi.Attachment, buildForIMAP bool) textproto.MIMEHeader { mediaType := att.MIMEType if mediaType == "application/pgp-encrypted" { mediaType = "application/octet-stream" } + transferEncoding := "base64" + if mediaType == rfc822Message && buildForIMAP { + transferEncoding = "8bit" + } + encodedName := pmmime.EncodeHeader(att.Name) disposition := "attachment" //nolint[goconst] if strings.Contains(att.Header.Get("Content-Disposition"), "inline") { @@ -121,7 +126,9 @@ func GetAttachmentHeader(att *pmapi.Attachment) textproto.MIMEHeader { h := make(textproto.MIMEHeader) h.Set("Content-Type", mime.FormatMediaType(mediaType, map[string]string{"name": encodedName})) - h.Set("Content-Transfer-Encoding", "base64") + if transferEncoding != "" { + h.Set("Content-Transfer-Encoding", transferEncoding) + } h.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{"filename": encodedName})) // Forward some original header lines. diff --git a/pkg/message/message.go b/pkg/message/message.go index d4904c58..f8dc8963 100644 --- a/pkg/message/message.go +++ b/pkg/message/message.go @@ -26,6 +26,10 @@ import ( "github.com/sirupsen/logrus" ) +const ( + rfc822Message = "message/rfc822" +) + var log = logrus.WithField("pkg", "pkg/message") //nolint[gochecknoglobals] func GetBoundary(m *pmapi.Message) string { diff --git a/pkg/message/section.go b/pkg/message/section.go index 5adae7b0..0396c7ff 100644 --- a/pkg/message/section.go +++ b/pkg/message/section.go @@ -201,7 +201,7 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s mediaType, params, _ := pmmime.ParseMediaType(info.Header.Get("Content-Type")) // If multipart, call getAllParts, else read to count lines. - if (strings.HasPrefix(mediaType, "multipart/") || mediaType == "message/rfc822") && params["boundary"] != "" { + if (strings.HasPrefix(mediaType, "multipart/") || mediaType == rfc822Message) && params["boundary"] != "" { newPath := append(currentPath, 1) var br *boundaryReader diff --git a/release-notes/bridge_early.md b/release-notes/bridge_early.md index db7430ff..b6f063dc 100644 --- a/release-notes/bridge_early.md +++ b/release-notes/bridge_early.md @@ -1,3 +1,13 @@ +## v1.6.6 +- 2021-02-26 + +### Fixed + +- Fixed update notifications +- Fixed GUI freeze while switching to early update channel +- Fixed Bridge autostart +- Improved signing of update packages + ## v1.6.5 - 2021-02-22 diff --git a/release-notes/bridge_stable.md b/release-notes/bridge_stable.md index 4adc7a6c..00da7a78 100644 --- a/release-notes/bridge_stable.md +++ b/release-notes/bridge_stable.md @@ -1,3 +1,24 @@ +## v1.6.6 +- 2021-03-04 + +### New + +- Allow to choose which keychain is used by Bridge on Linux +- Added automatic update CLI commands +- Improved performance during slow connection +- Added IMAP requests to the logs for easier debugging + +### Fixed + +- Fixed update notifications +- Fixed GUI freeze while switching to early update channel +- Fixed Bridge autostart +- Improved signing of update packages +- NoGUI bulid +- Background of GUI welcome message +- Incorrect total mailbox size displayed in Apple Mail + + ## v1.6.3 - 2021-02-16 diff --git a/test/features/bridge/imap/message/import.feature b/test/features/bridge/imap/message/import.feature index 48ef6f74..05e4752a 100644 --- a/test/features/bridge/imap/message/import.feature +++ b/test/features/bridge/imap/message/import.feature @@ -117,3 +117,36 @@ Feature: IMAP import messages Then IMAP response is "OK" And API mailbox "INBOX" for "user" has 0 message And API mailbox "Sent" for "user" has 1 message + + Scenario: Import embedded message + When IMAP client imports message to "INBOX" + """ + From: Foo + To: Bridge Test + Subject: Embedded message + Content-Type: multipart/mixed; boundary="boundary" + Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000 + + This is a multi-part message in MIME format. + --boundary + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 7bit + + + --boundary + Content-Type: message/rfc822; name="embedded.eml" + Content-Transfer-Encoding: 7bit + Content-Disposition: attachment; filename="embedded.eml" + + From: Bar + To: Bridge Test + Subject: (No Subject) + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: quoted-printable + + hello + + --boundary-- + + """ + Then IMAP response is "OK" diff --git a/test/features/bridge/smtp/send/embedded_message.feature b/test/features/bridge/smtp/send/embedded_message.feature new file mode 100644 index 00000000..843e5124 --- /dev/null +++ b/test/features/bridge/smtp/send/embedded_message.feature @@ -0,0 +1,38 @@ +Feature: SMTP sending embedded message + Scenario: Send it + Given there is connected user "user" + And there is SMTP client logged in as "user" + When SMTP client sends message + """ + From: Bridge Test <[userAddress]> + To: Internal Bridge + Subject: Embedded message + Content-Type: multipart/mixed; boundary="boundary" + + This is a multi-part message in MIME format. + --boundary + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 7bit + + + --boundary + Content-Type: message/rfc822; name="embedded.eml" + Content-Transfer-Encoding: 7bit + Content-Disposition: attachment; filename="embedded.eml" + + From: Bar + To: Bridge Test + Subject: (No Subject) + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: quoted-printable + + hello + + --boundary-- + + + """ + Then SMTP response is "OK" + And mailbox "Sent" for "user" has messages + | from | to | subject | + | [userAddress] | bridgetest@protonmail.com | Embedded message | diff --git a/test/features/ie/transfer/import_embedded.feature b/test/features/ie/transfer/import_embedded.feature new file mode 100644 index 00000000..59c53f09 --- /dev/null +++ b/test/features/ie/transfer/import_embedded.feature @@ -0,0 +1,43 @@ +Feature: Import embedded message + Background: + Given there is connected user "user" + And there is EML file "Inbox/hello.eml" + """ + From: Foo + To: Bridge Test + Subject: Embedded message + Content-Type: multipart/mixed; boundary="boundary" + Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000 + + This is a multi-part message in MIME format. + --boundary + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 7bit + + + --boundary + Content-Type: message/rfc822; name="embedded.eml" + Content-Transfer-Encoding: 7bit + Content-Disposition: attachment; filename="embedded.eml" + + From: Bar + To: Bridge Test + Subject: (No Subject) + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: quoted-printable + + hello + + --boundary-- + + """ + + Scenario: Import it + When user "user" imports local files + Then progress result is "OK" + And transfer exported 1 messages + And transfer imported 1 messages + And transfer failed for 0 messages + And API mailbox "INBOX" for "user" has messages + | from | to | subject | + | foo@example.com | bridgetest@pm.test | Embedded message | diff --git a/test/transfer_actions_test.go b/test/transfer_actions_test.go index 60b7a6a1..194d0c90 100644 --- a/test/transfer_actions_test.go +++ b/test/transfer_actions_test.go @@ -60,9 +60,9 @@ func userImportsLocalFilesToAddress(bddUserID, bddAddressID string) error { } func userImportsLocalFilesToAddressWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error { - return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) { + return doTransfer(bddUserID, bddAddressID, rules, func(username, address string) (*transfer.Transfer, error) { path := ctx.GetTransferLocalRootForImport() - return ctx.GetImportExport().GetLocalImporter(address, path) + return ctx.GetImportExport().GetLocalImporter(username, address, path) }) } @@ -81,9 +81,9 @@ func userImportsRemoteMessagesToAddress(bddUserID, bddAddressID string) error { } func userImportsRemoteMessagesToAddressWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error { - return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) { + return doTransfer(bddUserID, bddAddressID, rules, func(username, address string) (*transfer.Transfer, error) { imapServer := ctx.GetTransferRemoteIMAPServer() - return ctx.GetImportExport().GetRemoteImporter(address, imapServer.Username, imapServer.Password, imapServer.Host, imapServer.Port) + return ctx.GetImportExport().GetRemoteImporter(username, address, imapServer.Username, imapServer.Password, imapServer.Host, imapServer.Port) }) } @@ -102,9 +102,9 @@ func userExportsAddressToEMLFiles(bddUserID, bddAddressID string) error { } func userExportsAddressToEMLFilesWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error { - return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) { + return doTransfer(bddUserID, bddAddressID, rules, func(username, address string) (*transfer.Transfer, error) { path := ctx.GetTransferLocalRootForExport() - return ctx.GetImportExport().GetEMLExporter(address, path) + return ctx.GetImportExport().GetEMLExporter(username, address, path) }) } @@ -123,20 +123,20 @@ func userExportsAddressToMBOXFiles(bddUserID, bddAddressID string) error { } func userExportsAddressToMBOXFilesWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error { - return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) { + return doTransfer(bddUserID, bddAddressID, rules, func(username, address string) (*transfer.Transfer, error) { path := ctx.GetTransferLocalRootForExport() - return ctx.GetImportExport().GetMBOXExporter(address, path) + return ctx.GetImportExport().GetMBOXExporter(username, address, path) }) } // Helpers. -func doTransfer(bddUserID, bddAddressID string, rules *gherkin.DataTable, getTransferrer func(string) (*transfer.Transfer, error)) error { +func doTransfer(bddUserID, bddAddressID string, rules *gherkin.DataTable, getTransferrer func(string, string) (*transfer.Transfer, error)) error { account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID) if account == nil { return godog.ErrPending } - transferrer, err := getTransferrer(account.Address()) + transferrer, err := getTransferrer(account.Username(), account.Address()) if err != nil { return internalError(err, "failed to init transfer") }