Compare commits

...

25 Commits

Author SHA1 Message Date
cccadaee42 Other: Bridge HZM 1.6.7 & Import-Export Farg 1.3.2 2021-03-24 15:11:46 +01:00
bbb365f8a5 Merge branch 'release/farg' into devel 2021-03-24 14:55:55 +01:00
1f18d9d917 GODT-1117 Do not change updates location for Bridge now 2021-03-24 10:45:55 +01:00
59e0d63485 GODT-1105 Fix: Dylib hijack vulnerability found by https://objective-see.com/products/dhs.html 2021-03-24 08:37:30 +00:00
72fe5a636e GODT-1063: Add metainfo to launcher
Refactor metainfo file a bit
2021-03-24 07:04:28 +00:00
45a83133ba Other: increase SMTP line limit to 2^16 2021-03-17 11:45:54 +01:00
215eb4d6eb GODT-1085 Ignore live test of importing to sent and custom label 2021-03-17 08:10:50 +01:00
479b951c50 GODT-1076 Fix UIDPLUS response for importing existing message 2021-03-16 11:55:36 +00:00
a94c8a943f GODT-1077 IMAP sync counting 2021-03-16 12:35:36 +01:00
ea306f405e Other: print address mode in info level 2021-03-12 09:02:54 +01:00
1b405506b8 Merge remote-tracking branch 'remotes/origin/release-notes' into farg 2021-03-11 00:01:21 +01:00
38c6132f81 Other: Import-Export Farg 1.3.1 2021-03-11 00:00:40 +01:00
b7351dfaf8 Other 2021-03-10 21:52:52 +00:00
7e8f6943f2 Other 2021-03-10 21:35:31 +00:00
a0132e8440 GODT-1047 No silent updates for Import-Export app 2021-03-10 18:56:55 +00:00
27541784aa Merge master into devel 2021-03-10 14:52:45 +01:00
9e567f08b2 Other: release notes for 1.6.6 stable 2021-03-04 11:56:11 +00:00
bf274f984e Other: include latest go.mod/go.sum changes 2021-03-04 11:25:33 +00:00
3b60bbe13b Other 2021-03-04 09:50:29 +00:00
a73a1b623a GODT-803 Fix import to wrong target address 2021-03-02 16:02:23 +01:00
c0a8877018 Other: include latest go.mod/go.sum changes 2021-03-01 17:48:22 +01:00
904166c01c GODT-247 Cache and update files moved from user's cache to config 2021-03-01 14:06:58 +00:00
4761bc935a GODT-948 Embedded messages 2021-03-01 09:22:08 +00:00
71301d891f Other 2021-02-28 20:55:23 +01:00
d47be3c4c0 GODT-1043 Fix showing long login error in GUI dialog 2021-02-26 12:21:12 +00:00
54 changed files with 804 additions and 247 deletions

View File

@ -2,6 +2,53 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.6.7] HZM
### Added
* GODT-1111 Add correct metadata to Windows executables.
* GODT-1112 Add application to Windows Firewall exclusion list on install.
* GODT-1077 Track how many times message is built to help understand re-syncs.
### Changed
* GODT-247 Revise all storage locations (cache, config, local etc).
### Fixed
* GODT-948 Parser does not handle embedding of Content-Type: message/rfc822.
* GODT-1079 Correct 9001 error handling on login.
### Security
* GODT-1105 Dylib Hijacking security fix.
## [IE 1.3.2] Farg
### Added
* GODT-1111 Add correct metadata to Windows executables.
* GODT-1112 Add application to Windows Firewall exclusion list on install.
### Changed
* GODT-247 Revise all storage locations (cache, config, local etc).
### Fixed
* GODT-1079 Correct 9001 error handling on login.
### Security
* GODT-1105 Dylib Hijacking security fix.
## [IE 1.3.1] Farg
### Changed
* GODT-1047 No silent updates for Import-Export app.
* GODT-247 Cache and update files moved from user's cache to config.
### Fixed
* Other: include latest go.mod/go.sum changes.
* GODT-803 Fix import to wrong target address.
* GODT-948 Embedded messages.
* GODT-1043 Fix showing long login error in GUI dialog.
## [Bridge 1.6.6] HZM ## [Bridge 1.6.6] HZM
### Added ### Added

View File

@ -10,8 +10,8 @@ TARGET_OS?=${GOOS}
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher .PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
# Keep version hardcoded so app build works also without Git repository. # Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.6.6+git BRIDGE_APP_VERSION?=1.6.7+git
IE_APP_VERSION?=1.3.0+git IE_APP_VERSION?=1.3.2+git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns SRC_ICNS:=Bridge.icns
@ -19,6 +19,7 @@ SRC_SVG:=logo.svg
TGT_ICNS:=Bridge.icns TGT_ICNS:=Bridge.icns
EXE_NAME:=proton-bridge EXE_NAME:=proton-bridge
CONFIGNAME:=bridge CONFIGNAME:=bridge
WINDRES_DEFINE:=BUILD_BRIDGE
ifeq "${TARGET_CMD}" "Import-Export" ifeq "${TARGET_CMD}" "Import-Export"
APP_VERSION:=${IE_APP_VERSION} APP_VERSION:=${IE_APP_VERSION}
SRC_ICO:=ie.ico SRC_ICO:=ie.ico
@ -27,6 +28,7 @@ ifeq "${TARGET_CMD}" "Import-Export"
TGT_ICNS:=ImportExport.icns TGT_ICNS:=ImportExport.icns
EXE_NAME:=proton-ie EXE_NAME:=proton-ie
CONFIGNAME:=importExport CONFIGNAME:=importExport
WINDRES_DEFINE:=BUILD_IE
endif endif
REVISION:=$(shell git rev-parse --short=10 HEAD) REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z) BUILD_TIME:=$(shell date +%FT%T%z)
@ -56,7 +58,7 @@ EXE_QT:=${DIRNAME}
ifeq "${TARGET_OS}" "windows" ifeq "${TARGET_OS}" "windows"
EXE:=${EXE}.exe EXE:=${EXE}.exe
EXE_QT:=${EXE_QT}.exe EXE_QT:=${EXE_QT}.exe
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso RESOURCE_FILE:=resource.syso
endif endif
ifeq "${TARGET_OS}" "darwin" ifeq "${TARGET_OS}" "darwin"
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
@ -89,8 +91,14 @@ build-nogui: gofiles
build-ie-nogui: build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui TARGET_CMD=Import-Export $(MAKE) build-nogui
build-launcher: ifeq "${GOOS}" "windows"
go build ${BUILD_FLAGS_LAUNCHER} -o launcher-${APP} cmd/launcher/main.go PRERESOURCECMD:=cp ./resource.syso ./cmd/launcher/resource.syso
POSTRESOURCECMD:=rm -f ./cmd/launcher/resource.syso
endif
build-launcher: ${RESOURCE_FILE}
${PRERESOURCECMD}
go build ${BUILD_FLAGS_LAUNCHER} -o launcher-${EXE} ./cmd/launcher/
${POSTRESOURCECMD}
build-launcher-ie: build-launcher-ie:
TARGET_CMD=Import-Export $(MAKE) build-launcher TARGET_CMD=Import-Export $(MAKE) build-launcher
@ -134,7 +142,7 @@ ifneq "${GOOS}" "${TARGET_OS}"
endif endif
endif endif
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET} ${EXE_TARGET}: check-has-go gofiles ${RESOURCE_FILE} ${VENDOR_TARGET}
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR} rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
cp cmd/${TARGET_CMD}/main.go . cp cmd/${TARGET_CMD}/main.go .
qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET} qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET}
@ -142,13 +150,12 @@ ${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET}
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
rm -rf ${TARGET_OS} main.go rm -rf ${TARGET_OS} main.go
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
cp $^ $@
icon.rc: ./internal/frontend/share/icon.rc
cp $^ .
icon_windows.syso: icon.rc logo.ico
windres --target=pe-x86-64 -o $@ $<
WINDRES_YEAR:=$(shell date +%Y)
APP_VERSION_COMMA:=$(shell echo "${APP_VERSION}" | sed -e 's/[^0-9,.]*//g' -e 's/\./,/g')
resource.syso: ./internal/frontend/share/info.rc ./internal/frontend/share/icons/${SRC_ICO} .FORCE
rm -f ./*.syso
windres --target=pe-x86-64 -I ./internal/frontend/share/icons/ -D ${WINDRES_DEFINE} -D ICO_FILE=${SRC_ICO} -D EXE_NAME="${EXE_NAME}" -D FILE_VERSION="${APP_VERSION}" -D ORIGINAL_FILE_NAME="${EXE}" -D PRODUCT_VERSION="${APP_VERSION}" -D FILE_VERSION_COMMA=${APP_VERSION_COMMA} -D YEAR=${WINDRES_YEAR} -o $@ $<
## Rules for therecipe/qt ## Rules for therecipe/qt
.PHONY: prepare-vendor update-vendor update-qt-docs .PHONY: prepare-vendor update-vendor update-qt-docs
@ -294,6 +301,7 @@ LOG?=debug
LOG_IMAP?=client # client/server/all, or empty to turn it off LOG_IMAP?=client # client/server/all, or empty to turn it off
LOG_SMTP?=--log-smtp # 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?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
RUN_FLAGS_IE?=-m -l=${LOG}
run: run-nogui-cli run: run-nogui-cli
@ -316,11 +324,11 @@ run-ie-qml-preview:
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview $(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview
run-ie: run-ie:
TARGET_CMD=Import-Export $(MAKE) run TARGET_CMD=Import-Export RUN_FLAGS="${RUN_FLAGS_IE}" $(MAKE) run
run-ie-qt: 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: 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: clean-frontend-qt:
$(MAKE) -C internal/frontend/qt -f Makefile.local clean $(MAKE) -C internal/frontend/qt -f Makefile.local clean
@ -337,7 +345,7 @@ clean: clean-vendor
rm -rf cmd/Desktop-Bridge/deploy rm -rf cmd/Desktop-Bridge/deploy
rm -rf cmd/Import-Export/deploy rm -rf cmd/Import-Export/deploy
rm -f build last.log mem.pprof main.go rm -f build last.log mem.pprof main.go
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso rm -f resource.syso
rm -f release-notes/bridge.html rm -f release-notes/bridge.html
rm -f release-notes/import-export.html rm -f release-notes/import-export.html
@ -345,3 +353,5 @@ clean: clean-vendor
generate: generate:
go generate ./... go generate ./...
$(MAKE) add-license $(MAKE) add-license
.FORCE:

3
go.mod
View File

@ -54,12 +54,13 @@ require (
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/urfave/cli/v2 v2.2.0 github.com/urfave/cli/v2 v2.2.0
github.com/vmihailenco/msgpack/v5 v5.1.3 github.com/vmihailenco/msgpack/v5 v5.1.3
go.etcd.io/bbolt v1.3.5 go.etcd.io/bbolt v1.3.5

8
go.sum
View File

@ -223,8 +223,6 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245 h1:gk/AF9SGRj+RafNCoDcS3RRscb8S4BVbvqODOgWA7/8=
github.com/pkg/math v0.0.0-20141027224758-f2ed9e40e245/go.mod h1:2dhPPj2Li3DXrSY2U2ADdZy2B7sjQsT57lqENx1+FSE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
@ -264,6 +262,12 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o=
github.com/therecipe/qt/internal/binding/files/docs v0.0.0-20191019224306-1097424d656c h1:/VhcwU7WuFEVgDHZ9V8PIYAyYqQ6KNxFUjBMOf2aFZM=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=

View File

@ -136,6 +136,7 @@ func New( // nolint[funlen]
if err := logging.Init(logsPath); err != nil { if err := logging.Init(logsPath); err != nil {
return nil, err return nil, err
} }
logging.SetLevel("debug") // Proper level is set later in run.
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath)) crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := migrateFiles(configName); err != nil { if err := migrateFiles(configName); err != nil {

View File

@ -30,9 +30,11 @@ import (
// We can remove this eventually. // We can remove this eventually.
// //
// | entity | old location | new location | // | entity | old location | new location |
// |--------|-------------------------------------------|----------------------------------------| // |-----------|-------------------------------------------|----------------------------------------|
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json | // | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
// | c11 | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 | // | c11 1.5.x | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
// | c11 1.6.x | ~/.cache/protonmail/<app>/cache/c11 | ~/.config/protonmail/<app>/cache/c11 |
// | updates | ~/.cache/protonmail/<app>/updates | ~/.config/protonmail/<app>/updates |
func migrateFiles(configName string) error { func migrateFiles(configName string) error {
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName)) locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
if err != nil { if err != nil {
@ -41,43 +43,89 @@ func migrateFiles(configName string) error {
locations := locations.New(locationsProvider, configName) locations := locations.New(locationsProvider, configName)
userCacheDir := locationsProvider.UserCache() userCacheDir := locationsProvider.UserCache()
if err := migratePrefsFrom15x(locations, userCacheDir); err != nil {
return err
}
if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil {
return err
}
if err := migrateUpdatesFrom16x(configName, locations); err != nil {
return err
}
return nil
}
func migratePrefsFrom15x(locations *locations.Locations, userCacheDir string) error {
newSettingsDir, err := locations.ProvideSettingsPath() newSettingsDir, err := locations.ProvideSettingsPath()
if err != nil { if err != nil {
return err return err
} }
if err := moveIfExists( return moveIfExists(
filepath.Join(userCacheDir, "c11", "prefs.json"), filepath.Join(userCacheDir, "c11", "prefs.json"),
filepath.Join(newSettingsDir, "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 { if err != nil {
return err return err
} }
// Migration for versions before 1.6.x.
if err := moveIfExists( if err := moveIfExists(
filepath.Join(userCacheDir, "c11"), filepath.Join(olderCacheDir, "c11"),
filepath.Join(newCacheDir, "c11"), filepath.Join(latestCacheDir, "c11"),
); err != nil { ); err != nil {
return err return err
} }
// Migration for versions 1.6.x.
return moveIfExists(
filepath.Join(newerCacheDir, "c11"),
filepath.Join(latestCacheDir, "c11"),
)
}
func migrateUpdatesFrom16x(configName string, locations *locations.Locations) error {
// In order to properly update Bridge 1.6.X and higher we need to
// change the launcher first. Since this is not part of automatic
// updates the migration must wait until manual update. Until that
// we need to keep old path.
if configName == "bridge" {
return nil return nil
}
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 { func moveIfExists(source, destination string) error {
l := logrus.WithField("source", source).WithField("destination", destination)
if _, err := os.Stat(source); os.IsNotExist(err) { 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 return nil
} }
if _, err := os.Stat(destination); !os.IsNotExist(err) { 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 return nil
} }
l.Info("Migrating files")
return os.Rename(source, destination) return os.Rename(source, destination)
} }

View File

@ -28,8 +28,6 @@ import (
"github.com/ProtonMail/proton-bridge/internal/frontend" "github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/importexport" "github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -88,7 +86,7 @@ func run(b *base.Base, c *cli.Context) error {
return f.Loop() return f.Loop()
} }
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { //nolint[unparam]
version, err := u.Check() version, err := u.Check()
if err != nil { if err != nil {
logrus.WithError(err).Error("An error occurred while checking for updates") logrus.WithError(err).Error("An error occurred while checking for updates")
@ -107,27 +105,5 @@ func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool)
logrus.WithField("version", version.Version).Info("An update is available") logrus.WithField("version", version.Version).Info("An update is available")
if !autoUpdate {
f.NotifyManualUpdate(version, u.CanInstall(version)) f.NotifyManualUpdate(version, u.CanInstall(version))
return
}
if !u.CanInstall(version) {
logrus.Info("A manual update is required")
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
return
}
if err := u.InstallUpdate(version); err != nil {
if errors.Cause(err) == updater.ErrDownloadVerify {
logrus.WithError(err).Warning("Skipping update installation due to temporary error")
} else {
logrus.WithError(err).Error("The update couldn't be installed")
f.NotifySilentUpdateError(err)
}
return
}
f.NotifySilentUpdateInstalled()
} }

View File

@ -38,7 +38,7 @@ func (f *frontendCLI) importLocalMessages(c *ishell.Context) {
return 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) f.transfer(t, err, false, true)
} }
@ -68,7 +68,7 @@ func (f *frontendCLI) importRemoteMessages(c *ishell.Context) {
return 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) f.transfer(t, err, false, true)
} }
@ -81,7 +81,7 @@ func (f *frontendCLI) exportMessagesToEML(c *ishell.Context) {
return 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) f.transfer(t, err, true, false)
} }
@ -94,7 +94,7 @@ func (f *frontendCLI) exportMessagesToMBOX(c *ishell.Context) {
return 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) f.transfer(t, err, true, false)
} }

View File

@ -236,13 +236,13 @@ Item {
} }
} }
onNotifySilentUpdateRestartNeeded: { //onNotifySilentUpdateRestartNeeded: {
go.updateState = "updateRestart" // go.updateState = "updateRestart"
} //}
//
onNotifySilentUpdateError: { //onNotifySilentUpdateError: {
go.updateState = "updateError" // go.updateState = "updateError"
} //}
onNotifyLogout : { onNotifyLogout : {
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Import-Export app with this account.").arg(accname) ) go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Import-Export app with this account.").arg(accname) )

View File

@ -165,6 +165,7 @@ Column {
textColor : Style.main.textBlue textColor : Style.main.textBlue
onClicked: { onClicked: {
dialogExport.currentIndex = 0 dialogExport.currentIndex = 0
dialogExport.account = account
dialogExport.address = account dialogExport.address = account
dialogExport.show() dialogExport.show()
} }
@ -321,6 +322,7 @@ Column {
textBold: true textBold: true
textColor: Style.main.textBlue textColor: Style.main.textBlue
onClicked: { onClicked: {
dialogExport.account = account
dialogExport.address = listalias[index] dialogExport.address = listalias[index]
dialogExport.show() dialogExport.show()
} }
@ -339,6 +341,7 @@ Column {
textBold: true textBold: true
textColor: enabled ? Style.main.textBlue : Style.main.textDisabled textColor: enabled ? Style.main.textBlue : Style.main.textDisabled
onClicked: { onClicked: {
dialogImport.account = account
dialogImport.address = listalias[index] dialogImport.address = listalias[index]
dialogImport.show() dialogImport.show()
} }

View File

@ -35,6 +35,7 @@ Dialog {
title : set_title() title : set_title()
property string account
property string address property string address
property alias finish: finish property alias finish: finish
@ -428,7 +429,7 @@ Dialog {
onTriggered : { onTriggered : {
switch (currentIndex) { switch (currentIndex) {
case 0: case 0:
go.loadStructureForExport(root.address) go.loadStructureForExport(root.account, root.address)
sourceFoldersInput.hasItems = (transferRules.rowCount() > 0) sourceFoldersInput.hasItems = (transferRules.rowCount() > 0)
break break
case 2: case 2:

View File

@ -34,6 +34,7 @@ Dialog {
isDialogBusy: currentIndex==3 || currentIndex==4 isDialogBusy: currentIndex==3 || currentIndex==4
property string account
property string address property string address
property string inputPath : "" property string inputPath : ""
property bool isFromFile : inputEmail.text == "" && root.inputPath != "" property bool isFromFile : inputEmail.text == "" && root.inputPath != ""
@ -1032,6 +1033,7 @@ Dialog {
root.isFromIMAP, root.isFromIMAP,
root.inputPath, root.inputPath,
inputEmail.text, inputPassword.text, inputServer.text, inputPort.text, inputEmail.text, inputPassword.text, inputServer.text, inputPort.text,
root.account,
root.address root.address
) )
break break

View File

@ -96,6 +96,8 @@ Item {
onClicked: bugreportWin.show() onClicked: bugreportWin.show()
} }
/*
ButtonIconText { ButtonIconText {
id: autoUpdates id: autoUpdates
text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates") text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates")
@ -115,8 +117,6 @@ Item {
} }
} }
/*
ButtonIconText { ButtonIconText {
id: cacheClear id: cacheClear
text: qsTr("Clear Cache") text: qsTr("Clear Cache")

View File

@ -18,6 +18,7 @@
// Dialog with adding new user // Dialog with adding new user
import QtQuick 2.8 import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import ProtonUI 1.0 import ProtonUI 1.0
@ -83,6 +84,9 @@ StackLayout {
text : "" text : ""
color: Style.main.textBlue color: Style.main.textBlue
visible: false visible: false
width: root.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
} }
// prevent any action below // prevent any action below

View File

@ -70,7 +70,8 @@ Dialog {
id: topSep id: topSep
color : "transparent" color : "transparent"
width : Style.main.dummy 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 { InputField {

View File

@ -107,7 +107,7 @@ Dialog {
text: qsTr("Automatically update in the future", "Checkbox label for using autoupdates later on") text: qsTr("Automatically update in the future", "Checkbox label for using autoupdates later on")
checked: go.isAutoUpdate checked: go.isAutoUpdate
onToggled: go.toggleAutoUpdate() onToggled: go.toggleAutoUpdate()
visible: !root.forceUpdate visible: !root.forceUpdate && (go.isAutoUpdate != undefined)
} }
Row { Row {

View File

@ -23,13 +23,13 @@ import QtQuick.Window 2.2
Window { Window {
id : testroot id : testroot
width : 100 width : 150
height : 600 height : 600
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
visible : true visible : true
title : "GUI test Window" title : "GUI test Window"
color : "transparent" color : "transparent"
x : testgui.winMain.x - 120 x : testgui.winMain.x - 170
y : testgui.winMain.y y : testgui.winMain.y
property bool newVersion : true property bool newVersion : true
@ -110,8 +110,8 @@ Window {
ListElement { title: "NotifyManualUpdateRestart" } ListElement { title: "NotifyManualUpdateRestart" }
ListElement { title: "NotifyManualUpdateError" } ListElement { title: "NotifyManualUpdateError" }
ListElement { title: "ForceUpdate" } ListElement { title: "ForceUpdate" }
ListElement { title: "NotifySilentUpdateRestartNeeded" } //ListElement { title: "NotifySilentUpdateRestartNeeded" }
ListElement { title: "NotifySilentUpdateError" } //ListElement { title: "NotifySilentUpdateError" }
ListElement { title : "ImportStructure" } ListElement { title : "ImportStructure" }
ListElement { title : "DraftImpFailed" } ListElement { title : "DraftImpFailed" }
ListElement { title : "NoInterImp" } ListElement { title : "NoInterImp" }
@ -183,12 +183,12 @@ Window {
case "ForceUpdate" : case "ForceUpdate" :
go.notifyForceUpdate() go.notifyForceUpdate()
break; break;
case "NotifySilentUpdateRestartNeeded" : //case "NotifySilentUpdateRestartNeeded" :
go.notifySilentUpdateRestartNeeded() //go.notifySilentUpdateRestartNeeded()
break; //break;
case "NotifySilentUpdateError" : //case "NotifySilentUpdateError" :
go.notifySilentUpdateError() //go.notifySilentUpdateError()
break; //break;
case "ImportStructure" : case "ImportStructure" :
testgui.winMain.dialogImport.address = "cuto@pm.com" testgui.winMain.dialogImport.address = "cuto@pm.com"
testgui.winMain.dialogImport.show() testgui.winMain.dialogImport.show()
@ -836,7 +836,7 @@ Window {
id: go id: go
property int isAutoStart : 1 property int isAutoStart : 1
property bool isAutoUpdate : false //property bool isAutoUpdate : false
property bool isFirstStart : false property bool isFirstStart : false
property string currentAddress : "none" property string currentAddress : "none"
//property string goos : "windows" //property string goos : "windows"
@ -858,15 +858,15 @@ Window {
property string updateState property string updateState
property string updateVersion : "q0.1.0" property string updateVersion : "q0.1.0"
property bool updateCanInstall: true property bool updateCanInstall: false
property string updateLandingPage : "https://protonmail.com/import-export/download/" property string updateLandingPage : "https://protonmail.com/import-export/download/"
property string updateReleaseNotesLink : "https://protonmail.com/download/ie/release_notes.html" property string updateReleaseNotesLink : "https://protonmail.com/download/ie/release_notes.html"
signal notifyManualUpdate() signal notifyManualUpdate()
signal notifyManualUpdateRestartNeeded() signal notifyManualUpdateRestartNeeded()
signal notifyManualUpdateError() signal notifyManualUpdateError()
signal notifyForceUpdate() signal notifyForceUpdate()
signal notifySilentUpdateRestartNeeded() //signal notifySilentUpdateRestartNeeded()
signal notifySilentUpdateError() //signal notifySilentUpdateError()
function checkForUpdates() { function checkForUpdates() {
console.log("checkForUpdates") console.log("checkForUpdates")
go.notifyVersionIsTheLatest() go.notifyVersionIsTheLatest()
@ -1355,10 +1355,10 @@ Window {
return !fname.includes("fail") return !fname.includes("fail")
} }
onToggleAutoUpdate: { //onToggleAutoUpdate: {
workAndClose() // workAndClose()
isAutoUpdate = (isAutoUpdate!=false) ? false : true // isAutoUpdate = (isAutoUpdate!=false) ? false : true
console.log (" Test: onToggleAutoUpdate "+isAutoUpdate) // console.log (" Test: onToggleAutoUpdate "+isAutoUpdate)
} //}
} }
} }

View File

@ -29,7 +29,7 @@ const (
TypeMBOX = "MBOX" TypeMBOX = "MBOX"
) )
func (f *FrontendQt) LoadStructureForExport(addressOrID string) { func (f *FrontendQt) LoadStructureForExport(username, addressOrID string) {
errCode := errUnknownError errCode := errUnknownError
var err error var err error
defer func() { 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. // The only error can be problem to load PM user and address.
errCode = errPMLoadFailed errCode = errPMLoadFailed
return return

View File

@ -135,11 +135,11 @@ func (f *FrontendQt) SetVersion(version updater.VersionInfo) {
} }
func (f *FrontendQt) NotifySilentUpdateInstalled() { func (f *FrontendQt) NotifySilentUpdateInstalled() {
f.Qml.NotifySilentUpdateRestartNeeded() //f.Qml.NotifySilentUpdateRestartNeeded()
} }
func (f *FrontendQt) NotifySilentUpdateError(err error) { func (f *FrontendQt) NotifySilentUpdateError(err error) {
f.Qml.NotifySilentUpdateError() //f.Qml.NotifySilentUpdateError()
} }
func (f *FrontendQt) watchEvents() { func (f *FrontendQt) watchEvents() {
@ -245,11 +245,11 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
f.Qml.SetCredits(importexport.Credits) f.Qml.SetCredits(importexport.Credits)
f.Qml.SetFullversion(f.buildVersion) f.Qml.SetFullversion(f.buildVersion)
if f.settings.GetBool(settings.AutoUpdateKey) { //if f.settings.GetBool(settings.AutoUpdateKey) {
f.Qml.SetIsAutoUpdate(true) // f.Qml.SetIsAutoUpdate(true)
} else { //} else {
f.Qml.SetIsAutoUpdate(false) // f.Qml.SetIsAutoUpdate(false)
} //}
go func() { go func() {
defer f.panicHandler.HandlePanic() defer f.panicHandler.HandlePanic()
@ -339,17 +339,17 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool {
return true return true
} }
func (f *FrontendQt) toggleAutoUpdate() { //func (f *FrontendQt) toggleAutoUpdate() {
defer f.Qml.ProcessFinished() // defer f.Qml.ProcessFinished()
//
if f.settings.GetBool(settings.AutoUpdateKey) { // if f.settings.GetBool(settings.AutoUpdateKey) {
f.settings.SetBool(settings.AutoUpdateKey, false) // f.settings.SetBool(settings.AutoUpdateKey, false)
f.Qml.SetIsAutoUpdate(false) // f.Qml.SetIsAutoUpdate(false)
} else { // } else {
f.settings.SetBool(settings.AutoUpdateKey, true) // f.settings.SetBool(settings.AutoUpdateKey, true)
f.Qml.SetIsAutoUpdate(true) // f.Qml.SetIsAutoUpdate(true)
} // }
} //}
// checkInternet is almost idetical to bridge // checkInternet is almost idetical to bridge
func (f *FrontendQt) checkInternet() { func (f *FrontendQt) checkInternet() {

View File

@ -26,7 +26,7 @@ import (
) )
// wrapper for QML // 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 errCode := errUnknownError
var err error var err error
defer func() { defer func() {
@ -39,7 +39,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm
}() }()
if isFromIMAP { 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 { if err != nil {
switch { switch {
case errors.Is(err, &transfer.ErrIMAPConnection{}): case errors.Is(err, &transfer.ErrIMAPConnection{}):
@ -54,7 +54,7 @@ func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEm
return return
} }
} else { } else {
f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath) f.transfer, err = f.ie.GetLocalImporter(targetUsername, targetAddress, sourcePath)
if err != nil { if err != nil {
// The only error can be problem to load PM user and address. // The only error can be problem to load PM user and address.
errCode = errPMLoadFailed errCode = errPMLoadFailed

View File

@ -33,7 +33,7 @@ type GoQMLInterface struct {
_ func() `constructor:"init"` _ func() `constructor:"init"`
_ bool `property:"isAutoUpdate"` //_ bool `property:"isAutoUpdate"`
_ string `property:"currentAddress"` _ string `property:"currentAddress"`
_ string `property:"goos"` _ string `property:"goos"`
_ string `property:"credits"` _ string `property:"credits"`
@ -62,8 +62,8 @@ type GoQMLInterface struct {
_ func() `signal:"notifyManualUpdateRestartNeeded"` _ func() `signal:"notifyManualUpdateRestartNeeded"`
_ func() `signal:"notifyManualUpdateError"` _ func() `signal:"notifyManualUpdateError"`
_ func() `signal:"notifyForceUpdate"` _ func() `signal:"notifyForceUpdate"`
_ func() `signal:"notifySilentUpdateRestartNeeded"` //_ func() `signal:"notifySilentUpdateRestartNeeded"`
_ func() `signal:"notifySilentUpdateError"` //_ func() `signal:"notifySilentUpdateError"`
_ func() `slot:"checkForUpdates"` _ func() `slot:"checkForUpdates"`
_ func() `slot:"checkAndOpenReleaseNotes"` _ func() `slot:"checkAndOpenReleaseNotes"`
_ func() `signal:"openReleaseNotesExternally"` _ func() `signal:"openReleaseNotesExternally"`
@ -93,7 +93,7 @@ type GoQMLInterface struct {
_ func() `signal:"showWindow"` _ func() `signal:"showWindow"`
_ func() `slot:"toggleAutoUpdate"` //_ func() `slot:"toggleAutoUpdate"`
_ func() `slot:"quit"` _ func() `slot:"quit"`
_ func() `slot:"loadAccounts"` _ func() `slot:"loadAccounts"`
_ func() `slot:"openLogs"` _ func() `slot:"openLogs"`
@ -108,14 +108,14 @@ type GoQMLInterface struct {
_ func(description, client, address string) bool `slot:"sendBug"` _ func(description, client, address string) bool `slot:"sendBug"`
_ func(address string) bool `slot:"sendImportReport"` _ func(address string) bool `slot:"sendImportReport"`
_ func(address string) `slot:"loadStructureForExport"` _ func(username, address string) `slot:"loadStructureForExport"`
_ func() string `slot:"leastUsedColor"` _ func() string `slot:"leastUsedColor"`
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"` _ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
_ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"` _ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"`
_ func(email string, importEncrypted bool) `slot:"startImport"` _ func(email string, importEncrypted bool) `slot:"startImport"`
_ func() `slot:"resetSource"` _ 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"` _ string `property:"progressInit"`
@ -162,7 +162,7 @@ func (s *GoQMLInterface) init() {}
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) { func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectQuit(f.App.Quit) s.ConnectQuit(f.App.Quit)
s.ConnectToggleAutoUpdate(f.toggleAutoUpdate) //s.ConnectToggleAutoUpdate(f.toggleAutoUpdate)
s.ConnectLoadAccounts(f.Accounts.LoadAccounts) s.ConnectLoadAccounts(f.Accounts.LoadAccounts)
s.ConnectOpenLogs(f.openLogs) s.ConnectOpenLogs(f.openLogs)
s.ConnectOpenDownloadLink(f.openDownloadLink) s.ConnectOpenDownloadLink(f.openDownloadLink)
@ -207,4 +207,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectCheckPathStatus(CheckPathStatus) s.ConnectCheckPathStatus(CheckPathStatus)
s.ConnectEmitEvent(f.emitEvent) s.ConnectEmitEvent(f.emitEvent)
s.ConnectStartManualUpdate(f.startManualUpdate)
} }

View File

@ -1 +0,0 @@
IDI_ICON1 ICON DISCARDABLE "logo.ico"

View File

@ -0,0 +1,45 @@
#define STRINGIZE_(x) #x
#define STRINGIZE(x) STRINGIZE_(x)
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
#if defined BUILD_BRIDGE
#define FILE_COMMENTS "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer."
#define FILE_DESCRIPTION "ProtonMail Bridge"
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
#define PRODUCT_NAME "ProtonMail Bridge for Windows"
#elif defined BUILD_IE
#define FILE_COMMENTS "The Import-Export app helps you to migrate your emails from local files or remote IMAP servers to ProtonMail or simply export emails to local folder."
#define FILE_DESCRIPTION "ProtonMail Import-Export app"
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
#define PRODUCT_NAME "ProtonMail Import-Export app for Windows"
#else
#error No target specified
#endif
#define LEGAL_COPYRIGHT "(C) " STRINGIZE(YEAR) " Proton Technologies AG"
1 VERSIONINFO
FILEVERSION FILE_VERSION_COMMA,0
PRODUCTVERSION FILE_VERSION_COMMA,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "Comments", FILE_COMMENTS
VALUE "CompanyName", "Proton Technologies AG"
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", STRINGIZE(FILE_VERSION)
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", LEGAL_COPYRIGHT
VALUE "OriginalFilename", STRINGIZE(ORIGINAL_FILE_NAME)
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", STRINGIZE(PRODUCT_VERSION)
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 0x04B0
END
END

View File

@ -114,10 +114,10 @@ func (b *bridgeWrap) GetUser(query string) (User, error) {
type ImportExporter interface { type ImportExporter interface {
UserManager UserManager
GetLocalImporter(string, string) (*transfer.Transfer, error) GetLocalImporter(string, string, string) (*transfer.Transfer, error)
GetRemoteImporter(string, string, string, string, string) (*transfer.Transfer, error) GetRemoteImporter(string, string, string, string, string, string) (*transfer.Transfer, error)
GetEMLExporter(string, string) (*transfer.Transfer, error) GetEMLExporter(string, string, string) (*transfer.Transfer, error)
GetMBOXExporter(string, string) (*transfer.Transfer, error) GetMBOXExporter(string, string, string) (*transfer.Transfer, error)
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
ReportFile(osType, osVersion, accountName, address string, logdata []byte) error ReportFile(osType, osVersion, accountName, address string, logdata []byte) error
} }

View File

@ -181,7 +181,7 @@ func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.L
return err return err
} }
targetSeq := im.storeMailbox.GetUIDList([]string{m.ID}) targetSeq := im.storeMailbox.GetUIDList(IDs)
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq) return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
} }
} }
@ -226,7 +226,7 @@ func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *
return im.storeMailbox.ImportMessage(m, body, labels) return im.storeMailbox.ImportMessage(m, body, labels)
} }
func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem) (msg *imap.Message, err error) { func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []imap.FetchItem, msgBuildCountHistogram *msgBuildCountHistogram) (msg *imap.Message, err error) { //nolint[funlen]
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message") im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message")
seqNum, err := storeMessage.SequenceNumber() seqNum, err := storeMessage.SequenceNumber()
@ -268,7 +268,12 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
// on our part and we need to compute "real" size of decrypted data. // on our part and we need to compute "real" size of decrypted data.
if m.Size <= 0 { if m.Size <= 0 {
im.log.WithField("msgID", storeMessage.ID()).Trace("Size unknown - downloading body") im.log.WithField("msgID", storeMessage.ID()).Trace("Size unknown - downloading body")
if _, _, err = im.getBodyAndStructure(storeMessage); err != nil { // We are sure the size is not a problem right now. Clients
// might not first check sizes of all messages so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude getting size from the
// counting and see build count as real message build.
if _, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
return return
} }
} }
@ -279,7 +284,7 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
return nil, err return nil, err
} }
default: default:
if err = im.getLiteralForSection(item, msg, storeMessage); err != nil { if err = im.getLiteralForSection(item, msg, storeMessage, msgBuildCountHistogram); err != nil {
return return
} }
} }
@ -288,14 +293,14 @@ func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []ima
return msg, err return msg, err
} }
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider) error { func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) error {
section, err := imap.ParseBodySectionName(itemSection) section, err := imap.ParseBodySectionName(itemSection)
if err != nil { // Ignore error if err != nil { // Ignore error
return nil return nil
} }
var literal imap.Literal var literal imap.Literal
if literal, err = im.getMessageBodySection(storeMessage, section); err != nil { if literal, err = im.getMessageBodySection(storeMessage, section, msgBuildCountHistogram); err != nil {
return err return err
} }
@ -313,14 +318,19 @@ func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (bs *
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database") im.log.WithError(err).Debug("Fail to retrieve bodystructure from database")
} }
if bs == nil { if bs == nil {
if bs, _, err = im.getBodyAndStructure(storeMessage); err != nil { // We are sure the body structure is not a problem right now.
// Clients might do first fetch body structure so we couldn't
// be sure if seeing 1st or 2nd sync is all right or not.
// Therefore, it's better to exclude first body structure fetch
// from the counting and see build count as real message build.
if bs, _, err = im.getBodyAndStructure(storeMessage, nil); err != nil {
return return
} }
} }
return return
} }
func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) ( func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider, msgBuildCountHistogram *msgBuildCountHistogram) (
structure *message.BodyStructure, structure *message.BodyStructure,
bodyReader *bytes.Reader, err error, bodyReader *bytes.Reader, err error,
) { ) {
@ -352,6 +362,15 @@ func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) (
WithField("msgID", m.ID). WithField("msgID", m.ID).
Warn("Cannot update header while building") Warn("Cannot update header while building")
} }
if msgBuildCountHistogram != nil {
times, err := storeMessage.IncreaseBuildCount()
if err != nil {
im.log.WithError(err).
WithField("msgID", m.ID).
Warn("Cannot increase build count")
}
msgBuildCountHistogram.add(times)
}
// Drafts can change and we don't want to cache them. // Drafts can change and we don't want to cache them.
if !isMessageInDraftFolder(m) { if !isMessageInDraftFolder(m) {
cache.SaveMail(id, body, structure) cache.SaveMail(id, body, structure)
@ -379,7 +398,7 @@ func isMessageInDraftFolder(m *pmapi.Message) bool {
// This will download message (or read from cache) and pick up the section, // This will download message (or read from cache) and pick up the section,
// extract data (header,body, both) and trim the output if needed. // extract data (header,body, both) and trim the output if needed.
func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider, section *imap.BodySectionName) (literal imap.Literal, err error) { // nolint[funlen] func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider, section *imap.BodySectionName, msgBuildCountHistogram *msgBuildCountHistogram) (literal imap.Literal, err error) { // nolint[funlen]
var ( var (
structure *message.BodyStructure structure *message.BodyStructure
bodyReader *bytes.Reader bodyReader *bytes.Reader
@ -410,7 +429,7 @@ func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider,
} }
} else { } else {
// The rest of cases need download and decrypt. // The rest of cases need download and decrypt.
structure, bodyReader, err = im.getBodyAndStructure(storeMessage) structure, bodyReader, err = im.getBodyAndStructure(storeMessage, msgBuildCountHistogram)
if err != nil { if err != nil {
return return
} }
@ -564,7 +583,7 @@ func (im *imapMailbox) writeRelatedPart(p io.Writer, m *pmapi.Message, inlines [
return return
} }
h := message.GetAttachmentHeader(inline) h := message.GetAttachmentHeader(inline, true)
if p, err = related.CreatePart(h); err != nil { if p, err = related.CreatePart(h); err != nil {
return return
} }
@ -738,7 +757,7 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) (
defer buf.Reset() defer buf.Reset()
att := atts[idx] att := atts[idx]
attachmentHeader := message.GetAttachmentHeader(att) attachmentHeader := message.GetAttachmentHeader(att, true)
if partWriter, err = mw.CreatePart(attachmentHeader); err != nil { if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
return err return err
} }

View File

@ -482,12 +482,13 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
// //
// Messages must be sent to msgResponse. When the function returns, msgResponse must be closed. // Messages must be sent to msgResponse. When the function returns, msgResponse must be closed.
func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) error { func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) error {
msgBuildCountHistogram := newMsgBuildCountHistogram()
return im.logCommand(func() error { return im.logCommand(func() error {
return im.listMessages(isUID, seqSet, items, msgResponse) return im.listMessages(isUID, seqSet, items, msgResponse, msgBuildCountHistogram)
}, "FETCH", isUID, seqSet, items) }, "FETCH", isUID, seqSet, items, msgBuildCountHistogram)
} }
func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) (err error) { //nolint[funlen] func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message, msgBuildCountHistogram *msgBuildCountHistogram) (err error) { //nolint[funlen]
defer func() { defer func() {
close(msgResponse) close(msgResponse)
if err != nil { if err != nil {
@ -544,7 +545,7 @@ func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []ima
return nil, err return nil, err
} }
msg, err := im.getMessage(storeMessage, items) msg, err := im.getMessage(storeMessage, items, msgBuildCountHistogram)
if err != nil { if err != nil {
err = fmt.Errorf("list message build: %v", err) err = fmt.Errorf("list message build: %v", err)
l.WithField("metaID", storeMessage.ID()).Error(err) l.WithField("metaID", storeMessage.ID()).Error(err)

View File

@ -0,0 +1,65 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package imap
import (
"fmt"
"sync"
)
// msgBuildCountHistogram is used to analyse and log the number of repetitive
// downloads of requested messages per one fetch. The number of builds per each
// messageID is stored in persistent database. The msgBuildCountHistogram will
// take this number for each message in ongoing fetch and create histogram of
// repeats.
//
// Example: During `fetch 1:300` there were
// - 100 messages were downloaded first time
// - 100 messages were downloaded second time
// - 99 messages were downloaded 10th times
// - 1 messages were downloaded 100th times
type msgBuildCountHistogram struct {
// Key represents how many times message was build.
// Value stores how many messages are build X times based on the key.
counts map[uint32]uint32
lock sync.Locker
}
func newMsgBuildCountHistogram() *msgBuildCountHistogram {
return &msgBuildCountHistogram{
counts: map[uint32]uint32{},
lock: &sync.Mutex{},
}
}
func (c *msgBuildCountHistogram) String() string {
res := ""
for nRebuild, counts := range c.counts {
if res != "" {
res += ", "
}
res += fmt.Sprintf("[%d]:%d", nRebuild, counts)
}
return res
}
func (c *msgBuildCountHistogram) add(nRebuild uint32) {
c.lock.Lock()
defer c.lock.Unlock()
c.counts[nRebuild]++
}

View File

@ -103,6 +103,7 @@ type storeMessageProvider interface {
SetContentTypeAndHeader(string, mail.Header) error SetContentTypeAndHeader(string, mail.Header) error
SetBodyStructure(*pkgMsg.BodyStructure) error SetBodyStructure(*pkgMsg.BodyStructure) error
GetBodyStructure() (*pkgMsg.BodyStructure, error) GetBodyStructure() (*pkgMsg.BodyStructure, error)
IncreaseBuildCount() (uint32, error)
} }
type storeUserWrap struct { type storeUserWrap struct {

View File

@ -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. // 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) source := transfer.NewLocalProvider(path)
target, err := ie.getPMAPIProvider(address) target, err := ie.getPMAPIProvider(username, address)
if err != nil { if err != nil {
return nil, err 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. // GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account.
func (ie *ImportExport) GetRemoteImporter(address, username, password, host, port string) (*transfer.Transfer, error) { func (ie *ImportExport) GetRemoteImporter(username, address, remoteUsername, remotePassword, host, port string) (*transfer.Transfer, error) {
source, err := transfer.NewIMAPProvider(username, password, host, port) source, err := transfer.NewIMAPProvider(remoteUsername, remotePassword, host, port)
if err != nil { if err != nil {
return nil, err return nil, err
} }
target, err := ie.getPMAPIProvider(address) target, err := ie.getPMAPIProvider(username, address)
if err != nil { if err != nil {
return nil, err 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. // GetEMLExporter returns transferrer from ProtonMail account to local EML structure.
func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer, error) { func (ie *ImportExport) GetEMLExporter(username, address, path string) (*transfer.Transfer, error) {
source, err := ie.getPMAPIProvider(address) source, err := ie.getPMAPIProvider(username, address)
if err != nil { if err != nil {
return nil, err 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. // GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure.
func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfer, error) { func (ie *ImportExport) GetMBOXExporter(username, address, path string) (*transfer.Transfer, error) {
source, err := ie.getPMAPIProvider(address) source, err := ie.getPMAPIProvider(username, address)
if err != nil { if err != nil {
return nil, err 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) return transfer.New(ie.panicHandler, newExportMetricsManager(ie), logsPath, ie.cache.GetTransferDir(), source, target)
} }
func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvider, error) { func (ie *ImportExport) getPMAPIProvider(username, address string) (*transfer.PMAPIProvider, error) {
user, err := ie.Users.GetUser(address) user, err := ie.Users.GetUser(username)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -32,8 +32,8 @@ import (
// On linux: // On linux:
// - settings: ~/.config/protonmail/<app> // - settings: ~/.config/protonmail/<app>
// - logs: ~/.cache/protonmail/<app>/logs // - logs: ~/.cache/protonmail/<app>/logs
// - cache: ~/.cache/protonmail/<app>/cache // - cache: ~/.config/protonmail/<app>/cache
// - updates: ~/.cache/protonmail/<app>/updates // - updates: ~/.config/protonmail/<app>/updates
// - lockfile: ~/.cache/protonmail/<app>/<app>.lock // - lockfile: ~/.cache/protonmail/<app>/<app>.lock
type Locations struct { type Locations struct {
userConfig, userCache string userConfig, userCache string
@ -129,7 +129,7 @@ func (l *Locations) ProvideLogsPath() (string, error) {
return l.getLogsPath(), nil return l.getLogsPath(), nil
} }
// ProvideCachePath returns a location for user cache dirs (e.g. ~/.cache/<company>/<app>/cache). // ProvideCachePath returns a location for user cache dirs (e.g. ~/.config/<company>/<app>/cache).
// It creates it if it doesn't already exist. // It creates it if it doesn't already exist.
func (l *Locations) ProvideCachePath() (string, error) { func (l *Locations) ProvideCachePath() (string, error) {
if err := os.MkdirAll(l.getCachePath(), 0700); err != nil { if err := os.MkdirAll(l.getCachePath(), 0700); err != nil {
@ -139,6 +139,11 @@ func (l *Locations) ProvideCachePath() (string, error) {
return l.getCachePath(), nil 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/<company>/<app>/updates). // ProvideUpdatesPath returns a location for update files (e.g. ~/.cache/<company>/<app>/updates).
// It creates it if it doesn't already exist. // It creates it if it doesn't already exist.
func (l *Locations) ProvideUpdatesPath() (string, error) { func (l *Locations) ProvideUpdatesPath() (string, error) {
@ -149,6 +154,16 @@ func (l *Locations) ProvideUpdatesPath() (string, error) {
return l.getUpdatesPath(), nil 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 { func (l *Locations) getSettingsPath() string {
return l.userConfig return l.userConfig
} }
@ -158,11 +173,33 @@ func (l *Locations) getLogsPath() string {
} }
func (l *Locations) getCachePath() 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 { func (l *Locations) getUpdatesPath() string {
return filepath.Join(l.userCache, "updates") // In order to properly update Bridge 1.6.X and higher we need to
// change the launcher first. Since this is not part of automatic
// updates the migration must wait until manual update. Until that
// we need to keep old path.
if l.configName == "bridge" {
return l.GetOldUpdatesPath()
}
// 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. // Clear removes everything except the lock and update files.

View File

@ -45,7 +45,8 @@ func TestClearRemovesEverythingExceptLockAndUpdateFiles(t *testing.T) {
assert.NoError(t, l.Clear()) assert.NoError(t, l.Clear())
assert.FileExists(t, l.GetLockFile()) 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.getLogsPath())
assert.NoDirExists(t, l.getCachePath()) assert.NoDirExists(t, l.getCachePath())
assert.DirExists(t, l.getUpdatesPath()) assert.DirExists(t, l.getUpdatesPath())
@ -58,6 +59,7 @@ func TestClearUpdateFiles(t *testing.T) {
assert.FileExists(t, l.GetLockFile()) assert.FileExists(t, l.GetLockFile())
assert.DirExists(t, l.getSettingsPath()) assert.DirExists(t, l.getSettingsPath())
assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json"))
assert.DirExists(t, l.getLogsPath()) assert.DirExists(t, l.getLogsPath())
assert.DirExists(t, l.getCachePath()) assert.DirExists(t, l.getCachePath())
assert.NoDirExists(t, l.getUpdatesPath()) assert.NoDirExists(t, l.getUpdatesPath())
@ -75,6 +77,7 @@ func TestCleanLeavesStandardLocationsUntouched(t *testing.T) {
assert.FileExists(t, l.GetLockFile()) assert.FileExists(t, l.GetLockFile())
assert.DirExists(t, l.getSettingsPath()) assert.DirExists(t, l.getSettingsPath())
assert.FileExists(t, filepath.Join(l.getSettingsPath(), "prefs.json"))
assert.DirExists(t, l.getLogsPath()) assert.DirExists(t, l.getLogsPath())
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log1.txt")) assert.FileExists(t, filepath.Join(l.getLogsPath(), "log1.txt"))
assert.FileExists(t, filepath.Join(l.getLogsPath(), "log2.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.NoError(t, err)
require.DirExists(t, settings) require.DirExists(t, settings)
createFilesInDir(t, settings, "prefs.json")
require.FileExists(t, filepath.Join(settings, "prefs.json"))
logs, err := l.ProvideLogsPath() logs, err := l.ProvideLogsPath()
require.NoError(t, err) require.NoError(t, err)
require.DirExists(t, logs) require.DirExists(t, logs)

View File

@ -42,6 +42,7 @@ func NewSMTPServer(debug bool, port int, useSSL bool, tls *tls.Config, smtpBacke
s.TLSConfig = tls s.TLSConfig = tls
s.Domain = bridge.Host s.Domain = bridge.Host
s.AllowInsecureAuth = true s.AllowInsecureAuth = true
s.MaxLineLength = 2 << 16
if debug { if debug {
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")

View File

@ -268,8 +268,6 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
return false, errors.New("received empty event") return false, errors.New("received empty event")
} }
l = l.WithField("newEventID", event.EventID)
if err = loop.processEvent(event); err != nil { if err = loop.processEvent(event); err != nil {
return false, errors.Wrap(err, "failed to process event") return false, errors.Wrap(err, "failed to process event")
} }

View File

@ -67,10 +67,14 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe
} }
res, err := storeMailbox.client().Import([]*pmapi.ImportMsgReq{importReqs}) res, err := storeMailbox.client().Import([]*pmapi.ImportMsgReq{importReqs})
if err == nil && len(res) > 0 { if err != nil {
msg.ID = res[0].MessageID
}
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. // LabelMessages adds the label by calling an API.

View File

@ -148,3 +148,17 @@ func (message *Message) GetBodyStructure() (bs *pkgMsg.BodyStructure, err error)
} }
return bs, nil return bs, nil
} }
func (message *Message) IncreaseBuildCount() (times uint32, err error) {
txUpdate := func(tx *bolt.Tx) error {
times, err = message.store.txIncreaseMsgBuildCount(
tx.Bucket(msgBuildCountBucket),
message.ID(),
)
return err
}
if err = message.store.db.Update(txUpdate); err != nil {
return 0, err
}
return times, nil
}

View File

@ -54,6 +54,8 @@ var (
// * {messageID} -> message data (subject, from, to, time, headers, body size, ...) // * {messageID} -> message data (subject, from, to, time, headers, body size, ...)
// * bodystructure // * bodystructure
// * {messageID} -> message body structure // * {messageID} -> message body structure
// * msgbuildcount
// * {messageID} -> uint32 number of message builds to track re-sync issues
// * counts // * counts
// * {mailboxID} -> mailboxCounts: totalOnAPI, unreadOnAPI, labelName, labelColor, labelIsExclusive // * {mailboxID} -> mailboxCounts: totalOnAPI, unreadOnAPI, labelName, labelColor, labelIsExclusive
// * address_info // * address_info
@ -76,6 +78,7 @@ var (
// * {messageID} -> true // * {messageID} -> true
metadataBucket = []byte("metadata") //nolint[gochecknoglobals] metadataBucket = []byte("metadata") //nolint[gochecknoglobals]
bodystructureBucket = []byte("bodystructure") //nolint[gochecknoglobals] bodystructureBucket = []byte("bodystructure") //nolint[gochecknoglobals]
msgBuildCountBucket = []byte("msgbuildcount") //nolint[gochecknoglobals]
countsBucket = []byte("counts") //nolint[gochecknoglobals] countsBucket = []byte("counts") //nolint[gochecknoglobals]
addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals] addressInfoBucket = []byte("address_info") //nolint[gochecknoglobals]
addressModeBucket = []byte("address_mode") //nolint[gochecknoglobals] addressModeBucket = []byte("address_mode") //nolint[gochecknoglobals]
@ -204,6 +207,10 @@ func openBoltDatabase(filePath string) (db *bolt.DB, err error) {
return return
} }
if _, err = tx.CreateBucketIfNotExists(msgBuildCountBucket); err != nil {
return
}
if _, err = tx.CreateBucketIfNotExists(countsBucket); err != nil { if _, err = tx.CreateBucketIfNotExists(countsBucket); err != nil {
return return
} }
@ -263,7 +270,7 @@ func (store *Store) init(firstInit bool) (err error) {
} }
} }
store.log.WithField("mode", store.addressMode).Debug("Initialising store") store.log.WithField("mode", store.addressMode).Info("Initialising store")
labels, err := store.initCounts() labels, err := store.initCounts()
if err != nil { if err != nil {

View File

@ -191,6 +191,19 @@ func (store *Store) txGetBodyStructure(bsBucket *bolt.Bucket, msgID string) (*pk
return pkgMsg.DeserializeBodyStructure(raw) return pkgMsg.DeserializeBodyStructure(raw)
} }
func (store *Store) txIncreaseMsgBuildCount(b *bolt.Bucket, msgID string) (uint32, error) {
key := []byte(msgID)
count := uint32(0)
raw := b.Get(key)
if raw != nil {
count = btoi(raw)
}
count++
return count, b.Put(key, itob(count))
}
// createOrUpdateMessageEvent is helper to create only one message with // createOrUpdateMessageEvent is helper to create only one message with
// createOrUpdateMessagesEvent. // createOrUpdateMessagesEvent.
func (store *Store) createOrUpdateMessageEvent(msg *pmapi.Message) error { func (store *Store) createOrUpdateMessageEvent(msg *pmapi.Message) error {

View File

@ -57,19 +57,23 @@ func WriteAttachmentBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message, att
att.Name += ".gpg" att.Name += ".gpg"
att.MIMEType = "application/pgp-encrypted" //nolint att.MIMEType = "application/pgp-encrypted" //nolint
} else if err != nil && err != openpgperrors.ErrSignatureExpired { } else if err != nil && err != openpgperrors.ErrSignatureExpired {
err = fmt.Errorf("cannot decrypt attachment: %v", err) return fmt.Errorf("cannot decrypt attachment: %v", err)
return }
// 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. // Encode it.
ww := textwrapper.NewRFC822(w) ww := textwrapper.NewRFC822(w)
bw := base64.NewEncoder(base64.StdEncoding, ww) bw := base64.NewEncoder(base64.StdEncoding, ww)
var n int64 if n, err := io.Copy(bw, dr); err != nil {
if n, err = io.Copy(bw, dr); err != nil { return fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n)
err = fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n)
} }
return bw.Close()
_ = bw.Close()
return
} }

View File

@ -124,7 +124,7 @@ func (bld *Builder) writeRelatedPart(p io.Writer, inlines []*pmapi.Attachment) e
return err return err
} }
h := GetAttachmentHeader(inline) h := GetAttachmentHeader(inline, false)
if p, err = related.CreatePart(h); err != nil { if p, err = related.CreatePart(h); err != nil {
return err return err
} }
@ -194,7 +194,7 @@ func (bld *Builder) BuildMessage() (structure *BodyStructure, message []byte, er
return nil, nil, err return nil, nil, err
} }
attachmentHeader := GetAttachmentHeader(att) attachmentHeader := GetAttachmentHeader(att, false)
if partWriter, err = mw.CreatePart(attachmentHeader); err != nil { if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
return nil, nil, err 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++ { for i := 0; i < len(m.Attachments); i++ {
att := m.Attachments[i] att := m.Attachments[i]
r := readers[i] r := readers[i]
h := GetAttachmentHeader(att) h := GetAttachmentHeader(att, false)
p, err := mw.CreatePart(h) p, err := mw.CreatePart(h)
if err != nil { if err != nil {
return nil, err 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) data, err := ioutil.ReadAll(r)
if err != nil { if err != nil {
@ -332,6 +327,9 @@ func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
ww := textwrapper.NewRFC822(p)
bw := base64.NewEncoder(base64.StdEncoding, ww)
if _, err := bw.Write(pgpMessage.GetBinary()); err != nil { if _, err := bw.Write(pgpMessage.GetBinary()); err != nil {
return nil, err return nil, err
} }

View File

@ -107,12 +107,17 @@ func GetRelatedHeader(m *pmapi.Message) textproto.MIMEHeader {
return h return h
} }
func GetAttachmentHeader(att *pmapi.Attachment) textproto.MIMEHeader { func GetAttachmentHeader(att *pmapi.Attachment, buildForIMAP bool) textproto.MIMEHeader {
mediaType := att.MIMEType mediaType := att.MIMEType
if mediaType == "application/pgp-encrypted" { if mediaType == "application/pgp-encrypted" {
mediaType = "application/octet-stream" mediaType = "application/octet-stream"
} }
transferEncoding := "base64"
if mediaType == rfc822Message && buildForIMAP {
transferEncoding = "8bit"
}
encodedName := pmmime.EncodeHeader(att.Name) encodedName := pmmime.EncodeHeader(att.Name)
disposition := "attachment" //nolint[goconst] disposition := "attachment" //nolint[goconst]
if strings.Contains(att.Header.Get("Content-Disposition"), "inline") { 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 := make(textproto.MIMEHeader)
h.Set("Content-Type", mime.FormatMediaType(mediaType, map[string]string{"name": encodedName})) 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})) h.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{"filename": encodedName}))
// Forward some original header lines. // Forward some original header lines.

View File

@ -26,6 +26,10 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const (
rfc822Message = "message/rfc822"
)
var log = logrus.WithField("pkg", "pkg/message") //nolint[gochecknoglobals] var log = logrus.WithField("pkg", "pkg/message") //nolint[gochecknoglobals]
func GetBoundary(m *pmapi.Message) string { func GetBoundary(m *pmapi.Message) string {

View File

@ -201,7 +201,7 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
mediaType, params, _ := pmmime.ParseMediaType(info.Header.Get("Content-Type")) mediaType, params, _ := pmmime.ParseMediaType(info.Header.Get("Content-Type"))
// If multipart, call getAllParts, else read to count lines. // 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) newPath := append(currentPath, 1)
var br *boundaryReader var br *boundaryReader

View File

@ -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 ## v1.6.5
- 2021-02-22 - 2021-02-22

View File

@ -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 ## v1.6.3
- 2021-02-16 - 2021-02-16

View File

@ -1,51 +1,49 @@
## v1.3.1
- 2021-03-11
### New
- Reduce the number of import errors by supporting malformed undisclosed-recipient and better handling of overly long headers
- Improvements to how large attachments are processed
- New format of the release notes
### Fixed
- Linux font issues - Fedora specific
- Fixed rare message misplacement
- Ensure removal of the startup entry during uninstallation
- Update errors
## v1.2.2 ## v1.2.2
- 2020-11-27 - 2020-11-27
### New ### New
Improvements to the import from large mbox files with multiple labels - Improvements to the import from large mbox files with multiple labels
- Not allow to run multiple instances of the app or transfers at the same time
Not allow to run multiple instances of the app or transfers at the same time - Better handling and displaying of skipped messages
- Various enhancements of the import process related to parsing
Better handling and displaying of skipped messages - Cosmetic GUI changes
- Better error handling
Various enhancements of the import process related to parsing
Cosmetic GUI changes
Better error handling
### Fixed ### Fixed
- Linux font issues - Fedora specific
Linux font issues - Fedora specific - App response to the user pausing and canceling import or export
- Upgrade errors
App response to the user pausing and canceling import or export
Upgrade errors
## v1.1.2 ## v1.1.2
- 2020-09-23 - 2020-09-23
### New ### New
- Improving performance
Improving performance - Speed up import by implementing parallel processing (parallel fetch, encrypt and upload of messages)
- Optimising the initial fetch of messages from external accounts
* Speed up import by implementing parallel processing (parallel fetch, encrypt and upload of messages) - Better message parsing
* Optimising the initial fetch of messages from external accounts - Better handling of attachments and non-standard formatting
- Improved stability of the message parser
Better message parsing - Improved metrics
- Added persistent anonymous API cookies
* Better handling of attachments and non-standard formatting
* Improved stability of the message parser
Improved metrics
* Added persistent anonymous API cookies
### Fixed ### Fixed
- Fixed issues causing failing of import
Fixed issues causing failing of import - Import from mbox files with long lines
- Improvements to import from Yahoo accounts
* Import from mbox files with long lines
* Improvements to import from Yahoo accounts

View File

@ -117,3 +117,64 @@ Feature: IMAP import messages
Then IMAP response is "OK" Then IMAP response is "OK"
And API mailbox "INBOX" for "user" has 0 message And API mailbox "INBOX" for "user" has 0 message
And API mailbox "Sent" for "user" has 1 message And API mailbox "Sent" for "user" has 1 message
Scenario: Import embedded message
When IMAP client imports message to "INBOX"
"""
From: Foo <foo@example.com>
To: Bridge Test <bridgetest@pm.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 <bar@example.com>
To: Bridge Test <bridgetest@pm.test>
Subject: (No Subject)
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
hello
--boundary--
"""
Then IMAP response is "OK"
# We cannot control internal IDs on live server.
@ignore-live
Scenario: Import existing message
When IMAP client imports message to "INBOX"
"""
From: Foo <foo@example.com>
To: Bridge Test <bridgetest@pm.test>
Subject: Hello
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
X-Pm-Internal-Id: 1
Hello
"""
Then IMAP response is "OK \[APPENDUID \d 1\] APPEND completed"
When IMAP client imports message to "INBOX"
"""
From: Foo <foo@example.com>
To: Bridge Test <bridgetest@pm.test>
Subject: Hello
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
X-Pm-Internal-Id: 1
Hello
"""
Then IMAP response is "OK \[APPENDUID \d 1\] APPEND completed"

View File

@ -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 <bridgetest@protonmail.com>
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 <bar@example.com>
To: Bridge Test <bridgetest@pm.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 |

View File

@ -294,3 +294,42 @@ Feature: SMTP sending of HTML messages
} }
} }
""" """
Scenario: HTML message with extremely long line (greater than default 2000 line limit) to external account
When SMTP client sends message
"""
From: Bridge Test <[userAddress]>
To: External Bridge <pm.bridge.qa@gmail.com>
Subject: HTML text external
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=utf-8
In-Reply-To: <base64hashOfSomeMessage@protonmail.internalid>
<html><body>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<body></html>
"""
Then SMTP response is "OK"
And mailbox "Sent" for "user" has messages
| from | to | subject |
| [userAddress] | pm.bridge.qa@gmail.com | HTML text external |
And message is sent with API call
"""
{
"Message": {
"Subject": "HTML text external",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "pm.bridge.qa@gmail.com",
"Name": "External Bridge"
}
],
"CCList": [],
"BCCList": [],
"MIMEType": "text/html"
}
}
"""

View File

@ -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 <foo@example.com>
To: Bridge Test <bridgetest@pm.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 <bar@example.com>
To: Bridge Test <bridgetest@pm.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 |

View File

@ -35,6 +35,16 @@ Feature: Import to sent
| foo@example.com | bridgetest@pm.test | one | | foo@example.com | bridgetest@pm.test | one |
| bar@example.com | bridgetest@pm.test | two | | bar@example.com | bridgetest@pm.test | two |
# Messages imported to label only are added automatically to Archive folder.
# Then it depends on the order: if the message is first imported to Sent
# folder and later to that label with importing to Archive, message will not
# be in Sent but Archive. The order is semi-random for the big messages,
# e.g., it will do alphabetical order of mailboxes, but for under ten small
# messages the order is random every time (because we are importing in
# batches of up to ten messages and iterating through map we use to collect
# messages is random). So we cannot for this test ensure the same output
# every time.
@ignore-live
Scenario: Import to sent and custom label Scenario: Import to sent and custom label
And there is EML file "Label/one.eml" And there is EML file "Label/one.eml"
""" """

View File

@ -19,6 +19,7 @@ package tests
import ( import (
"strconv" "strconv"
"strings"
"time" "time"
"github.com/cucumber/godog" "github.com/cucumber/godog"
@ -47,9 +48,12 @@ func imapResponseIs(expectedResponse string) error {
func imapResponseNamedIs(clientID, expectedResponse string) error { func imapResponseNamedIs(clientID, expectedResponse string) error {
res := ctx.GetIMAPLastResponse(clientID) res := ctx.GetIMAPLastResponse(clientID)
if expectedResponse == "OK" { switch {
case expectedResponse == "OK":
res.AssertOK() res.AssertOK()
} else { case strings.HasPrefix(expectedResponse, "OK"):
res.AssertResult(expectedResponse)
default:
res.AssertError(expectedResponse) res.AssertError(expectedResponse)
} }
return ctx.GetTestingError() return ctx.GetTestingError()

View File

@ -107,6 +107,13 @@ func (ir *IMAPResponse) AssertOK() *IMAPResponse {
return ir return ir
} }
func (ir *IMAPResponse) AssertResult(wantResult string) *IMAPResponse {
ir.wait()
a.NoError(ir.t, ir.err)
a.Regexp(ir.t, wantResult, ir.result, "Expected result %s but got %s", wantResult, ir.result)
return ir
}
func (ir *IMAPResponse) AssertError(wantErrMsg string) *IMAPResponse { func (ir *IMAPResponse) AssertError(wantErrMsg string) *IMAPResponse {
ir.wait() ir.wait()
if ir.err == nil { if ir.err == nil {

View File

@ -60,9 +60,9 @@ func userImportsLocalFilesToAddress(bddUserID, bddAddressID string) error {
} }
func userImportsLocalFilesToAddressWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) 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() 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 { 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() 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 { 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() 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 { 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() path := ctx.GetTransferLocalRootForExport()
return ctx.GetImportExport().GetMBOXExporter(address, path) return ctx.GetImportExport().GetMBOXExporter(username, address, path)
}) })
} }
// Helpers. // 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) account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
if account == nil { if account == nil {
return godog.ErrPending return godog.ErrPending
} }
transferrer, err := getTransferrer(account.Address()) transferrer, err := getTransferrer(account.Username(), account.Address())
if err != nil { if err != nil {
return internalError(err, "failed to init transfer") return internalError(err, "failed to init transfer")
} }

View File

@ -17,10 +17,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. # along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
# The Qt libs are dynamically loaded with rules like: `@rpath/QtGui.framework/Versions/5/QtGui`
## Make sure that mac exe will not contain broken library links # @rpath instructs the dynamic linker to search a list of paths in order to locate the framework
# * remove absolute paths for Qt libs # The rules can be listed using `otool -l "${path_to_binary}"`
# * add relative part to app bundle Frameworks # The building process of therecipe/qt or qmake leaves the rules with additinal unwanted paths
# + absolute path to build directory
# + dummy replacement `/break_the_rpath`
# We need to manually remove those and add the path relative to exectuable: `@executable_path/../Frameworks`
path_to_binary=$1 path_to_binary=$1
@ -29,11 +32,11 @@ if [ -z ${path_to_binary} ]; then
exit 2 exit 2
fi fi
for remove_path_qt in $(otool -l "${path_to_binary}" | grep '/Users/' | awk '{print $2}'); for path_to_remove in $(otool -l "${path_to_binary}" | egrep '/Users/|break_the_rpath' | awk '{print $2}');
do do
if [ ! -z "${remove_path_qt}" ]; then if [ ! -z "${path_to_remove}" ]; then
printf "\e[0;32mRemove path to qt ${remove_path_qt} ...\033[0m\n\e[0;31m" printf "\e[0;32mRemove path to qt '${path_to_remove}' ...\033[0m\n\e[0;31m"
install_name_tool -delete_rpath "${remove_path_qt}" "${path_to_binary}" || exit 1 install_name_tool -delete_rpath "${path_to_remove}" "${path_to_binary}" || exit 1
fi fi
done done
rpath_rule=$(otool -l "${path_to_binary}" | grep executable_path | awk '{print $2}') rpath_rule=$(otool -l "${path_to_binary}" | grep executable_path | awk '{print $2}')