Compare commits

...

30 Commits

Author SHA1 Message Date
199a4d1e3a Other: Release Bridge HZM 1.6.6 2021-02-25 16:28:17 +01:00
18668aafc9 GODT-1029: Fix tray icon not updating under certain conditions 2021-02-25 14:53:43 +00:00
fd73ec6861 GODT-1062: Fix lost notification bar when window is closed 2021-02-25 14:53:43 +00:00
feeb7179f5 GODT-1058 Install after chaning channel right away only in case of downgrade 2021-02-25 14:47:12 +00:00
0e5a45671f GODT-1073 Added: Re-write autostart link on every start if turned on in preferences. 2021-02-24 19:32:59 +00:00
2beb0d298e Other: QA build checks for update every 5 minutes 2021-02-24 20:34:13 +01:00
22a6fcd87f Other: add debug message dump when sending 2021-02-23 10:38:15 +00:00
f499252444 GODT-1055 Fix flaky empty trash test 2021-02-23 08:37:07 +00:00
b27e3fdb28 Merge release/hzm in devel 2021-02-22 17:36:31 +01:00
415e56d928 Other Update bridge_early.md 2021-02-22 15:42:30 +01:00
845074f421 Other: Bridge HZM 1.6.5 2021-02-19 13:00:01 +01:00
28f46deef9 Other: only choose pass if usable 2021-02-18 13:23:38 +01:00
2a078b76e6 GODT-1045 build without Qt by default 2021-02-18 09:45:18 +00:00
3428557b15 Other: Bridge HZM 1.6.4 2021-02-17 14:17:11 +01:00
1f25aeab31 GODT-980: placeholder for user agent 2021-02-17 13:49:51 +01:00
4e531d4524 GODT-1036 Event loop Sentry reporting of failures and refresh 2021-02-17 09:17:19 +00:00
7fc7083c76 GODT-957 Increase space to hide difference 2021-02-17 08:37:12 +00:00
0fe69d9de1 GODT-937: Add keychain switcher to frontend
GODT-1008: Fix transparent dialog under certain conditions
2021-02-17 07:35:59 +00:00
8b436186a4 GODT-1034 More tolerant connection speed detection 2021-02-17 06:13:15 +00:00
4d000c2376 GODT-1018 Pre-push git hook to check lints 2021-02-17 05:10:42 +00:00
56bce8e06f Other: Make all command line flags as const strings 2021-02-16 22:01:50 +00:00
6fd614595d Other: 1.6.3 release notes update 2021-02-16 19:39:43 +01:00
7bb7e1a518 GODT-1041 Log IMAP requests to debug Apple Mail re-sync issue 2021-02-16 14:15:37 +00:00
fb89fb7b31 Other: pretty print prefs.json 2021-02-15 11:31:42 +01:00
e6ae344f1f GODT-797 APPEND waits for EXPUNGE to prevent data loss when Outlook moves from Spam or Trash 2021-02-12 15:33:31 +01:00
bad8cad97d Other: fix nogui build 2021-02-12 09:34:10 +01:00
77cd2955f1 chore: remove credits 2021-02-11 15:10:53 +00:00
567b65df8d feat: autoupdates CLI commands 2021-02-11 08:40:51 +00:00
06b3ed9b85 GODT-317 Fix wrong total mailbox size in Apple Mail 2021-02-11 07:29:28 +00:00
565c0b6ddf Fixing changelog punctuation. 2021-02-10 16:09:47 +01:00
102 changed files with 1484 additions and 610 deletions

3
.gitignore vendored
View File

@ -26,6 +26,9 @@ internal/frontend/qml/ProtonUI/images
internal/frontend/qml/ImportExportUI/images internal/frontend/qml/ImportExportUI/images
frontend/qml/*.qmlc frontend/qml/*.qmlc
# Credits files (generated).
internal/**/credits.go
# Build files # Build files
/launcher-* /launcher-*
/bridge_*_*.tgz /bridge_*_*.tgz

View File

@ -2,6 +2,56 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.6.6] HZM
### Added
* Other: QA build checks for update every 5 minutes.
* Other: QA build adds debug message dump when sending.
### Changed
* GODT-1045 build without Qt by default.
### Fixed
* GODT-1029 Fix tray icon not updating under certain conditions.
* GODT-1062 Fix lost notification bar when window is closed.
* GODT-1058 Install version after chaning channel right away only in case of downgrade.
* GODT-1073 Re-write autostart link on every start if turned on in preferences.
* GODT-1055 Fix flaky empty trash test.
## [Bridge 1.6.5] HZM
### Changed
* GODT-1059 Check if keychain is usable on linux before using it by default.
## [Bridge 1.6.4] HZM
### Added
* Other: Autoupdates CLI commands.
### Removed
* Other: Remove credits.
### Changed
* GODT-980 Placeholder for user agent.
* GODT-1036 Event loop Sentry reporting of failures and refresh.
* GODT-957 Increase space to hide difference.
* GODT-937 Add keychain switcher to frontend.
* GODT-1008 Fix transparent dialog under certain conditions.
* GODT-1034 More tolerant connection speed detection.
* GODT-1018 Pre-push git hook to check lints.
* Other: Make all command line flags as const strings.
* GODT-1041 Log IMAP requests to debug Apple Mail re-sync issue.
* Other: Pretty print prefs.json.
### Fixed
* Other: Fix nogui build.
* GODT-317 Fix wrong total mailbox size in Apple Mail.
* Other: Fixing changelog punctuation.
* GODT-797 APPEND waits for EXPUNGE to prevent data loss when Outlook moves from Spam or Trash.
## [Bridge 1.6.3] HZM ## [Bridge 1.6.3] HZM
### Added ### Added
@ -10,15 +60,15 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Changed ### Changed
* GODT-885 Do not explicitly unlabel folders during move to match behaviour of other clients. * GODT-885 Do not explicitly unlabel folders during move to match behaviour of other clients.
* GODT-616 Better user message about wrong mailbox password. * GODT-616 Better user message about wrong mailbox password.
* GODT-1021 Do not allow copy Inbox->Sent or Sent->Inbox * GODT-1021 Do not allow copy Inbox->Sent or Sent->Inbox.
* GODT-976 Exclude updates from clearing cache and clear cache, including updates, while switching early access off * GODT-976 Exclude updates from clearing cache and clear cache, including updates, while switching early access off.
* GODT-1033 Retry starting IMAP server after connection was down * GODT-1033 Retry starting IMAP server after connection was down.
### Fixed ### Fixed
* GODT-1011 Stable integration test deleting many messages using UID EXPUNGE. * GODT-1011 Stable integration test deleting many messages using UID EXPUNGE.
* GODT-1015 Use lenient version parser to properly parse version provided by Mac. * GODT-1015 Use lenient version parser to properly parse version provided by Mac.
* GODT-919 Notify about update right after the start. * GODT-919 Notify about update right after the start.
* GODT-919 GODT-1022 Logs and signals * GODT-919 GODT-1022 Logs and signals.
## [IE 1.3.0] Farg ## [IE 1.3.0] Farg

View File

@ -10,7 +10,7 @@ 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.3+git BRIDGE_APP_VERSION?=1.6.6+git
IE_APP_VERSION?=1.3.0+git IE_APP_VERSION?=1.3.0+git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico SRC_ICO:=logo.ico
@ -33,7 +33,7 @@ BUILD_TIME:=$(shell date +%FT%T%z)
BUILD_FLAGS:=-tags='${BUILD_TAGS}' BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS} BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui' BUILD_FLAGS_GUI:=-tags='${BUILD_TAGS} build_qt'
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/internal/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME}) GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/internal/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
ifneq "${BUILD_LDFLAGS}" "" ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+=${BUILD_LDFLAGS} GO_LDFLAGS+=${BUILD_LDFLAGS}
@ -45,7 +45,7 @@ ifeq "${TARGET_OS}" "windows"
endif endif
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}' BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_NOGUI+=-ldflags '${GO_LDFLAGS}' BUILD_FLAGS_GUI+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}' BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
@ -83,8 +83,8 @@ build: ${TGZ_TARGET}
build-ie: build-ie:
TARGET_CMD=Import-Export $(MAKE) build TARGET_CMD=Import-Export $(MAKE) build
build-nogui: build-nogui: gofiles
go build ${BUILD_FLAGS_NOGUI} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go go build ${BUILD_FLAGS} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
build-ie-nogui: build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui TARGET_CMD=Import-Export $(MAKE) build-nogui
@ -137,7 +137,7 @@ endif
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET} ${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${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} ${QT_BUILD_TARGET} qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET}
mv deploy cmd/${TARGET_CMD} mv deploy cmd/${TARGET_CMD}
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
@ -180,7 +180,7 @@ update-qt-docs:
go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API) go get github.com/therecipe/qt/internal/binding/files/docs/$(QT_API)
## Dev dependencies ## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated .PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
LINTVER:="v1.29.0" LINTVER:="v1.29.0"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh" LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
@ -197,6 +197,9 @@ install-linter: check-has-go
install-go-mod-outdated: install-go-mod-outdated:
which go-mod-outdated || go get -u github.com/psampaz/go-mod-outdated which go-mod-outdated || go get -u github.com/psampaz/go-mod-outdated
install-git-hooks:
cp utils/githooks/* .git/hooks/
chmod +x .git/hooks/*
## Checks, mocks and docs ## Checks, mocks and docs
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc release-notes .PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc release-notes
@ -249,14 +252,13 @@ mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
lint: lint-golang lint-license lint-changelog lint: gofiles lint-golang lint-license lint-changelog
lint-license: lint-license:
./utils/missing_license.sh check ./utils/missing_license.sh check
lint-changelog: lint-changelog:
./utils/changelog_linter.sh Changelog.md ./utils/changelog_linter.sh Changelog.md
./utils/changelog_linter.sh unreleased.md
lint-golang: lint-golang:
which golangci-lint || $(MAKE) install-linter which golangci-lint || $(MAKE) install-linter
@ -301,12 +303,12 @@ run-qt-cli: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
run-nogui: clean-vendor gofiles run-nogui: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log PROTONMAIL_ENV=dev go run ${BUILD_FLAGS} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log
run-nogui-cli: clean-vendor gofiles run-nogui-cli: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c PROTONMAIL_ENV=dev go run ${BUILD_FLAGS} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c
run-debug: run-debug:
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS} PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS}
run-qml-preview: run-qml-preview:
$(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview $(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview

View File

@ -25,13 +25,14 @@ import (
"runtime" "runtime"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/crash" "github.com/ProtonMail/proton-bridge/internal/crash"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/logging" "github.com/ProtonMail/proton-bridge/internal/logging"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/internal/versioner" "github.com/ProtonMail/proton-bridge/internal/versioner"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -44,7 +45,7 @@ var (
) )
func main() { // nolint[funlen] func main() { // nolint[funlen]
reporter := sentry.NewReporter(appName, constants.Version) reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
crashHandler := crash.NewHandler(reporter.ReportException) crashHandler := crash.NewHandler(reporter.ReportException)
defer crashHandler.HandlePanic() defer crashHandler.HandlePanic()

5
go.mod
View File

@ -6,7 +6,7 @@ go 1.13
// They are in a separate require block to highlight this. // They are in a separate require block to highlight this.
require ( require (
github.com/docker/docker-credential-helpers v0.6.3 github.com/docker/docker-credential-helpers v0.6.3
github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df github.com/emersion/go-imap v1.0.6
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
) )
@ -29,7 +29,7 @@ require (
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
github.com/emersion/go-mbox v1.0.2 github.com/emersion/go-mbox v1.0.2
github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b github.com/emersion/go-message v0.12.1-0.20201221184100-40c3f864532b
@ -54,6 +54,7 @@ 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

6
go.sum
View File

@ -77,8 +77,8 @@ github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCd
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78= github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU= github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0= github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM= github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I= github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
@ -223,6 +223,8 @@ 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=

View File

@ -43,38 +43,55 @@ import (
"github.com/ProtonMail/proton-bridge/internal/config/cache" "github.com/ProtonMail/proton-bridge/internal/config/cache"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/tls" "github.com/ProtonMail/proton-bridge/internal/config/tls"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/cookies" "github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/crash" "github.com/ProtonMail/proton-bridge/internal/crash"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/logging" "github.com/ProtonMail/proton-bridge/internal/logging"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/internal/versioner" "github.com/ProtonMail/proton-bridge/internal/versioner"
"github.com/ProtonMail/proton-bridge/pkg/keychain" "github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/allan-simon/go-singleinstance" "github.com/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
const (
flagCPUProfile = "cpu-prof"
flagCPUProfileShort = "p"
flagMemProfile = "mem-prof"
flagMemProfileShort = "m"
flagLogLevel = "log-level"
flagLogLevelShort = "l"
// FlagCLI indicate to start with command line interface
FlagCLI = "cli"
flagCLIShort = "c"
flagRestart = "restart"
flagLauncher = "launcher"
)
type Base struct { type Base struct {
CrashHandler *crash.Handler SentryReporter *sentry.Reporter
Locations *locations.Locations CrashHandler *crash.Handler
Settings *settings.Settings Locations *locations.Locations
Lock *os.File Settings *settings.Settings
Cache *cache.Cache Lock *os.File
Listener listener.Listener Cache *cache.Cache
Creds *credentials.Store Listener listener.Listener
CM *pmapi.ClientManager Creds *credentials.Store
CookieJar *cookies.Jar CM *pmapi.ClientManager
Updater *updater.Updater CookieJar *cookies.Jar
Versioner *versioner.Versioner UserAgent *useragent.UserAgent
TLS *tls.TLS Updater *updater.Updater
Autostart *autostart.App Versioner *versioner.Versioner
TLS *tls.TLS
Autostart *autostart.App
Name string // the app's name Name string // the app's name
usage string // the app's usage description usage string // the app's usage description
@ -92,7 +109,10 @@ func New( // nolint[funlen]
keychainName, keychainName,
cacheVersion string, cacheVersion string,
) (*Base, error) { ) (*Base, error) {
sentryReporter := sentry.NewReporter(appName, constants.Version) userAgent := useragent.New()
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
crashHandler := crash.NewHandler( crashHandler := crash.NewHandler(
sentryReporter.ReportException, sentryReporter.ReportException,
crash.ShowErrorNotification(appName), crash.ShowErrorNotification(appName),
@ -166,20 +186,9 @@ func New( // nolint[funlen]
return nil, err return nil, err
} }
apiConfig := pmapi.GetAPIConfig(configName, constants.Version) cm := pmapi.NewClientManager(getAPIConfig(configName, listener), userAgent)
apiConfig.ConnectionOffHandler = func() {
listener.Emit(events.InternetOffEvent, "")
}
apiConfig.ConnectionOnHandler = func() {
listener.Emit(events.InternetOnEvent, "")
}
apiConfig.UpgradeApplicationHandler = func() {
listener.Emit(events.UpgradeApplicationEvent, "")
}
cm := pmapi.NewClientManager(apiConfig)
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener)) cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
cm.SetCookieJar(jar) cm.SetCookieJar(jar)
sentryReporter.SetUserAgentProvider(cm)
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey) key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil { if err != nil {
@ -220,19 +229,21 @@ func New( // nolint[funlen]
} }
return &Base{ return &Base{
CrashHandler: crashHandler, SentryReporter: sentryReporter,
Locations: locations, CrashHandler: crashHandler,
Settings: settingsObj, Locations: locations,
Lock: lock, Settings: settingsObj,
Cache: cache, Lock: lock,
Listener: listener, Cache: cache,
Creds: credentials.NewStore(kc), Listener: listener,
CM: cm, Creds: credentials.NewStore(kc),
CookieJar: jar, CM: cm,
Updater: updater, CookieJar: jar,
Versioner: versioner, UserAgent: userAgent,
TLS: tls.New(settingsPath), Updater: updater,
Autostart: autostart, Versioner: versioner,
TLS: tls.New(settingsPath),
Autostart: autostart,
Name: appName, Name: appName,
usage: appUsage, usage: appUsage,
@ -252,32 +263,32 @@ func (b *Base) NewApp(action func(*Base, *cli.Context) error) *cli.App {
app.Action = b.run(action) app.Action = b.run(action)
app.Flags = []cli.Flag{ app.Flags = []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
Name: "cpu-prof", Name: flagCPUProfile,
Aliases: []string{"p"}, Aliases: []string{flagCPUProfileShort},
Usage: "Generate CPU profile", Usage: "Generate CPU profile",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "mem-prof", Name: flagMemProfile,
Aliases: []string{"m"}, Aliases: []string{flagMemProfileShort},
Usage: "Generate memory profile", Usage: "Generate memory profile",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "log-level", Name: flagLogLevel,
Aliases: []string{"l"}, Aliases: []string{flagLogLevelShort},
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)", Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "cli", Name: FlagCLI,
Aliases: []string{"c"}, Aliases: []string{flagCLIShort},
Usage: "Use command line interface", Usage: "Use command line interface",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "restart", Name: flagRestart,
Usage: "The number of times the application has already restarted", Usage: "The number of times the application has already restarted",
Hidden: true, Hidden: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "launcher", Name: flagLauncher,
Usage: "The launcher to use to restart the application", Usage: "The launcher to use to restart the application",
Hidden: true, Hidden: true,
}, },
@ -302,21 +313,21 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
defer func() { _ = b.Lock.Close() }() defer func() { _ = b.Lock.Close() }()
// If launcher was used to start the app, use that for restart/autostart. // If launcher was used to start the app, use that for restart/autostart.
if launcher := c.String("launcher"); launcher != "" { if launcher := c.String(flagLauncher); launcher != "" {
b.Autostart.Exec = []string{launcher} b.Autostart.Exec = []string{launcher}
b.command = launcher b.command = launcher
} }
if doCPUProfile := c.Bool("cpu-prof"); doCPUProfile { if c.Bool(flagCPUProfile) {
startCPUProfile() startCPUProfile()
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
} }
if doMemoryProfile := c.Bool("mem-prof"); doMemoryProfile { if c.Bool(flagMemProfile) {
defer makeMemoryProfile() defer makeMemoryProfile()
} }
logging.SetLevel(c.String("log-level")) logging.SetLevel(c.String(flagLogLevel))
logrus. logrus.
WithField("appName", b.Name). WithField("appName", b.Name).
@ -328,7 +339,7 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
Info("Run app") Info("Run app")
b.CrashHandler.AddRecoveryAction(func(interface{}) error { b.CrashHandler.AddRecoveryAction(func(interface{}) error {
if c.Int("restart") > maxAllowedRestarts { if c.Int(flagRestart) > maxAllowedRestarts {
logrus. logrus.
WithField("restart", c.Int("restart")). WithField("restart", c.Int("restart")).
Warn("Not restarting, already restarted too many times") Warn("Not restarting, already restarted too many times")
@ -364,3 +375,13 @@ func (b *Base) doTeardown() error {
return nil return nil
} }
func getAPIConfig(configName string, listener listener.Listener) *pmapi.ClientConfig {
apiConfig := pmapi.GetAPIConfig(configName, constants.Version)
apiConfig.ConnectionOffHandler = func() { listener.Emit(events.InternetOffEvent, "") }
apiConfig.ConnectionOnHandler = func() { listener.Emit(events.InternetOnEvent, "") }
apiConfig.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
return apiConfig
}

View File

@ -38,21 +38,28 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
const (
flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp"
flagNoWindow = "no-window"
flagNonInteractive = "noninteractive"
)
func New(base *base.Base) *cli.App { func New(base *base.Base) *cli.App {
app := base.NewApp(run) app := base.NewApp(run)
app.Flags = append(app.Flags, []cli.Flag{ app.Flags = append(app.Flags, []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "log-imap", Name: flagLogIMAP,
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"}, Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "log-smtp", Name: flagLogSMTP,
Usage: "Enable logging of SMTP communications (may contain decrypted data!)"}, Usage: "Enable logging of SMTP communications (may contain decrypted data!)"},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "no-window", Name: flagNoWindow,
Usage: "Don't show window after start"}, Usage: "Don't show window after start"},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "noninteractive", Name: flagNonInteractive,
Usage: "Start Bridge entirely noninteractively"}, Usage: "Start Bridge entirely noninteractively"},
}...) }...)
@ -64,8 +71,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Failed to load TLS config") logrus.WithError(err).Fatal("Failed to load TLS config")
} }
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.SentryReporter, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, bridge) imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, bridge)
smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge) smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge)
@ -79,9 +85,9 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
imapPort := b.Settings.GetInt(settings.IMAPPortKey) imapPort := b.Settings.GetInt(settings.IMAPPortKey)
imap.NewIMAPServer( imap.NewIMAPServer(
b.CrashHandler, b.CrashHandler,
c.String("log-imap") == "client" || c.String("log-imap") == "all", c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
c.String("log-imap") == "server" || c.String("log-imap") == "all", c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
imapPort, tlsConfig, imapBackend, b.Listener).ListenAndServe() imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe()
}() }()
go func() { go func() {
@ -89,12 +95,12 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
smtpPort := b.Settings.GetInt(settings.SMTPPortKey) smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
useSSL := b.Settings.GetBool(settings.SMTPSSLKey) useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
smtp.NewSMTPServer( smtp.NewSMTPServer(
c.Bool("log-smtp"), c.Bool(flagLogSMTP),
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe() smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
}() }()
// Bridge supports no-window option which we should use for autostart. // Bridge supports no-window option which we should use for autostart.
b.Autostart.Exec = append(b.Autostart.Exec, "--no-window") b.Autostart.Exec = append(b.Autostart.Exec, "--"+flagNoWindow)
// We want to remove old versions if the app exits successfully. // We want to remove old versions if the app exits successfully.
b.AddTeardownAction(b.Versioner.RemoveOldVersions) b.AddTeardownAction(b.Versioner.RemoveOldVersions)
@ -105,9 +111,9 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
var frontendMode string var frontendMode string
switch { switch {
case c.Bool("cli"): case c.Bool(base.FlagCLI):
frontendMode = "cli" frontendMode = "cli"
case c.Bool("noninteractive"): case c.Bool(flagNonInteractive):
return <-(make(chan error)) // Block forever. return <-(make(chan error)) // Block forever.
default: default:
frontendMode = "qt" frontendMode = "qt"
@ -118,12 +124,13 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
constants.BuildVersion, constants.BuildVersion,
b.Name, b.Name,
frontendMode, frontendMode,
!c.Bool("no-window"), !c.Bool(flagNoWindow),
b.CrashHandler, b.CrashHandler,
b.Locations, b.Locations,
b.Settings, b.Settings,
b.Listener, b.Listener,
b.Updater, b.Updater,
b.UserAgent,
bridge, bridge,
smtpBackend, smtpBackend,
b.Autostart, b.Autostart,
@ -132,7 +139,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
// Watch for updates routine // Watch for updates routine
go func() { go func() {
ticker := time.NewTicker(time.Hour) ticker := time.NewTicker(constants.UpdateCheckInterval)
for { for {
checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey)) checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey))

View File

@ -49,7 +49,7 @@ func run(b *base.Base, c *cli.Context) error {
var frontendMode string var frontendMode string
switch { switch {
case c.Bool("cli"): case c.Bool(base.FlagCLI):
frontendMode = "cli" frontendMode = "cli"
default: default:
frontendMode = "qt" frontendMode = "qt"

View File

@ -26,6 +26,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -46,16 +47,13 @@ type Bridge struct {
clientManager users.ClientManager clientManager users.ClientManager
updater Updater updater Updater
versioner Versioner versioner Versioner
userAgentClientName string
userAgentClientVersion string
userAgentOS string
} }
func New( func New(
locations Locator, locations Locator,
cache Cacher, cache Cacher,
s SettingsProvider, s SettingsProvider,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, clientManager users.ClientManager,
@ -69,7 +67,7 @@ func New(
clientManager.AllowProxy() clientManager.AllowProxy()
} }
storeFactory := newStoreFactory(cache, panicHandler, clientManager, eventListener) storeFactory := newStoreFactory(cache, sentryReporter, panicHandler, clientManager, eventListener)
u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true) u := users.New(locations, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
b := &Bridge{ b := &Bridge{
Users: u, Users: u,
@ -118,40 +116,6 @@ func (b *Bridge) heartbeat() {
} }
} }
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
func (b *Bridge) GetCurrentClient() string {
res := b.userAgentClientName
if b.userAgentClientVersion != "" {
res = res + " " + b.userAgentClientVersion
}
return res
}
// SetCurrentClient updates client info (e.g. Thunderbird) and sets the user agent
// on pmapi. By default no client is used, IMAP has to detect it on first login.
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
b.userAgentClientName = clientName
b.userAgentClientVersion = clientVersion
b.updateUserAgent()
}
// SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
// `runtime.GOOS`, but this can be overridden in case of better detection.
func (b *Bridge) SetCurrentOS(os string) {
b.userAgentOS = os
b.updateUserAgent()
}
func (b *Bridge) updateUserAgent() {
logrus.
WithField("clientName", b.userAgentClientName).
WithField("clientVersion", b.userAgentClientVersion).
WithField("OS", b.userAgentOS).
Info("Updating user agent")
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
}
// ReportBug reports a new bug from the user. // ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error { func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
c := b.clientManager.GetAnonymousClient() c := b.clientManager.GetAnonymousClient()
@ -187,26 +151,41 @@ func (b *Bridge) GetUpdateChannel() updater.UpdateChannel {
// Downgrading to previous version (by switching from early to stable, for example) // Downgrading to previous version (by switching from early to stable, for example)
// requires clearing all data including update files due to possibility of // requires clearing all data including update files due to possibility of
// inconsistency between versions and absence of backwards migration scripts. // inconsistency between versions and absence of backwards migration scripts.
func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) error { func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) (needRestart bool, err error) {
b.settings.Set(settings.UpdateChannelKey, string(channel)) b.settings.Set(settings.UpdateChannelKey, string(channel))
version, err := b.updater.Check() version, err := b.updater.Check()
if err != nil { if err != nil {
return err return false, err
} }
if b.updater.IsDowngrade(version) { // We have to deal right away only with downgrade - that action needs to
if err := b.Users.ClearData(); err != nil { // clear data and updates, and install bridge right away. But regular
log.WithError(err).Error("Failed to clear data while downgrading channel") // upgrade can be leaved out for periodic check.
} if !b.updater.IsDowngrade(version) {
if err := b.locations.ClearUpdates(); err != nil { return false, nil
log.WithError(err).Error("Failed to clear updates while downgrading channel") }
}
if err := b.Users.ClearData(); err != nil {
log.WithError(err).Error("Failed to clear data while downgrading channel")
}
if err := b.locations.ClearUpdates(); err != nil {
log.WithError(err).Error("Failed to clear updates while downgrading channel")
} }
if err := b.updater.InstallUpdate(version); err != nil { if err := b.updater.InstallUpdate(version); err != nil {
return err return false, err
} }
return b.versioner.RemoveOtherVersions(version.Version) return true, b.versioner.RemoveOtherVersions(version.Version)
}
// GetKeychainApp returns current keychain helper.
func (b *Bridge) GetKeychainApp() string {
return b.settings.Get(settings.PreferredKeychainKey)
}
// SetKeychainApp sets current keychain helper.
func (b *Bridge) SetKeychainApp(helper string) {
b.settings.Set(settings.PreferredKeychainKey, helper)
} }

View File

@ -1,22 +0,0 @@
// 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/>.
// Code generated by ./credits.sh at Mon Feb 1 10:34:22 CET 2021. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -21,39 +21,42 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
) )
type storeFactory struct { type storeFactory struct {
cache Cacher cache Cacher
panicHandler users.PanicHandler sentryReporter *sentry.Reporter
clientManager users.ClientManager panicHandler users.PanicHandler
eventListener listener.Listener clientManager users.ClientManager
storeCache *store.Cache eventListener listener.Listener
storeCache *store.Cache
} }
func newStoreFactory( func newStoreFactory(
cache Cacher, cache Cacher,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
clientManager users.ClientManager, clientManager users.ClientManager,
eventListener listener.Listener, eventListener listener.Listener,
) *storeFactory { ) *storeFactory {
return &storeFactory{ return &storeFactory{
cache: cache, cache: cache,
panicHandler: panicHandler, sentryReporter: sentryReporter,
clientManager: clientManager, panicHandler: panicHandler,
eventListener: eventListener, clientManager: clientManager,
storeCache: store.NewCache(cache.GetIMAPCachePath()), eventListener: eventListener,
storeCache: store.NewCache(cache.GetIMAPCachePath()),
} }
} }
// New creates new store for given user. // New creates new store for given user.
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) { func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
storePath := getUserStorePath(f.cache.GetDBDir(), user.ID()) storePath := getUserStorePath(f.cache.GetDBDir(), user.ID())
return store.New(f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache) return store.New(f.sentryReporter, f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache)
} }
// Remove removes all store files for given user. // Remove removes all store files for given user.

View File

@ -21,6 +21,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strconv" "strconv"
"sync" "sync"
@ -73,13 +74,12 @@ func (p *keyValueStore) save() error {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
f, err := os.Create(p.path) b, err := json.MarshalIndent(p.cache, "", "\t")
if err != nil { if err != nil {
return err return err
} }
defer f.Close() //nolint[errcheck]
return json.NewEncoder(f).Encode(p.cache) return ioutil.WriteFile(p.path, b, 0600)
} }
func (p *keyValueStore) setDefault(key, value string) { func (p *keyValueStore) setDefault(key, value string) {

View File

@ -72,20 +72,20 @@ func TestKeyValueStoreSetDefault(t *testing.T) {
func TestKeyValueStoreSet(t *testing.T) { func TestKeyValueStoreSet(t *testing.T) {
pref := newTestEmptyKeyValueStore(t) pref := newTestEmptyKeyValueStore(t)
pref.Set("str", "value") pref.Set("str", "value")
checkSavedKeyValueStore(t, "{\"str\":\"value\"}") checkSavedKeyValueStore(t, "{\n\t\"str\": \"value\"\n}")
} }
func TestKeyValueStoreSetInt(t *testing.T) { func TestKeyValueStoreSetInt(t *testing.T) {
pref := newTestEmptyKeyValueStore(t) pref := newTestEmptyKeyValueStore(t)
pref.SetInt("int", 42) pref.SetInt("int", 42)
checkSavedKeyValueStore(t, "{\"int\":\"42\"}") checkSavedKeyValueStore(t, "{\n\t\"int\": \"42\"\n}")
} }
func TestKeyValueStoreSetBool(t *testing.T) { func TestKeyValueStoreSetBool(t *testing.T) {
pref := newTestEmptyKeyValueStore(t) pref := newTestEmptyKeyValueStore(t)
pref.SetBool("trueBool", true) pref.SetBool("trueBool", true)
pref.SetBool("falseBool", false) pref.SetBool("falseBool", false)
checkSavedKeyValueStore(t, "{\"falseBool\":\"false\",\"trueBool\":\"true\"}") checkSavedKeyValueStore(t, "{\n\t\"falseBool\": \"false\",\n\t\"trueBool\": \"true\"\n}")
} }
func newTestEmptyKeyValueStore(t *testing.T) *keyValueStore { func newTestEmptyKeyValueStore(t *testing.T) *keyValueStore {
@ -101,5 +101,5 @@ func newTestKeyValueStore(t *testing.T) *keyValueStore {
func checkSavedKeyValueStore(t *testing.T, expected string) { func checkSavedKeyValueStore(t *testing.T, expected string) {
data, err := ioutil.ReadFile(testPrefFilePath) data, err := ioutil.ReadFile(testPrefFilePath)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected+"\n", string(data)) require.Equal(t, expected, string(data))
} }

View File

@ -25,29 +25,27 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
) )
// IsCatalinaOrNewer checks that host is MacOS Catalina 10.15.x or higher. // IsCatalinaOrNewer checks whether host is MacOS Catalina 10.15.x or higher.
func IsCatalinaOrNewer() bool { func IsCatalinaOrNewer() bool {
if runtime.GOOS != "darwin" { if runtime.GOOS != "darwin" {
return false return false
} }
return isVersionCatalinaOrNewer(getMacVersion())
}
func getMacVersion() string { rawVersion, err := exec.Command("sw_vers", "-productVersion").Output()
out, err := exec.Command("sw_vers", "-productVersion").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func isVersionCatalinaOrNewer(version string) bool {
v, err := semver.NewVersion(version)
if err != nil { if err != nil {
return false return false
} }
catalina := semver.MustParse("10.15.0") return isVersionCatalinaOrNewer(strings.TrimSpace(string(rawVersion)))
return v.GreaterThan(catalina) || v.Equal(catalina) }
func isVersionCatalinaOrNewer(rawVersion string) bool {
semVersion, err := semver.NewVersion(rawVersion)
if err != nil {
return false
}
minVersion := semver.MustParse("10.15.0")
return semVersion.GreaterThan(minVersion) || semVersion.Equal(minVersion)
} }

View File

@ -15,38 +15,45 @@
// 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/>.
package pmapi package useragent
import ( import (
"fmt" "fmt"
"regexp"
"runtime" "runtime"
"strings"
) )
// removeBrackets handle unwanted brackets in client identification string and join with given joinBy parameter. type UserAgent struct {
// Mac OS X Mail/13.0 (3601.0.4) -> Mac OS X Mail/13.0-3601.0.4 (joinBy = "-") client, platform string
func removeBrackets(s string, joinBy string) (r string) {
r = strings.ReplaceAll(s, " (", joinBy)
r = strings.ReplaceAll(r, "(", joinBy) // Should be faster than regex.
r = strings.ReplaceAll(r, ")", "")
return
} }
func formatUserAgent(clientName, clientVersion, os string) string { func New() *UserAgent {
client := "" return &UserAgent{
if clientName != "" { client: "",
client = removeBrackets(clientName, "-") platform: runtime.GOOS,
if clientVersion != "" {
client += "/" + removeBrackets(clientVersion, "-")
}
} }
}
if os == "" {
os = runtime.GOOS func (ua *UserAgent) SetClient(name, version string) {
} ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2"))
}
os = removeBrackets(os, " ")
func (ua *UserAgent) HasClient() bool {
return fmt.Sprintf("%s (%s)", client, os) return ua.client != ""
}
func (ua *UserAgent) SetPlatform(platform string) {
ua.platform = platform
}
func (ua *UserAgent) String() string {
var client string
if ua.client != "" {
client = ua.client
} else {
client = "NoClient/0.0.1"
}
return fmt.Sprintf("%v (%v)", client, ua.platform)
} }

View File

@ -0,0 +1,86 @@
// 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 useragent
import (
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserAgent(t *testing.T) {
tests := []struct {
name, version, platform string
want string
}{
// No name/version, no platform.
{
want: fmt.Sprintf("NoClient/0.0.1 (%v)", runtime.GOOS),
},
// No name/version, with platform.
{
platform: "macOS 10.15",
want: "NoClient/0.0.1 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Mac OS X Mail",
version: "1.0.0",
platform: "macOS 10.15",
want: "Mac OS X Mail/1.0.0 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Mac OS X Mail",
version: "13.4 (3608.120.23.2.4)",
platform: "macOS 10.15",
want: "Mac OS X Mail/13.4-3608.120.23.2.4 (macOS 10.15)",
},
// With name/version, with platform.
{
name: "Thunderbird",
version: "78.6.1",
platform: "Windows 10 (10.0)",
want: "Thunderbird/78.6.1 (Windows 10 (10.0))",
},
}
for _, test := range tests {
test := test
t.Run(test.want, func(t *testing.T) {
ua := New()
if test.name != "" && test.version != "" {
ua.SetClient(test.name, test.version)
}
if test.platform != "" {
ua.SetPlatform(test.platform)
}
assert.Equal(t, test.want, ua.String())
})
}
}

View File

@ -0,0 +1,28 @@
// 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/>.
// +build !build_qa
package constants
import "time"
// nolint[gochecknoglobals]
var (
// UpdateCheckInterval defines how often we check for new version
UpdateCheckInterval = time.Hour //nolint[gochecknoglobals]
)

View File

@ -0,0 +1,28 @@
// 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/>.
// +build build_qa
package constants
import "time"
// nolint[gochecknoglobals]
var (
// UpdateCheckInterval defines how often we check for new version
UpdateCheckInterval = time.Duration(5 * time.Minute)
)

View File

@ -19,7 +19,7 @@
package crash package crash
import ( import (
"github.com/ProtonMail/proton-bridge/pkg/sentry" "github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )

View File

@ -102,10 +102,6 @@ func New( //nolint[funlen]
Aliases: []string{"p"}, Aliases: []string{"p"},
Func: fe.changePort, Func: fe.changePort,
}) })
changeCmd.AddCmd(&ishell.Cmd{Name: "proxy",
Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.toggleAllowProxy,
})
changeCmd.AddCmd(&ishell.Cmd{Name: "smtp-security", changeCmd.AddCmd(&ishell.Cmd{Name: "smtp-security",
Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)", Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)",
Aliases: []string{"ssl", "starttls"}, Aliases: []string{"ssl", "starttls"},
@ -113,13 +109,56 @@ func New( //nolint[funlen]
}) })
fe.AddCmd(changeCmd) fe.AddCmd(changeCmd)
// DoH commands.
dohCmd := &ishell.Cmd{Name: "proxy",
Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked",
}
dohCmd.AddCmd(&ishell.Cmd{Name: "allow",
Help: "allow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.allowProxy,
})
dohCmd.AddCmd(&ishell.Cmd{Name: "disallow",
Help: "disallow bridge to securely connect to proton via a third party when it is being blocked",
Func: fe.disallowProxy,
})
fe.AddCmd(dohCmd)
// Updates commands.
updatesCmd := &ishell.Cmd{Name: "updates",
Help: "manage bridge updates",
}
updatesCmd.AddCmd(&ishell.Cmd{Name: "check",
Help: "check for Bridge updates",
Func: fe.checkUpdates,
})
autoUpdatesCmd := &ishell.Cmd{Name: "autoupdates",
Help: "manage bridge updates",
}
updatesCmd.AddCmd(autoUpdatesCmd)
autoUpdatesCmd.AddCmd(&ishell.Cmd{Name: "enable",
Help: "automatically keep bridge up to date",
Func: fe.enableAutoUpdates,
})
autoUpdatesCmd.AddCmd(&ishell.Cmd{Name: "disable",
Help: "require bridge to be manually updated",
Func: fe.disableAutoUpdates,
})
updatesChannelCmd := &ishell.Cmd{Name: "channel",
Help: "switch updates channel",
}
updatesCmd.AddCmd(updatesChannelCmd)
updatesChannelCmd.AddCmd(&ishell.Cmd{Name: "early",
Help: "switch to the early-access updates channel",
Func: fe.selectEarlyChannel,
})
updatesChannelCmd.AddCmd(&ishell.Cmd{Name: "stable",
Help: "switch to the stable updates channel",
Func: fe.selectStableChannel,
})
fe.AddCmd(updatesCmd)
// Check commands. // Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."} checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
Help: "check for Bridge updates. (aliases: u, v, version)",
Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates,
})
checkCmd.AddCmd(&ishell.Cmd{Name: "internet", checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)", Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"}, Aliases: []string{"i", "con", "connection"},

View File

@ -132,19 +132,31 @@ func (f *frontendCLI) changePort(c *ishell.Context) {
} }
} }
func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) { func (f *frontendCLI) allowProxy(c *ishell.Context) {
if f.settings.GetBool(settings.AllowProxyKey) { if f.settings.GetBool(settings.AllowProxyKey) {
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.") f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") { return
f.settings.SetBool(settings.AllowProxyKey, false) }
f.bridge.DisallowProxy()
} f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
} else {
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.") if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") { f.settings.SetBool(settings.AllowProxyKey, true)
f.settings.SetBool(settings.AllowProxyKey, true) f.bridge.AllowProxy()
f.bridge.AllowProxy() }
} }
func (f *frontendCLI) disallowProxy(c *ishell.Context) {
if !f.settings.GetBool(settings.AllowProxyKey) {
f.Println("Bridge is already set to NOT use alternative routing to connect to Proton if it is being blocked.")
return
}
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.settings.SetBool(settings.AllowProxyKey, false)
f.bridge.DisallowProxy()
} }
} }

View File

@ -21,11 +21,23 @@ import (
"strings" "strings"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/abiosoft/ishell" "github.com/abiosoft/ishell"
) )
func (f *frontendCLI) checkUpdates(c *ishell.Context) { func (f *frontendCLI) checkUpdates(c *ishell.Context) {
f.Println("Your version is up to date.") version, err := f.updater.Check()
if err != nil {
f.Println("An error occurred while checking for updates.")
return
}
if f.updater.IsUpdateApplicable(version) {
f.Println("An update is available.")
} else {
f.Println("Your version is up to date.")
}
} }
func (f *frontendCLI) printCredits(c *ishell.Context) { func (f *frontendCLI) printCredits(c *ishell.Context) {
@ -33,3 +45,68 @@ func (f *frontendCLI) printCredits(c *ishell.Context) {
f.Println(pkg) f.Println(pkg)
} }
} }
func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) {
if f.settings.GetBool(settings.AutoUpdateKey) {
f.Println("Bridge is already set to automatically install updates.")
return
}
f.Println("Bridge is currently set to NOT automatically install updates.")
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
f.settings.SetBool(settings.AutoUpdateKey, true)
}
}
func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) {
if !f.settings.GetBool(settings.AutoUpdateKey) {
f.Println("Bridge is already set to NOT automatically install updates.")
return
}
f.Println("Bridge is currently set to automatically install updates.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.settings.SetBool(settings.AutoUpdateKey, false)
}
}
func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) {
if f.bridge.GetUpdateChannel() == updater.EarlyChannel {
f.Println("Bridge is already on the early-access update channel.")
return
}
f.Println("Bridge is currently on the stable update channel.")
if f.yesNoQuestion("Are you sure you want to switch to the early-access update channel") {
needRestart, err := f.bridge.SetUpdateChannel(updater.EarlyChannel)
if err != nil {
f.Println("There was a problem switching update channel.")
}
if needRestart {
f.restarter.SetToRestart()
}
}
}
func (f *frontendCLI) selectStableChannel(c *ishell.Context) {
if f.bridge.GetUpdateChannel() == updater.StableChannel {
f.Println("Bridge is already on the stable update channel.")
return
}
f.Println("Bridge is currently on the early-access update channel.")
f.Println("Switching to the stable channel may reset all data!")
if f.yesNoQuestion("Are you sure you want to switch to the stable update channel") {
needRestart, err := f.bridge.SetUpdateChannel(updater.StableChannel)
if err != nil {
f.Println("There was a problem switching update channel.")
}
if needRestart {
f.restarter.SetToRestart()
}
}
}

View File

@ -22,6 +22,7 @@ import (
"github.com/ProtonMail/go-autostart" "github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli" "github.com/ProtonMail/proton-bridge/internal/frontend/cli"
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie" cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt" "github.com/ProtonMail/proton-bridge/internal/frontend/qt"
@ -60,6 +61,7 @@ func New(
settings *settings.Settings, settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updater types.Updater,
userAgent *useragent.UserAgent,
bridge *bridge.Bridge, bridge *bridge.Bridge,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App, autostart *autostart.App,
@ -77,6 +79,7 @@ func New(
settings, settings,
eventListener, eventListener,
updater, updater,
userAgent,
bridgeWrap, bridgeWrap,
noEncConfirmator, noEncConfirmator,
autostart, autostart,
@ -95,6 +98,7 @@ func newBridgeFrontend(
settings *settings.Settings, settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger, bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App, autostart *autostart.App,
@ -122,6 +126,7 @@ func newBridgeFrontend(
settings, settings,
eventListener, eventListener,
updater, updater,
userAgent,
bridge, bridge,
noEncConfirmator, noEncConfirmator,
autostart, autostart,

View File

@ -0,0 +1,194 @@
// 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/>.
// Change default keychain dialog
import QtQuick 2.8
import BridgeUI 1.0
import ProtonUI 1.0
import QtQuick.Controls 2.2 as QC
import QtQuick.Layouts 1.0
Dialog {
id: root
title : "Change which keychain Bridge uses as default"
subtitle : "Select which keychain is used (Bridge will automatically restart)"
isDialogBusy: currentIndex==1
property var selectedKeychain
Connections {
target: go.selectedKeychain
onValueChanged: {
console.debug("go.selectedKeychain == ", go.selectedKeychain)
}
}
ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.minimumHeight: root.titleHeight + Style.dialog.heightSeparator
Layout.maximumHeight: root.titleHeight + Style.dialog.heightSeparator
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ColumnLayout {
anchors.centerIn: parent
Repeater {
id: keychainRadioButtons
model: go.availableKeychain
QC.RadioButton {
id: radioDelegate
text: modelData
checked: go.selectedKeychain === modelData
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: Style.main.spacing
indicator: Text {
text : radioDelegate.checked ? Style.fa.check_circle : Style.fa.circle_o
color : radioDelegate.checked ? Style.main.textBlue : Style.main.textInactive
font {
pointSize: Style.dialog.iconSize * Style.pt
family: Style.fontawesome.name
}
}
contentItem: Text {
text: radioDelegate.text
color: Style.main.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: checked
}
horizontalAlignment : Text.AlignHCenter
verticalAlignment : Text.AlignVCenter
leftPadding: Style.dialog.iconSize
}
onCheckedChanged: {
if (checked) {
root.selectedKeychain = modelData
}
}
}
}
Item {
Layout.fillWidth: true
Layout.minimumHeight: Style.dialog.heightSeparator
Layout.maximumHeight: Style.dialog.heightSeparator
}
Row {
id: buttonRow
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main: Style.dialog.text
fa_icon: Style.fa.times
text: qsTr("Cancel", "dismisses current action")
onClicked : root.hide()
}
ButtonRounded {
id: buttonYes
color_main: Style.dialog.text
color_minor: Style.main.textBlue
isOpaque: true
fa_icon: Style.fa.check
text: qsTr("Okay", "confirms and dismisses a notification")
onClicked : root.confirmed()
}
}
}
}
}
ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.minimumHeight: root.titleHeight + Style.dialog.heightSeparator
Layout.maximumHeight: root.titleHeight + Style.dialog.heightSeparator
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Text {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width : parent.width/2
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
text : "Default keychain is now set to " + root.selectedKeychain +
"\n\n" +
qsTr("Settings will be applied after the next start.", "notification about setting being applied after next start") +
"\n\n" +
qsTr("Bridge will now restart.", "notification about restarting")
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
}
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
function confirmed() {
if (selectedKeychain === go.selectedKeychain) {
root.hide()
return
}
incrementCurrentIndex()
timer.start()
}
timer.interval : 5000
Connections {
target: timer
onTriggered: {
// This action triggers restart on the backend side.
go.selectedKeychain = selectedKeychain
}
}
}

View File

@ -229,7 +229,7 @@ Dialog {
currentIndex : 0 currentIndex : 0
title : qsTr("Clear cache", "title of page that displays during cache clearing") title : qsTr("Clear cache", "title of page that displays during cache clearing")
question : qsTr("Are you sure you want to clear your local cache?", "displays during cache clearing") question : qsTr("Are you sure you want to clear your local cache?", "displays during cache clearing")
note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, temporarily slowing down the email download process significantly.", "displays during cache clearing") note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, and requires you to reconfigure your client.", "displays during cache clearing")
answer : qsTr("Clearing the cache ...", "displays during cache clearing") answer : qsTr("Clearing the cache ...", "displays during cache clearing")
} }
}, },
@ -310,7 +310,7 @@ Dialog {
target: root target: root
currentIndex : 0 currentIndex : 0
question : qsTr("Are you sure you want to leave early access? Please keep in mind this operation clears the cache and restarts Bridge.") question : qsTr("Are you sure you want to leave early access? Please keep in mind this operation clears the cache and restarts Bridge.")
note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, temporarily slowing down the email download process significantly.") note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, and requires you to reconfigure your client.")
title : qsTr("Disable early access") title : qsTr("Disable early access")
answer : qsTr("Disabling early access...") answer : qsTr("Disabling early access...")
} }

View File

@ -303,6 +303,10 @@ Window {
id: dialogChangePort id: dialogChangePort
} }
DialogKeychainChange {
id: dialogChangeKeychain
}
DialogConnectionTroubleshoot { DialogConnectionTroubleshoot {
id: dialogConnectionTroubleshoot id: dialogConnectionTroubleshoot
} }

View File

@ -239,6 +239,25 @@ Item {
dialogGlobal.show() dialogGlobal.show()
} }
} }
ButtonIconText {
id: changeKeychain
visible: advancedSettings.isAdvanced && (go.availableKeychain.length > 1)
text: qsTr("Change keychain", "button to open dialog with default keychain selection")
leftIcon.text : Style.fa.key
rightIcon {
text : qsTr("Change", "clickable link next to change keychain button in settings")
color: Style.main.text
font {
family : changeKeychain.font.family // use default font, not font-awesome
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogChangeKeychain.show()
}
}
} }
} }
} }

View File

@ -2,6 +2,7 @@ module BridgeUI
AccountDelegate 1.0 AccountDelegate.qml AccountDelegate 1.0 AccountDelegate.qml
Credits 1.0 Credits.qml Credits 1.0 Credits.qml
DialogFirstStart 1.0 DialogFirstStart.qml DialogFirstStart 1.0 DialogFirstStart.qml
DialogKeychainChange 1.0 DialogKeychainChange.qml
DialogPortChange 1.0 DialogPortChange.qml DialogPortChange 1.0 DialogPortChange.qml
DialogYesNo 1.0 DialogYesNo.qml DialogYesNo 1.0 DialogYesNo.qml
DialogTLSCertInfo 1.0 DialogTLSCertInfo.qml DialogTLSCertInfo 1.0 DialogTLSCertInfo.qml

View File

@ -107,17 +107,53 @@ Item {
gui.openMainWindow(false) gui.openMainWindow(false)
if (go.isConnectionOK) { if (go.isConnectionOK) {
if( winMain.updateState=="noInternet") { if( winMain.updateState=="noInternet") {
go.setUpdateState("upToDate") go.updateState = "upToDate"
} }
} else { } else {
go.setUpdateState("noInternet") go.updateState = "noInternet"
} }
} }
onSetUpdateState : { onUpdateStateChanged : {
// Update tray icon if needed
switch (go.updateState) {
case "internetCheck":
break;
case "noInternet" :
gui.warningFlags |= Style.warnInfoBar
break;
case "oldVersion":
gui.warningFlags |= Style.warnInfoBar
break;
case "forceUpdate":
// Force update should presist once it happened and never be overwritten.
// That means that tray icon should allways remain in error state.
// But since we have only two sources of error icon in tray (force update
// + installation fail) and both are unrecoverable and we do not ever remove
// error flag from gui.warningFlags - it is ok to rely on gui.warningFlags and
// not on winMain.updateState (which presist forceUpdate)
gui.warningFlags |= Style.errorInfoBar
break;
case "upToDate":
gui.warningFlags &= ~Style.warnInfoBar
break;
case "updateRestart":
gui.warningFlags |= Style.warnInfoBar
break;
case "updateError":
gui.warningFlags |= Style.errorInfoBar
break;
default :
break;
}
// if main window is closed - most probably it is destroyed (see closeMainWindow())
if (winMain == null) {
return
}
// once app is outdated prevent from state change // once app is outdated prevent from state change
if (winMain.updateState != "forceUpdate") { if (winMain.updateState != "forceUpdate") {
winMain.updateState = updateState winMain.updateState = go.updateState
} }
} }
@ -129,15 +165,14 @@ Item {
} }
onNotifyManualUpdate: { onNotifyManualUpdate: {
go.setUpdateState("oldVersion") go.updateState = "oldVersion"
} }
onNotifyManualUpdateRestartNeeded: { onNotifyManualUpdateRestartNeeded: {
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
go.setUpdateState("updateRestart") go.updateState = "updateRestart"
winMain.dialogUpdate.finished(false) winMain.dialogUpdate.finished(false)
// after manual update - just retart immidiatly // after manual update - just retart immidiatly
@ -147,28 +182,25 @@ Item {
onNotifyManualUpdateError: { onNotifyManualUpdateError: {
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
go.setUpdateState("updateError") go.updateState = "updateError"
winMain.dialogUpdate.finished(true) winMain.dialogUpdate.finished(true)
} }
onNotifyForceUpdate : { onNotifyForceUpdate : {
go.setUpdateState("forceUpdate") go.updateState = "forceUpdate"
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
} }
onNotifySilentUpdateRestartNeeded: { onNotifySilentUpdateRestartNeeded: {
go.setUpdateState("updateRestart") go.updateState = "updateRestart"
} }
onNotifySilentUpdateError: { onNotifySilentUpdateError: {
go.setUpdateState("updateError") go.updateState = "updateError"
gui.openMainWindow(true)
} }
onNotifyLogout : { onNotifyLogout : {
@ -287,9 +319,17 @@ Item {
if (showAndRise) { if (showAndRise) {
gui.winMain.showAndRise() gui.winMain.showAndRise()
} }
// restore update notification bar: trigger updateStateChanged
var tmp = go.updateState
go.updateState = ""
go.updateState = tmp
} }
function closeMainWindow () { function closeMainWindow () {
// Historical reasons: once upon a time there was a report about high GPU
// usage on MacOS while bridge is closed. Legends say that destroying
// MainWindow solved this.
gui.winMain.hide() gui.winMain.hide()
gui.winMain.destroy(5000) gui.winMain.destroy(5000)
gui.winMain = null gui.winMain = null

View File

@ -30,7 +30,6 @@ Item {
id: gui id: gui
property alias winMain: winMain property alias winMain: winMain
property bool isFirstWindow: true property bool isFirstWindow: true
property int warningFlags: 0
property var locale : Qt.locale("en_US") property var locale : Qt.locale("en_US")
property date netBday : new Date("1989-03-13T00:00:00") property date netBday : new Date("1989-03-13T00:00:00")
@ -96,17 +95,17 @@ Item {
go.isConnectionOK = isAvailable go.isConnectionOK = isAvailable
if (go.isConnectionOK) { if (go.isConnectionOK) {
if( winMain.updateState==gui.enums.statusNoInternet) { if( winMain.updateState==gui.enums.statusNoInternet) {
go.setUpdateState(gui.enums.statusUpToDate) go.updateState = gui.enums.statusUpToDate
} }
} else { } else {
go.setUpdateState(gui.enums.statusNoInternet) go.updateState = gui.enums.statusNoInternet
} }
} }
onSetUpdateState : { onUpdateStateChanged : {
// once app is outdated prevent from state change // once app is outdated prevent from state change
if (winMain.updateState != "forceUpdate") { if (winMain.updateState != "forceUpdate") {
winMain.updateState = updateState winMain.updateState = go.updateState
} }
} }
@ -207,14 +206,14 @@ Item {
} }
onNotifyManualUpdate: { onNotifyManualUpdate: {
go.setUpdateState("oldVersion") go.updateState = "oldVersion"
} }
onNotifyManualUpdateRestartNeeded: { onNotifyManualUpdateRestartNeeded: {
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
go.setUpdateState("updateRestart") go.updateState = "updateRestart"
winMain.dialogUpdate.finished(false) winMain.dialogUpdate.finished(false)
// after manual update - just retart immidiatly // after manual update - just retart immidiatly
@ -226,23 +225,23 @@ Item {
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
go.setUpdateState("updateError") go.updateState = "updateError"
winMain.dialogUpdate.finished(true) winMain.dialogUpdate.finished(true)
} }
onNotifyForceUpdate : { onNotifyForceUpdate : {
go.setUpdateState("forceUpdate") go.updateState = "forceUpdate"
if (!winMain.dialogUpdate.visible) { if (!winMain.dialogUpdate.visible) {
winMain.dialogUpdate.show() winMain.dialogUpdate.show()
} }
} }
onNotifySilentUpdateRestartNeeded: { onNotifySilentUpdateRestartNeeded: {
go.setUpdateState("updateRestart") go.updateState = "updateRestart"
} }
onNotifySilentUpdateError: { onNotifySilentUpdateError: {
go.setUpdateState("updateError") go.updateState = "updateError"
} }
onNotifyLogout : { onNotifyLogout : {

View File

@ -111,6 +111,11 @@ StackLayout {
Accessible.description: title Accessible.description: title
Accessible.focusable: true Accessible.focusable: true
onVisibleChanged: {
if (background.visible != visible) {
background.visible = visible
}
}
visible : false visible : false
anchors { anchors {

View File

@ -66,14 +66,15 @@ Rectangle {
ClickIconText { ClickIconText {
id: linkText id: linkText
anchors.verticalCenter : message.verticalCenter anchors.verticalCenter : message.verticalCenter
iconText : "" iconText : " "
fontSize : root.fontSize fontSize : root.fontSize
textUnderline: true
} }
ClickIconText { ClickIconText {
id: actionText id: actionText
anchors.verticalCenter : message.verticalCenter anchors.verticalCenter : message.verticalCenter
iconText : "" iconText : " "
fontSize : root.fontSize fontSize : root.fontSize
textUnderline: true textUnderline: true
} }
@ -107,31 +108,25 @@ Rectangle {
onStateChanged : { onStateChanged : {
switch (root.state) { switch (root.state) {
case "internetCheck": case "internetCheck":
break; break;
case "noInternet" : case "noInternet" :
gui.warningFlags |= Style.warnInfoBar retryInternet.start()
retryInternet.start() secLeft=checkInterval[iTry]
secLeft=checkInterval[iTry] break;
break;
case "oldVersion": case "oldVersion":
gui.warningFlags |= Style.warnInfoBar break;
break;
case "forceUpdate": case "forceUpdate":
gui.warningFlags |= Style.errorInfoBar break;
break;
case "upToDate": case "upToDate":
gui.warningFlags &= ~Style.warnInfoBar iTry = 0
iTry = 0 secLeft=checkInterval[iTry]
secLeft=checkInterval[iTry] break;
break;
case "updateRestart": case "updateRestart":
gui.warningFlags |= Style.warnInfoBar break;
break;
case "updateError": case "updateError":
gui.warningFlags |= Style.errorInfoBar break;
break;
default : default :
break; break;
} }
if (root.state!="noInternet") { if (root.state!="noInternet") {
@ -247,7 +242,7 @@ Rectangle {
PropertyChanges { PropertyChanges {
target: linkText target: linkText
visible: true visible: true
text: "(" + qsTr("view release notes", "display the release notes from the new version") + ")" text: qsTr("Release Notes", "display the release notes from the new version")
onClicked: gui.openReleaseNotes() onClicked: gui.openReleaseNotes()
} }
PropertyChanges { PropertyChanges {
@ -270,7 +265,7 @@ Rectangle {
target: closeSign target: closeSign
visible: true visible: true
onClicked: { onClicked: {
root.state = "upToDate" go.updateState = "upToDate"
} }
} }
}, },

View File

@ -24,7 +24,7 @@ import QtQuick.Window 2.2
Window { Window {
id: testroot id: testroot
width : 150 width : 250
height : 600 height : 600
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
visible : true visible : true
@ -60,7 +60,7 @@ Window {
Text { Text {
id: systrText id: systrText
anchors { anchors {
right : test_systray.right horizontalCenter: parent.horizontalCenter
verticalCenter: test_systray.verticalCenter verticalCenter: test_systray.verticalCenter
} }
text: "unset" text: "unset"
@ -281,6 +281,9 @@ Window {
property bool hasNoKeychain : true property bool hasNoKeychain : true
property var availableKeychain: ["pass-app", "gnome-keyring"]
property var selectedKeychain: "gnome-keyring"
property string wrongCredentials property string wrongCredentials
property string wrongMailboxPassword property string wrongMailboxPassword
property string canNotReachAPI property string canNotReachAPI
@ -296,6 +299,7 @@ Window {
property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00" property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00"
property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;" property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;"
property string updateState
property string updateVersion : "QA.1.0" property string updateVersion : "QA.1.0"
property bool updateCanInstall: true property bool updateCanInstall: true
property string updateLandingPage : "https://protonmail.com/bridge/download/" property string updateLandingPage : "https://protonmail.com/bridge/download/"
@ -337,7 +341,6 @@ Window {
signal notifyPortIssue(bool busyPortIMAP, bool busyPortSMTP) signal notifyPortIssue(bool busyPortIMAP, bool busyPortSMTP)
signal notifyVersionIsTheLatest() signal notifyVersionIsTheLatest()
signal setUpdateState(string updateState)
signal notifyKeychainRebuild() signal notifyKeychainRebuild()
signal notifyHasNoKeychain() signal notifyHasNoKeychain()

View File

@ -856,6 +856,7 @@ Window {
property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00" property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00"
property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;" property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;"
property string updateState
property string updateVersion : "q0.1.0" property string updateVersion : "q0.1.0"
property bool updateCanInstall: true property bool updateCanInstall: true
property string updateLandingPage : "https://protonmail.com/import-export/download/" property string updateLandingPage : "https://protonmail.com/import-export/download/"
@ -900,7 +901,6 @@ Window {
signal showQuit() signal showQuit()
signal notifyVersionIsTheLatest() signal notifyVersionIsTheLatest()
signal setUpdateState(string updateState)
signal showMainWin() signal showMainWin()
signal hideMainWin() signal hideMainWin()

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtcommon package qtcommon

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtcommon package qtcommon

View File

@ -1,4 +1,4 @@
// +build !nogui // +build build_qt
#include "common.h" #include "common.h"
#include "_cgo_export.h" #include "_cgo_export.h"

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtcommon package qtcommon

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtcommon package qtcommon

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build nogui // +build !build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -16,7 +16,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qtie package qtie
@ -53,6 +53,7 @@ type GoQMLInterface struct {
_ string `property:"fullversion"` _ string `property:"fullversion"`
_ string `property:"downloadLink"` _ string `property:"downloadLink"`
_ string `property:"updateState"`
_ string `property:"updateVersion"` _ string `property:"updateVersion"`
_ bool `property:"updateCanInstall"` _ bool `property:"updateCanInstall"`
_ string `property:"updateLandingPage"` _ string `property:"updateLandingPage"`
@ -77,7 +78,6 @@ type GoQMLInterface struct {
_ string `property:"versionCheckFailed"` _ string `property:"versionCheckFailed"`
// //
_ func(isAvailable bool) `signal:"setConnectionStatus"` _ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func(updateState string) `signal:"setUpdateState"`
_ func() `slot:"checkInternet"` _ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt
@ -84,7 +84,7 @@ func (s *FrontendQt) clearCache() {
channel := s.bridge.GetUpdateChannel() channel := s.bridge.GetUpdateChannel()
if channel == updater.EarlyChannel { if channel == updater.EarlyChannel {
if err := s.bridge.SetUpdateChannel(updater.StableChannel); err != nil { if _, err := s.bridge.SetUpdateChannel(updater.StableChannel); err != nil {
s.Qml.NotifyManualUpdateError() s.Qml.NotifyManualUpdateError()
return return
} }

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
// Package qt is the Qt User interface for Desktop bridge. // Package qt is the Qt User interface for Desktop bridge.
// //
@ -39,16 +39,17 @@ import (
"github.com/ProtonMail/go-autostart" "github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig" "github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common" qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
"github.com/therecipe/qt/core" "github.com/therecipe/qt/core"
@ -74,6 +75,7 @@ type FrontendQt struct {
settings *settings.Settings settings *settings.Settings
eventListener listener.Listener eventListener listener.Listener
updater types.Updater updater types.Updater
userAgent *useragent.UserAgent
bridge types.Bridger bridge types.Bridger
noEncConfirmator types.NoEncConfirmator noEncConfirmator types.NoEncConfirmator
@ -113,12 +115,15 @@ func New(
settings *settings.Settings, settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger, bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App, autostart *autostart.App,
restarter types.Restarter, restarter types.Restarter,
) *FrontendQt { ) *FrontendQt {
tmp := &FrontendQt{ userAgent.SetPlatform(core.QSysInfo_PrettyProductName())
f := &FrontendQt{
version: version, version: version,
buildVersion: buildVersion, buildVersion: buildVersion,
programName: programName, programName: programName,
@ -128,6 +133,7 @@ func New(
settings: settings, settings: settings,
eventListener: eventListener, eventListener: eventListener,
updater: updater, updater: updater,
userAgent: userAgent,
bridge: bridge, bridge: bridge,
noEncConfirmator: noEncConfirmator, noEncConfirmator: noEncConfirmator,
programVer: "v" + version, programVer: "v" + version,
@ -137,13 +143,9 @@ func New(
// Initializing.Done is only called sync.Once. Please keep the increment // Initializing.Done is only called sync.Once. Please keep the increment
// set to 1 // set to 1
tmp.initializing.Add(1) f.initializing.Add(1)
// Nicer string for OS. return f
currentOS := core.QSysInfo_PrettyProductName()
bridge.SetCurrentOS(currentOS)
return tmp
} }
// InstanceExistAlert is a global warning window indicating an instance already exists. // InstanceExistAlert is a global warning window indicating an instance already exists.
@ -338,27 +340,35 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
s.Qml.SetCredits(bridge.Credits) s.Qml.SetCredits(bridge.Credits)
s.Qml.SetFullversion(s.buildVersion) s.Qml.SetFullversion(s.buildVersion)
// Autostart. // Autostart: rewrite the current definition of autostart
if s.Qml.IsFirstStart() { // - when it is the first time
if s.autostart.IsEnabled() { // - when starting after clear cache
// - when there is already autostart file from past
//
// This will make sure that autostart will use the latest path to
// launcher or bridge.
isAutoStartEnabled := s.autostart.IsEnabled()
if s.Qml.IsFirstStart() || isAutoStartEnabled {
if isAutoStartEnabled {
if err := s.autostart.Disable(); err != nil { if err := s.autostart.Disable(); err != nil {
log.Error("First disable ", err) log.
WithField("first", s.Qml.IsFirstStart()).
WithField("wasEnabled", isAutoStartEnabled).
WithError(err).
Error("Disable on start failed.")
s.autostartError(err) s.autostartError(err)
} }
} }
s.toggleAutoStart() if err := s.autostart.Enable(); err != nil {
} log.
if s.autostart.IsEnabled() { WithField("first", s.Qml.IsFirstStart()).
s.Qml.SetIsAutoStart(true) WithField("wasEnabled", isAutoStartEnabled).
} else { WithError(err).
s.Qml.SetIsAutoStart(false) Error("Enable on start failed.")
} s.autostartError(err)
}
if s.settings.GetBool(settings.AutoUpdateKey) {
s.Qml.SetIsAutoUpdate(true)
} else {
s.Qml.SetIsAutoUpdate(false)
} }
s.Qml.SetIsAutoStart(s.autostart.IsEnabled())
if s.settings.GetBool(settings.AllowProxyKey) { if s.settings.GetBool(settings.AllowProxyKey) {
s.Qml.SetIsProxyAllowed(true) s.Qml.SetIsProxyAllowed(true)
@ -372,6 +382,14 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
s.Qml.SetIsEarlyAccess(false) s.Qml.SetIsEarlyAccess(false)
} }
availableKeychain := []string{}
for chain := range keychain.Helpers {
availableKeychain = append(availableKeychain, chain)
}
s.Qml.SetAvailableKeychain(availableKeychain)
s.Qml.SetSelectedKeychain(s.settings.Get(settings.PreferredKeychainKey))
// Set reporting of outgoing email without encryption. // Set reporting of outgoing email without encryption.
s.Qml.SetIsReportingOutgoingNoEnc(s.settings.GetBool(settings.ReportOutgoingNoEncKey)) s.Qml.SetIsReportingOutgoingNoEnc(s.settings.GetBool(settings.ReportOutgoingNoEncKey))
@ -497,7 +515,7 @@ func (s *FrontendQt) sendBug(description, client, address string) (isOK bool) {
} }
func (s *FrontendQt) getLastMailClient() string { func (s *FrontendQt) getLastMailClient() string {
return s.bridge.GetCurrentClient() return s.userAgent.String()
} }
func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) { func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
@ -547,20 +565,22 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
func (s *FrontendQt) toggleAutoStart() { func (s *FrontendQt) toggleAutoStart() {
defer s.Qml.ProcessFinished() defer s.Qml.ProcessFinished()
var err error var err error
if s.autostart.IsEnabled() { wasEnabled := s.autostart.IsEnabled()
if wasEnabled {
err = s.autostart.Disable() err = s.autostart.Disable()
} else { } else {
err = s.autostart.Enable() err = s.autostart.Enable()
} }
isEnabled := s.autostart.IsEnabled()
if err != nil { if err != nil {
log.Error("Enable autostart: ", err) log.
WithField("wasEnabled", wasEnabled).
WithField("isEnabled", isEnabled).
WithError(err).
Error("Autostart change failed.")
s.autostartError(err) s.autostartError(err)
} }
if s.autostart.IsEnabled() { s.Qml.SetIsAutoStart(isEnabled)
s.Qml.SetIsAutoStart(true)
} else {
s.Qml.SetIsAutoStart(false)
}
} }
func (s *FrontendQt) toggleAutoUpdate() { func (s *FrontendQt) toggleAutoUpdate() {
@ -585,14 +605,16 @@ func (s *FrontendQt) toggleEarlyAccess() {
channel = updater.EarlyChannel channel = updater.EarlyChannel
} }
err := s.bridge.SetUpdateChannel(channel) needRestart, err := s.bridge.SetUpdateChannel(channel)
s.Qml.SetIsEarlyAccess(channel == updater.EarlyChannel) s.Qml.SetIsEarlyAccess(channel == updater.EarlyChannel)
if err != nil { if err != nil {
s.Qml.NotifyManualUpdateError() s.Qml.NotifyManualUpdateError()
return return
} }
s.restarter.SetToRestart() if needRestart {
s.App.Quit() s.restarter.SetToRestart()
s.App.Quit()
}
} }
func (s *FrontendQt) toggleAllowProxy() { func (s *FrontendQt) toggleAllowProxy() {
@ -711,3 +733,16 @@ func (s *FrontendQt) setGUIIsReady() {
s.initializing.Done() s.initializing.Done()
}) })
} }
func (s *FrontendQt) getKeychain() string {
return s.bridge.GetKeychainApp()
}
func (s *FrontendQt) setKeychain(keychain string) {
if keychain != s.bridge.GetKeychainApp() {
s.bridge.SetKeychainApp(keychain)
s.restarter.SetToRestart()
s.App.Quit()
}
}

View File

@ -15,7 +15,7 @@
// 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/>.
// +build nogui // +build !build_qt
package qt package qt
@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/go-autostart" "github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
@ -71,6 +72,7 @@ func New(
settings *settings.Settings, settings *settings.Settings,
eventListener listener.Listener, eventListener listener.Listener,
updater types.Updater, updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger, bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator, noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App, autostart *autostart.App,

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt

View File

@ -15,7 +15,7 @@
// 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/>.
// +build !nogui // +build build_qt
package qt package qt
@ -50,6 +50,7 @@ type GoQMLInterface struct {
_ string `property:"fullversion"` _ string `property:"fullversion"`
_ string `property:"downloadLink"` _ string `property:"downloadLink"`
_ string `property:"updateState"`
_ string `property:"updateVersion"` _ string `property:"updateVersion"`
_ bool `property:"updateCanInstall"` _ bool `property:"updateCanInstall"`
_ string `property:"updateLandingPage"` _ string `property:"updateLandingPage"`
@ -66,6 +67,9 @@ type GoQMLInterface struct {
_ func() `slot:"startManualUpdate"` _ func() `slot:"startManualUpdate"`
_ func() `slot:"guiIsReady"` _ func() `slot:"guiIsReady"`
_ []string `property:"availableKeychain"`
_ string `property:"selectedKeychain"`
// Translations. // Translations.
_ string `property:"wrongCredentials"` _ string `property:"wrongCredentials"`
_ string `property:"wrongMailboxPassword"` _ string `property:"wrongMailboxPassword"`
@ -79,9 +83,8 @@ type GoQMLInterface struct {
_ float32 `property:"progress"` _ float32 `property:"progress"`
_ string `property:"progressDescription"` _ string `property:"progressDescription"`
_ func(isAvailable bool) `signal:"setConnectionStatus"` _ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func(updateState string) `signal:"setUpdateState"` _ func() `slot:"checkInternet"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"` _ func() `slot:"setToRestart"`
@ -209,4 +212,7 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc) s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)
s.ConnectShouldSendAnswer(f.shouldSendAnswer) s.ConnectShouldSendAnswer(f.shouldSendAnswer)
s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord) s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord)
s.ConnectSetSelectedKeychain(f.setKeychain)
s.ConnectSelectedKeychain(f.getKeychain)
} }

View File

@ -61,6 +61,7 @@
<file alias="BubbleMenu.qml" >./qml/BridgeUI/BubbleMenu.qml</file> <file alias="BubbleMenu.qml" >./qml/BridgeUI/BubbleMenu.qml</file>
<file alias="Credits.qml" >./qml/BridgeUI/Credits.qml</file> <file alias="Credits.qml" >./qml/BridgeUI/Credits.qml</file>
<file alias="DialogFirstStart.qml" >./qml/BridgeUI/DialogFirstStart.qml</file> <file alias="DialogFirstStart.qml" >./qml/BridgeUI/DialogFirstStart.qml</file>
<file alias="DialogKeychainChange.qml" >./qml/BridgeUI/DialogKeychainChange.qml</file>
<file alias="DialogPortChange.qml" >./qml/BridgeUI/DialogPortChange.qml</file> <file alias="DialogPortChange.qml" >./qml/BridgeUI/DialogPortChange.qml</file>
<file alias="DialogYesNo.qml" >./qml/BridgeUI/DialogYesNo.qml</file> <file alias="DialogYesNo.qml" >./qml/BridgeUI/DialogYesNo.qml</file>
<file alias="DialogTLSCertInfo.qml" >./qml/BridgeUI/DialogTLSCertInfo.qml</file> <file alias="DialogTLSCertInfo.qml" >./qml/BridgeUI/DialogTLSCertInfo.qml</file>

View File

@ -75,13 +75,13 @@ type User interface {
type Bridger interface { type Bridger interface {
UserManager UserManager
GetCurrentClient() string
SetCurrentOS(os string)
ReportBug(osType, osVersion, description, accountName, address, emailClient string) error ReportBug(osType, osVersion, description, accountName, address, emailClient string) error
AllowProxy() AllowProxy()
DisallowProxy() DisallowProxy()
GetUpdateChannel() updater.UpdateChannel GetUpdateChannel() updater.UpdateChannel
SetUpdateChannel(updater.UpdateChannel) error SetUpdateChannel(updater.UpdateChannel) (needRestart bool, err error)
GetKeychainApp() string
SetKeychainApp(keychain string)
} }
type bridgeWrap struct { type bridgeWrap struct {

View File

@ -29,7 +29,6 @@ type cacheProvider interface {
} }
type bridger interface { type bridger interface {
SetCurrentClient(clientName, clientVersion string)
GetUser(query string) (bridgeUser, error) GetUser(query string) (bridgeUser, error)
} }

View File

@ -23,13 +23,13 @@ import (
) )
type currentClientSetter interface { type currentClientSetter interface {
SetCurrentClient(name, version string) SetClient(name, version string)
} }
// Extension for IMAP server // Extension for IMAP server
type extension struct { type extension struct {
extID imapserver.ConnExtension extID imapserver.ConnExtension
setter currentClientSetter clientSetter currentClientSetter
} }
func (ext *extension) Capabilities(conn imapserver.Conn) []string { func (ext *extension) Capabilities(conn imapserver.Conn) []string {
@ -44,8 +44,8 @@ func (ext *extension) Command(name string) imapserver.HandlerFactory {
return func() imapserver.Handler { return func() imapserver.Handler {
if hdlrID, ok := newIDHandler().(*imapid.Handler); ok { if hdlrID, ok := newIDHandler().(*imapid.Handler); ok {
return &handler{ return &handler{
hdlrID: hdlrID, hdlrID: hdlrID,
setter: ext.setter, clientSetter: ext.clientSetter,
} }
} }
return nil return nil
@ -57,8 +57,8 @@ func (ext *extension) NewConn(conn imapserver.Conn) imapserver.Conn {
} }
type handler struct { type handler struct {
hdlrID *imapid.Handler hdlrID *imapid.Handler
setter currentClientSetter clientSetter currentClientSetter
} }
func (hdlr *handler) Parse(fields []interface{}) error { func (hdlr *handler) Parse(fields []interface{}) error {
@ -69,21 +69,18 @@ func (hdlr *handler) Handle(conn imapserver.Conn) error {
err := hdlr.hdlrID.Handle(conn) err := hdlr.hdlrID.Handle(conn)
if err == nil { if err == nil {
id := hdlr.hdlrID.Command.ID id := hdlr.hdlrID.Command.ID
hdlr.setter.SetCurrentClient( hdlr.clientSetter.SetClient(id[imapid.FieldName], id[imapid.FieldVersion])
id[imapid.FieldName],
id[imapid.FieldVersion],
)
} }
return err return err
} }
// NewExtension returns extension which is adding RFC2871 ID capability, with // NewExtension returns extension which is adding RFC2871 ID capability, with
// direct interface to set information about email client to backend. // direct interface to set information about email client to backend.
func NewExtension(serverID imapid.ID, setter currentClientSetter) imapserver.Extension { func NewExtension(serverID imapid.ID, clientSetter currentClientSetter) imapserver.Extension {
if conExtID, ok := imapid.NewExtension(serverID).(imapserver.ConnExtension); ok { if conExtID, ok := imapid.NewExtension(serverID).(imapserver.ConnExtension); ok {
return &extension{ return &extension{
extID: conExtID, extID: conExtID,
setter: setter, clientSetter: clientSetter,
} }
} }
return nil return nil

View File

@ -19,6 +19,7 @@ package imap
import ( import (
"strings" "strings"
"time"
"github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -56,6 +57,25 @@ func newIMAPMailbox(panicHandler panicHandler, user *imapUser, storeMailbox stor
} }
} }
// logCommand is helper to log commands requested by IMAP client with their
// params, result, and duration, but without private data.
// It's logged as INFO so it's logged for every user by default. This should
// help devs to find out reasons why clients, mostly Apple Mail, does re-sync.
// FETCH, APPEND, STORE, COPY, MOVE, and EXPUNGE should be using this helper.
func (im *imapMailbox) logCommand(callback func() error, cmd string, params ...interface{}) error {
start := time.Now()
err := callback()
// Not using im.log to not include addressID which is not needed in this case.
log.WithFields(logrus.Fields{
"userID": im.storeUser.UserID(),
"labelID": im.storeMailbox.LabelID(),
"duration": time.Since(start),
"err": err,
"params": params,
}).Info(cmd)
return err
}
// Name returns this mailbox name. // Name returns this mailbox name.
func (im *imapMailbox) Name() string { func (im *imapMailbox) Name() string {
// Called from go-imap in goroutines - we need to handle panics for each function. // Called from go-imap in goroutines - we need to handle panics for each function.
@ -177,17 +197,16 @@ func (im *imapMailbox) Check() error {
// Expunge permanently removes all messages that have the \Deleted flag set // Expunge permanently removes all messages that have the \Deleted flag set
// from the currently selected mailbox. // from the currently selected mailbox.
func (im *imapMailbox) Expunge() error { func (im *imapMailbox) Expunge() error {
// Wait for any APPENDS to finish in order to avoid data loss when // See comment of appendExpungeLock.
// Outlook sends commands too quickly STORE \Deleted, APPEND, EXPUNGE, if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel {
// APPEND FINISHED: im.user.appendExpungeLock.Lock()
// defer im.user.appendExpungeLock.Unlock()
// Based on Outlook APPEND request we will not create new message but }
// move the original to desired mailbox. If the message is currently
// in Trash or Spam and EXPUNGE happens before APPEND processing is
// finished the message is deleted from Proton instead of moved to
// the desired mailbox.
im.user.waitForAppend()
return im.logCommand(im.expunge, "EXPUNGE")
}
func (im *imapMailbox) expunge() error {
im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage) im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage) defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
@ -197,6 +216,18 @@ func (im *imapMailbox) Expunge() error {
// UIDExpunge permanently removes messages that have the \Deleted flag set // UIDExpunge permanently removes messages that have the \Deleted flag set
// and UID passed from SeqSet from the currently selected mailbox. // and UID passed from SeqSet from the currently selected mailbox.
func (im *imapMailbox) UIDExpunge(seqSet *imap.SeqSet) error { func (im *imapMailbox) UIDExpunge(seqSet *imap.SeqSet) error {
return im.logCommand(func() error {
return im.uidExpunge(seqSet)
}, "UID EXPUNGE", seqSet)
}
func (im *imapMailbox) uidExpunge(seqSet *imap.SeqSet) error {
// See comment of appendExpungeLock.
if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel {
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
}
im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage) im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage)
defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage) defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage)

View File

@ -66,13 +66,16 @@ func (dnc *doNotCacheError) errorOrNil() error {
// //
// If the Backend implements Updater, it must notify the client immediately // If the Backend implements Updater, it must notify the client immediately
// via a mailbox update. // via a mailbox update.
func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { // nolint[funlen] func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
return im.logCommand(func() error {
return im.createMessage(flags, date, body)
}, "APPEND", flags, date)
}
func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.Literal) error { // nolint[funlen]
// Called from go-imap in goroutines - we need to handle panics for each function. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() defer im.panicHandler.HandlePanic()
im.user.appendStarted()
defer im.user.appendFinished()
m, _, _, readers, err := message.Parse(body) m, _, _, readers, err := message.Parse(body)
if err != nil { if err != nil {
return err return err
@ -154,6 +157,9 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
} }
} }
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
// Avoid appending a message which is already on the server. Apply the // Avoid appending a message which is already on the server. Apply the
// new label instead. This always happens with Outlook (it uses APPEND // new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY). // instead of COPY).

View File

@ -38,6 +38,12 @@ import (
// If the Backend implements Updater, it must notify the client immediately // If the Backend implements Updater, it must notify the client immediately
// via a message update. // via a message update.
func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error { func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error {
return im.logCommand(func() error {
return im.updateMessagesFlags(uid, seqSet, operation, flags)
}, "STORE", uid, seqSet, operation, flags)
}
func (im *imapMailbox) updateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error {
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"flags": flags, "flags": flags,
"operation": operation, "operation": operation,
@ -198,6 +204,12 @@ func (im *imapMailbox) addOrRemoveFlags(operation imap.FlagsOp, messageIDs, flag
// destination mailbox. The flags and internal date of the message(s) SHOULD // destination mailbox. The flags and internal date of the message(s) SHOULD
// be preserved, and the Recent flag SHOULD be set, in the copy. // be preserved, and the Recent flag SHOULD be set, in the copy.
func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error { func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
return im.logCommand(func() error {
return im.copyMessages(uid, seqSet, targetLabel)
}, "COPY", uid, seqSet, targetLabel)
}
func (im *imapMailbox) copyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
// Called from go-imap in goroutines - we need to handle panics for each function. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() defer im.panicHandler.HandlePanic()
@ -209,6 +221,12 @@ func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel s
// This should not be used until MOVE extension has option to send UIDPLUS // This should not be used until MOVE extension has option to send UIDPLUS
// responses. // responses.
func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error { func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
return im.logCommand(func() error {
return im.moveMessages(uid, seqSet, targetLabel)
}, "MOVE", uid, seqSet, targetLabel)
}
func (im *imapMailbox) moveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
// Called from go-imap in goroutines - we need to handle panics for each function. // Called from go-imap in goroutines - we need to handle panics for each function.
defer im.panicHandler.HandlePanic() defer im.panicHandler.HandlePanic()
@ -463,7 +481,13 @@ func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria)
// 3501 section 6.4.5 for a list of items that can be requested. // 3501 section 6.4.5 for a list of items that can be requested.
// //
// 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) (err error) { //nolint[funlen] func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) error {
return im.logCommand(func() error {
return im.listMessages(isUID, seqSet, items, msgResponse)
}, "FETCH", isUID, seqSet, items)
}
func (im *imapMailbox) listMessages(isUID bool, seqSet *imap.SeqSet, items []imap.FetchItem, msgResponse chan<- *imap.Message) (err error) { //nolint[funlen]
defer func() { defer func() {
close(msgResponse) close(msgResponse)
if err != nil { if err != nil {

View File

@ -28,6 +28,7 @@ import (
imapid "github.com/ProtonMail/go-imap-id" imapid "github.com/ProtonMail/go-imap-id"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/imap/id" "github.com/ProtonMail/proton-bridge/internal/imap/id"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
@ -39,6 +40,7 @@ import (
imapmove "github.com/emersion/go-imap-move" imapmove "github.com/emersion/go-imap-move"
imapquota "github.com/emersion/go-imap-quota" imapquota "github.com/emersion/go-imap-quota"
imapunselect "github.com/emersion/go-imap-unselect" imapunselect "github.com/emersion/go-imap-unselect"
"github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server" imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -47,6 +49,7 @@ import (
type imapServer struct { type imapServer struct {
panicHandler panicHandler panicHandler panicHandler
server *imapserver.Server server *imapserver.Server
userAgent *useragent.UserAgent
eventListener listener.Listener eventListener listener.Listener
debugClient bool debugClient bool
debugServer bool debugServer bool
@ -55,7 +58,7 @@ type imapServer struct {
} }
// NewIMAPServer constructs a new IMAP server configured with the given options. // NewIMAPServer constructs a new IMAP server configured with the given options.
func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend *imapBackend, eventListener listener.Listener) *imapServer { //nolint[golint] func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, port int, tls *tls.Config, imapBackend backend.Backend, userAgent *useragent.UserAgent, eventListener listener.Listener) *imapServer { // nolint[golint]
s := imapserver.New(imapBackend) s := imapserver.New(imapBackend)
s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) s.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
s.TLSConfig = tls s.TLSConfig = tls
@ -93,7 +96,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
s.Enable( s.Enable(
imapidle.NewExtension(), imapidle.NewExtension(),
imapmove.NewExtension(), imapmove.NewExtension(),
id.NewExtension(serverID, imapBackend.bridge), id.NewExtension(serverID, userAgent),
imapquota.NewExtension(), imapquota.NewExtension(),
imapappendlimit.NewExtension(), imapappendlimit.NewExtension(),
imapunselect.NewExtension(), imapunselect.NewExtension(),
@ -103,6 +106,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
server := &imapServer{ server := &imapServer{
panicHandler: panicHandler, panicHandler: panicHandler,
server: s, server: s,
userAgent: userAgent,
eventListener: eventListener, eventListener: eventListener,
debugClient: debugClient, debugClient: debugClient,
debugServer: debugServer, debugServer: debugServer,
@ -144,9 +148,10 @@ func (s *imapServer) listenAndServe(retries int) {
return return
} }
err = s.server.Serve(&debugListener{ err = s.server.Serve(&connListener{
Listener: l, Listener: l,
server: s, server: s,
userAgent: s.userAgent,
}) })
// Serve returns error every time, even after closing the server. // Serve returns error every time, even after closing the server.
// User shouldn't be notified about error if server shouldn't be running, // User shouldn't be notified about error if server shouldn't be running,
@ -233,18 +238,19 @@ func (s *imapServer) monitorDisconnectedUsers() {
} }
} }
// debugListener sets debug loggers on server containing fields with local // connListener sets debug loggers on server containing fields with local
// and remote addresses right after new connection is accepted. // and remote addresses right after new connection is accepted.
type debugListener struct { type connListener struct {
net.Listener net.Listener
server *imapServer server *imapServer
userAgent *useragent.UserAgent
} }
func (dl *debugListener) Accept() (net.Conn, error) { func (l *connListener) Accept() (net.Conn, error) {
conn, err := dl.Listener.Accept() conn, err := l.Listener.Accept()
if err == nil && (dl.server.debugServer || dl.server.debugClient) { if err == nil && (l.server.debugServer || l.server.debugClient) {
debugLog := log debugLog := log
if addr := conn.LocalAddr(); addr != nil { if addr := conn.LocalAddr(); addr != nil {
debugLog = debugLog.WithField("loc", addr.String()) debugLog = debugLog.WithField("loc", addr.String())
@ -254,14 +260,18 @@ func (dl *debugListener) Accept() (net.Conn, error) {
} }
var localDebug, remoteDebug io.Writer var localDebug, remoteDebug io.Writer
if dl.server.debugServer { if l.server.debugServer {
localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel) localDebug = debugLog.WithField("pkg", "imap/server").WriterLevel(logrus.DebugLevel)
} }
if dl.server.debugClient { if l.server.debugClient {
remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel) remoteDebug = debugLog.WithField("pkg", "imap/client").WriterLevel(logrus.DebugLevel)
} }
dl.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug) l.server.server.Debug = imap.NewDebugWriter(localDebug, remoteDebug)
}
if !l.userAgent.HasClient() {
l.userAgent.SetClient("UnknownClient", "0.0.1")
} }
return conn, err return conn, err

View File

@ -23,6 +23,7 @@ import (
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/ProtonMail/proton-bridge/pkg/ports"
@ -48,6 +49,7 @@ func TestIMAPServerTurnOffAndOnAgain(t *testing.T) {
panicHandler: panicHandler, panicHandler: panicHandler,
server: server, server: server,
eventListener: eventListener, eventListener: eventListener,
userAgent: useragent.New(),
} }
s.isRunning.Store(false) s.isRunning.Store(false)

View File

@ -41,7 +41,22 @@ type imapUser struct {
currentAddressLowercase string currentAddressLowercase string
appendInProcess sync.WaitGroup // Some clients, for example Outlook, do MOVE by STORE \Deleted, APPEND,
// EXPUNGE where APPEN and EXPUNGE can go in parallel. Usual IMAP servers
// do not deduplicate messages and this it's not an issue, but for APPEND
// for PM means just assigning label. That would cause to assign label and
// then delete the message, or in other words cause data loss.
// go-imap does not call CreateMessage till it gets the whole message from
// IMAP client, therefore with big message, simple wait for APPEND before
// performing EXPUNGE is not enough. There has to be two-way lock. Only
// that way even if EXPUNGE is called few ms before APPEND and message
// is deleted, APPEND will not just assing label but creates the message
// again.
// The issue is only when moving message from folder which is causing
// real removal, so Trash and Spam. Those only need to use the lock to
// not cause huge slow down as EXPUNGE is implicitly called also after
// UNSELECT, CLOSE, or LOGOUT.
appendExpungeLock sync.Mutex
} }
// newIMAPUser returns struct implementing go-imap/user interface. // newIMAPUser returns struct implementing go-imap/user interface.
@ -218,8 +233,9 @@ func (iu *imapUser) GetQuota(name string) (*imapquota.Status, error) {
resources := make(map[string][2]uint32) resources := make(map[string][2]uint32)
var list [2]uint32 var list [2]uint32
list[0] = uint32(usedSpace / 1000) // Quota is "in units of 1024 octets" (or KB) and PM returns bytes.
list[1] = uint32(maxSpace / 1000) list[0] = uint32(usedSpace / 1024)
list[1] = uint32(maxSpace / 1024)
resources[imapquota.ResourceStorage] = list resources[imapquota.ResourceStorage] = list
status := &imapquota.Status{ status := &imapquota.Status{
Name: "", Name: "",
@ -250,15 +266,3 @@ func (iu *imapUser) CreateMessageLimit() *uint32 {
upload := uint32(maxUpload) upload := uint32(maxUpload)
return &upload return &upload
} }
func (iu *imapUser) appendStarted() {
iu.appendInProcess.Add(1)
}
func (iu *imapUser) appendFinished() {
iu.appendInProcess.Done()
}
func (iu *imapUser) waitForAppend() {
iu.appendInProcess.Wait()
}

View File

@ -1,22 +0,0 @@
// 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/>.
// Code generated by ./credits.sh at Mon Feb 1 10:34:22 CET 2021. DO NOT EDIT.
package importexport
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -45,28 +45,21 @@ func init() { // nolint[noinit]
}) })
} }
type userAgentProvider interface {
GetUserAgent() string
}
type Reporter struct { type Reporter struct {
appName string appName string
appVersion string appVersion string
uap userAgentProvider userAgent fmt.Stringer
} }
// NewReporter creates new sentry reporter with appName and appVersion to report. // NewReporter creates new sentry reporter with appName and appVersion to report.
func NewReporter(appName, appVersion string) *Reporter { func NewReporter(appName, appVersion string, userAgent fmt.Stringer) *Reporter {
return &Reporter{ return &Reporter{
appName: appName, appName: appName,
appVersion: appVersion, appVersion: appVersion,
userAgent: userAgent,
} }
} }
func (r *Reporter) SetUserAgentProvider(uap userAgentProvider) {
r.uap = uap
}
func (r *Reporter) ReportException(i interface{}) error { func (r *Reporter) ReportException(i interface{}) error {
err := fmt.Errorf("recover: %v", i) err := fmt.Errorf("recover: %v", i)
@ -97,19 +90,11 @@ func (r *Reporter) scopedReport(doReport func()) error {
return nil return nil
} }
// In case clientManager is not yet created we can get at least OS string.
var userAgent string
if r.uap != nil {
userAgent = r.uap.GetUserAgent()
} else {
userAgent = runtime.GOOS
}
tags := map[string]string{ tags := map[string]string{
"OS": runtime.GOOS, "OS": runtime.GOOS,
"Client": r.appName, "Client": r.appName,
"Version": r.appVersion, "Version": r.appVersion,
"UserAgent": userAgent, "UserAgent": r.userAgent.String(),
"UserID": "", "UserID": "",
} }

View File

@ -35,8 +35,8 @@ func TestSkipDuringUnwind(t *testing.T) {
}() }()
wantSkippedFunctions := []string{ wantSkippedFunctions := []string{
"github.com/ProtonMail/proton-bridge/pkg/sentry.TestSkipDuringUnwind", "github.com/ProtonMail/proton-bridge/internal/sentry.TestSkipDuringUnwind",
"github.com/ProtonMail/proton-bridge/pkg/sentry.TestSkipDuringUnwind.func1", "github.com/ProtonMail/proton-bridge/internal/sentry.TestSkipDuringUnwind.func1",
} }
r.Equal(t, wantSkippedFunctions, skippedFunctions) r.Equal(t, wantSkippedFunctions, skippedFunctions)
} }
@ -45,8 +45,8 @@ func TestFilterOutPanicHandlers(t *testing.T) {
skippedFunctions = []string{ skippedFunctions = []string{
"github.com/ProtonMail/proton-bridge/pkg/config.(*PanicHandler).HandlePanic", "github.com/ProtonMail/proton-bridge/pkg/config.(*PanicHandler).HandlePanic",
"github.com/ProtonMail/proton-bridge/pkg/config.HandlePanic", "github.com/ProtonMail/proton-bridge/pkg/config.HandlePanic",
"github.com/ProtonMail/proton-bridge/pkg/sentry.ReportSentryCrash", "github.com/ProtonMail/proton-bridge/internal/sentry.ReportSentryCrash",
"github.com/ProtonMail/proton-bridge/pkg/sentry.ReportSentryCrash.func1", "github.com/ProtonMail/proton-bridge/internal/sentry.ReportSentryCrash.func1",
} }
frames := []sentry.Frame{ frames := []sentry.Frame{
@ -57,8 +57,8 @@ func TestFilterOutPanicHandlers(t *testing.T) {
{Module: "main", Function: "run"}, {Module: "main", Function: "run"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "(*PanicHandler).HandlePanic"}, {Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "(*PanicHandler).HandlePanic"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "HandlePanic"}, {Module: "github.com/ProtonMail/proton-bridge/pkg/config", Function: "HandlePanic"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/sentry", Function: "ReportSentryCrash"}, {Module: "github.com/ProtonMail/proton-bridge/internal/sentry", Function: "ReportSentryCrash"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/sentry", Function: "ReportSentryCrash.func1"}, {Module: "github.com/ProtonMail/proton-bridge/internal/sentry", Function: "ReportSentryCrash.func1"},
} }
gotFrames := filterOutPanicHandlers(frames) gotFrames := filterOutPanicHandlers(frames)

View File

@ -0,0 +1,22 @@
// 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/>.
// +build !build_qa
package smtp
func dumpMessageData([]byte, string) {}

60
internal/smtp/dump_qa.go Normal file
View File

@ -0,0 +1,60 @@
// 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/>.
// +build build_qa
package smtp
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
)
func dumpMessageData(b []byte, subject string) {
home, err := os.UserHomeDir()
if err != nil {
logrus.WithError(err).Error("Failed to dump raw message data")
return
}
path := filepath.Join(home, "bridge-qa")
if err := os.MkdirAll(path, 0700); err != nil {
logrus.WithError(err).Error("Failed to dump raw message data")
return
}
if len(subject) > 16 {
subject = subject[:16]
}
if err := ioutil.WriteFile(
filepath.Join(path, fmt.Sprintf("%v-%v.eml", subject, time.Now().Format(time.RFC3339Nano))),
b,
0600,
); err != nil {
logrus.WithError(err).Error("Failed to dump raw message data")
return
}
logrus.WithField("path", path).Info("Dumped raw message data")
}

View File

@ -215,6 +215,10 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
// Called from go-smtp in goroutines - we need to handle panics for each function. // Called from go-smtp in goroutines - we need to handle panics for each function.
defer su.panicHandler.HandlePanic() defer su.panicHandler.HandlePanic()
b := new(bytes.Buffer)
messageReader = io.TeeReader(messageReader, b)
mailSettings, err := su.client().GetMailSettings() mailSettings, err := su.client().GetMailSettings()
if err != nil { if err != nil {
return err return err
@ -405,6 +409,8 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
req.PreparePackages() req.PreparePackages()
dumpMessageData(b.Bytes(), message.Subject)
return su.storeUser.SendMessage(message.ID, req) return su.storeUser.SendMessage(message.ID, req)
} }

View File

@ -28,8 +28,13 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const pollInterval = 30 * time.Second const (
const pollIntervalSpread = 5 * time.Second pollInterval = 30 * time.Second
pollIntervalSpread = 5 * time.Second
// errMaxSentry defines after how many errors in a row to report it to sentry.
errMaxSentry = 20
)
type eventLoop struct { type eventLoop struct {
cache *Cache cache *Cache
@ -41,6 +46,7 @@ type eventLoop struct {
isRunning bool // The whole event loop is running. isRunning bool // The whole event loop is running.
pollCounter int pollCounter int
errCounter int
log *logrus.Entry log *logrus.Entry
@ -227,9 +233,18 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
_, errUnauthorized := errors.Cause(err).(*pmapi.ErrUnauthorized) _, errUnauthorized := errors.Cause(err).(*pmapi.ErrUnauthorized)
if err == nil {
loop.errCounter = 0
}
// All errors except Invalid Token (which is not possible to recover from) are ignored. // All errors except Invalid Token (which is not possible to recover from) are ignored.
if err != nil && !errUnauthorized && errors.Cause(err) != pmapi.ErrInvalidToken { if err != nil && !errUnauthorized && errors.Cause(err) != pmapi.ErrInvalidToken {
l.WithError(err).Error("Error skipped") l.WithError(err).WithField("errors", loop.errCounter).Error("Error skipped")
loop.errCounter++
if loop.errCounter == errMaxSentry {
if sentryErr := loop.store.sentryReporter.ReportMessage("Warning: event loop issues: " + err.Error() + ", " + loop.currentEventID); sentryErr != nil {
l.WithError(sentryErr).Error("Failed to report error to sentry")
}
}
err = nil err = nil
} }
}() }()
@ -283,6 +298,10 @@ func (loop *eventLoop) processEvent(event *pmapi.Event) (err error) {
eventLog.Info("Processing refresh event") eventLog.Info("Processing refresh event")
loop.store.triggerSync() loop.store.triggerSync()
if sentryErr := loop.store.sentryReporter.ReportMessage("Warning: refresh occurred, " + loop.currentEventID); sentryErr != nil {
loop.log.WithError(sentryErr).Error("Failed to report refresh to sentry")
}
return return
} }

View File

@ -24,6 +24,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
@ -95,10 +96,11 @@ var (
// Store is local user storage, which handles the synchronization between IMAP and PM API. // Store is local user storage, which handles the synchronization between IMAP and PM API.
type Store struct { type Store struct {
panicHandler PanicHandler sentryReporter *sentry.Reporter
eventLoop *eventLoop panicHandler PanicHandler
user BridgeUser eventLoop *eventLoop
clientManager ClientManager user BridgeUser
clientManager ClientManager
log *logrus.Entry log *logrus.Entry
@ -115,7 +117,8 @@ type Store struct {
} }
// New creates or opens a store for the given `user`. // New creates or opens a store for the given `user`.
func New( func New( // nolint[funlen]
sentryReporter *sentry.Reporter,
panicHandler PanicHandler, panicHandler PanicHandler,
user BridgeUser, user BridgeUser,
clientManager ClientManager, clientManager ClientManager,
@ -145,14 +148,15 @@ func New(
} }
store = &Store{ store = &Store{
panicHandler: panicHandler, sentryReporter: sentryReporter,
clientManager: clientManager, panicHandler: panicHandler,
user: user, clientManager: clientManager,
cache: cache, user: user,
filePath: path, cache: cache,
db: bdb, filePath: path,
lock: &sync.RWMutex{}, db: bdb,
log: l, lock: &sync.RWMutex{},
log: l,
} }
// Minimal increase is event pollInterval, doubles every failed retry up to 5 minutes. // Minimal increase is event pollInterval, doubles every failed retry up to 5 minutes.

View File

@ -125,6 +125,7 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool, msgs ...*pmapi.M
var err error var err error
mocks.store, err = New( mocks.store, err = New(
nil, // Sentry reporter is not used under unit tests.
mocks.panicHandler, mocks.panicHandler,
mocks.user, mocks.user,
mocks.clientManager, mocks.clientManager,

View File

@ -188,18 +188,6 @@ func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
} }
// SetUserAgent mocks base method
func (m *MockClientManager) SetUserAgent(arg0, arg1, arg2 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetUserAgent", arg0, arg1, arg2)
}
// SetUserAgent indicates an expected call of SetUserAgent
func (mr *MockClientManagerMockRecorder) SetUserAgent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserAgent", reflect.TypeOf((*MockClientManager)(nil).SetUserAgent), arg0, arg1, arg2)
}
// MockCredentialsStorer is a mock of CredentialsStorer interface // MockCredentialsStorer is a mock of CredentialsStorer interface
type MockCredentialsStorer struct { type MockCredentialsStorer struct {
ctrl *gomock.Controller ctrl *gomock.Controller

View File

@ -55,7 +55,6 @@ type ClientManager interface {
DisallowProxy() DisallowProxy()
GetAuthUpdateChannel() chan pmapi.ClientAuth GetAuthUpdateChannel() chan pmapi.ClientAuth
CheckConnection() error CheckConnection() error
SetUserAgent(clientName, clientVersion, os string)
} }
type StoreMaker interface { type StoreMaker interface {

View File

@ -26,6 +26,7 @@ import (
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks" usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
@ -185,9 +186,10 @@ func initMocks(t *testing.T) mocks {
// Set up store factory. // Set up store factory.
m.storeMaker.EXPECT().New(gomock.Any()).DoAndReturn(func(user store.BridgeUser) (*store.Store, error) { m.storeMaker.EXPECT().New(gomock.Any()).DoAndReturn(func(user store.BridgeUser) (*store.Store, error) {
var sentryReporter *sentry.Reporter // Sentry reporter is not used under unit tests.
dbFile, err := ioutil.TempFile("", "bridge-store-db-*.db") dbFile, err := ioutil.TempFile("", "bridge-store-db-*.db")
require.NoError(t, err, "could not get temporary file for store db") require.NoError(t, err, "could not get temporary file for store db")
return store.New(m.PanicHandler, user, m.clientManager, m.eventListener, dbFile.Name(), m.storeCache) return store.New(sentryReporter, m.PanicHandler, user, m.clientManager, m.eventListener, dbFile.Name(), m.storeCache)
}).AnyTimes() }).AnyTimes()
m.storeMaker.EXPECT().Remove(gomock.Any()).AnyTimes() m.storeMaker.EXPECT().Remove(gomock.Any()).AnyTimes()

View File

@ -19,10 +19,12 @@ package keychain
import ( import (
"os/exec" "os/exec"
"reflect"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/pass" "github.com/docker/docker-credential-helpers/pass"
"github.com/docker/docker-credential-helpers/secretservice" "github.com/docker/docker-credential-helpers/secretservice"
"github.com/sirupsen/logrus"
) )
const ( const (
@ -43,9 +45,9 @@ func init() { // nolint[noinit]
// If Pass is available, use it by default. // If Pass is available, use it by default.
// Otherwise, if GnomeKeyring is available, use it by default. // Otherwise, if GnomeKeyring is available, use it by default.
if _, ok := Helpers[Pass]; ok { if _, ok := Helpers[Pass]; ok && isUsable(newPassHelper("")) {
defaultHelper = Pass defaultHelper = Pass
} else if _, ok := Helpers[GnomeKeyring]; ok { } else if _, ok := Helpers[GnomeKeyring]; ok && isUsable(newGnomeKeyringHelper("")) {
defaultHelper = GnomeKeyring defaultHelper = GnomeKeyring
} }
} }
@ -57,3 +59,36 @@ func newPassHelper(string) (credentials.Helper, error) {
func newGnomeKeyringHelper(string) (credentials.Helper, error) { func newGnomeKeyringHelper(string) (credentials.Helper, error) {
return &secretservice.Secretservice{}, nil return &secretservice.Secretservice{}, nil
} }
// isUsable returns whether the credentials helper is usable.
func isUsable(helper credentials.Helper, err error) bool {
l := logrus.WithField("helper", reflect.TypeOf(helper))
if err != nil {
l.WithError(err).Warn("Keychain helper couldn't be created")
return false
}
creds := &credentials.Credentials{
ServerURL: "bridge/check",
Username: "check",
Secret: "check",
}
if err := helper.Add(creds); err != nil {
l.WithError(err).Warn("Failed to add test credentials to keychain")
return false
}
if _, _, err := helper.Get(creds.ServerURL); err != nil {
l.WithError(err).Warn("Failed to get test credentials from keychain")
return false
}
if err := helper.Delete(creds.ServerURL); err != nil {
l.WithError(err).Warn("Failed to delete test credentials from keychain")
return false
}
return true
}

View File

@ -78,13 +78,6 @@ type ClientConfig struct {
// The client application name and version. // The client application name and version.
AppVersion string AppVersion string
// The client application user agent in format `client name/client version (os)`, e.g.:
// (Intel Mac OS X 10_15_3)
// Mac OS X Mail/13.0 (3608.60.0.2.5) (Intel Mac OS X 10_15_3)
// Thunderbird/1.5.0 (Ubuntu 18.04.4 LTS)
// MSOffice 12 (Windows 10 (10.0))
UserAgent string
// The client ID. // The client ID.
ClientID string ClientID string
@ -236,7 +229,7 @@ func (c *client) Do(req *http.Request, retryUnauthorized bool) (res *http.Respon
func (c *client) doBuffered(req *http.Request, bodyBuffer []byte, retryUnauthorized bool) (res *http.Response, err error) { // nolint[funlen] func (c *client) doBuffered(req *http.Request, bodyBuffer []byte, retryUnauthorized bool) (res *http.Response, err error) { // nolint[funlen]
isAuthReq := strings.Contains(req.URL.Path, "/auth") isAuthReq := strings.Contains(req.URL.Path, "/auth")
req.Header.Set("User-Agent", c.cm.config.UserAgent) req.Header.Set("User-Agent", c.cm.userAgent.String())
req.Header.Set("x-pm-appversion", c.cm.config.AppVersion) req.Header.Set("x-pm-appversion", c.cm.config.AppVersion)
if c.uid != "" { if c.uid != "" {
@ -455,18 +448,22 @@ func (c *client) readAllMinSpeed(data io.Reader, cancelRequest context.CancelFun
}) })
// speedCheckSeconds controls how often we check the transfer speed. // speedCheckSeconds controls how often we check the transfer speed.
const speedCheckSeconds = 3 // Note that connection can be unstable, on average very fast, but can be
// idle for few seconds; or that API can take its time before sending
// another data, e.g., API can send some data and take some time before
// processing and sending the rest of the response.
const speedCheckSeconds = 30
var buffer bytes.Buffer var buffer bytes.Buffer
for { for {
_, err := io.CopyN(&buffer, data, c.cm.config.MinBytesPerSecond*speedCheckSeconds) _, err := io.CopyN(&buffer, data, c.cm.config.MinBytesPerSecond*speedCheckSeconds)
timer.Stop() timer.Stop()
timer.Reset(speedCheckSeconds * time.Second)
if err == io.EOF { if err == io.EOF {
break break
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
timer.Reset(speedCheckSeconds * time.Second)
} }
return ioutil.ReadAll(&buffer) return ioutil.ReadAll(&buffer)

View File

@ -172,7 +172,7 @@ func TestClient_FirstReadTimeout(t *testing.T) {
func TestClient_MinSpeedTimeout(t *testing.T) { func TestClient_MinSpeedTimeout(t *testing.T) {
finish, c := newTestServerCallbacks(t, finish, c := newTestServerCallbacks(t,
routeSlow(4*time.Second), // 1 second longer than the minimum transfer speed poll time. routeSlow(31*time.Second), // 1 second longer than the minimum transfer speed poll time.
) )
defer finish() defer finish()

View File

@ -24,6 +24,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -37,6 +38,7 @@ type ClientManager struct { //nolint[maligned]
newClient func(userID string) Client newClient func(userID string) Client
config *ClientConfig config *ClientConfig
userAgent *useragent.UserAgent
roundTripper http.RoundTripper roundTripper http.RoundTripper
clients map[string]Client clients map[string]Client
@ -86,9 +88,10 @@ type tokenExpiration struct {
} }
// NewClientManager creates a new ClientMan which manages clients configured with the given client config. // NewClientManager creates a new ClientMan which manages clients configured with the given client config.
func NewClientManager(config *ClientConfig) (cm *ClientManager) { func NewClientManager(config *ClientConfig, userAgent *useragent.UserAgent) (cm *ClientManager) {
cm = &ClientManager{ cm = &ClientManager{
config: config, config: config,
userAgent: userAgent,
roundTripper: http.DefaultTransport, roundTripper: http.DefaultTransport,
clients: make(map[string]Client), clients: make(map[string]Client),
@ -118,7 +121,6 @@ func NewClientManager(config *ClientConfig) (cm *ClientManager) {
cm.newClient = func(userID string) Client { cm.newClient = func(userID string) Client {
return newClient(cm, userID) return newClient(cm, userID)
} }
cm.SetUserAgent("", "", "") // Set default user agent.
go cm.watchTokenExpirations() go cm.watchTokenExpirations()
@ -169,16 +171,12 @@ func (cm *ClientManager) SetRoundTripper(rt http.RoundTripper) {
cm.roundTripper = rt cm.roundTripper = rt
} }
func (cm *ClientManager) GetClientConfig() *ClientConfig { func (cm *ClientManager) GetAppVersion() string {
return cm.config return cm.config.AppVersion
}
func (cm *ClientManager) SetUserAgent(clientName, clientVersion, os string) {
cm.config.UserAgent = formatUserAgent(clientName, clientVersion, os)
} }
func (cm *ClientManager) GetUserAgent() string { func (cm *ClientManager) GetUserAgent() string {
return cm.config.UserAgent return cm.userAgent.String()
} }
// GetClient returns a client for the given userID. // GetClient returns a client for the given userID.

View File

@ -17,8 +17,10 @@
package pmapi package pmapi
import "github.com/ProtonMail/proton-bridge/internal/config/useragent"
func newTestClientManager(cfg *ClientConfig) *ClientManager { func newTestClientManager(cfg *ClientConfig) *ClientManager {
cm := NewClientManager(cfg) cm := NewClientManager(cfg, useragent.New())
go func() { go func() {
for range cm.authUpdates { for range cm.authUpdates {

View File

@ -75,17 +75,18 @@ func certFingerprint(cert *x509.Certificate) string {
return fmt.Sprintf(`pin-sha256=%q`, base64.StdEncoding.EncodeToString(hash[:])) return fmt.Sprintf(`pin-sha256=%q`, base64.StdEncoding.EncodeToString(hash[:]))
} }
type clientConfigProvider interface { type clientInfoProvider interface {
GetClientConfig() *ClientConfig GetAppVersion() string
GetUserAgent() string
} }
type tlsReporter struct { type tlsReporter struct {
cm clientConfigProvider cm clientInfoProvider
p *pinChecker p *pinChecker
sentReports []sentReport sentReports []sentReport
} }
func newTLSReporter(p *pinChecker, cm clientConfigProvider) *tlsReporter { func newTLSReporter(p *pinChecker, cm clientInfoProvider) *tlsReporter {
return &tlsReporter{ return &tlsReporter{
cm: cm, cm: cm,
p: p, p: p,
@ -102,13 +103,14 @@ func (r *tlsReporter) reportCertIssue(remoteURI, host, port string, connState tl
certChain = marshalCert7468(connState.PeerCertificates) certChain = marshalCert7468(connState.PeerCertificates)
} }
cfg := r.cm.GetClientConfig() appVersion := r.cm.GetAppVersion()
userAgent := r.cm.GetUserAgent()
report := newTLSReport(host, port, connState.ServerName, certChain, r.p.trustedPins, cfg.AppVersion) report := newTLSReport(host, port, connState.ServerName, certChain, r.p.trustedPins, appVersion)
if !r.hasRecentlySentReport(report) { if !r.hasRecentlySentReport(report) {
r.recordReport(report) r.recordReport(report)
go report.sendReport(remoteURI, cfg.UserAgent) go report.sendReport(remoteURI, userAgent)
} }
} }

View File

@ -27,12 +27,16 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type fakeClientConfigProvider struct { type fakeClientInfoProvider struct {
version, useragent string version, useragent string
} }
func (c *fakeClientConfigProvider) GetClientConfig() *ClientConfig { func (c *fakeClientInfoProvider) GetAppVersion() string {
return &ClientConfig{AppVersion: c.version, UserAgent: c.useragent} return c.version
}
func (c *fakeClientInfoProvider) GetUserAgent() string {
return c.useragent
} }
func TestPinCheckerDoubleReport(t *testing.T) { func TestPinCheckerDoubleReport(t *testing.T) {
@ -42,7 +46,7 @@ func TestPinCheckerDoubleReport(t *testing.T) {
reportCounter++ reportCounter++
})) }))
r := newTLSReporter(newPinChecker(TrustedAPIPins), &fakeClientConfigProvider{version: "3", useragent: "useragent"}) r := newTLSReporter(newPinChecker(TrustedAPIPins), &fakeClientInfoProvider{version: "3", useragent: "useragent"})
// Report the same issue many times. // Report the same issue many times.
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {

View File

@ -1,55 +0,0 @@
// 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 pmapi
import (
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUpdateCurrentUserAgentGOOS(t *testing.T) {
userAgent := formatUserAgent("", "", "")
assert.Equal(t, " ("+runtime.GOOS+")", userAgent)
}
func TestUpdateCurrentUserAgentOS(t *testing.T) {
userAgent := formatUserAgent("", "", "os")
assert.Equal(t, " (os)", userAgent)
}
func TestUpdateCurrentUserAgentClientVer(t *testing.T) {
userAgent := formatUserAgent("", "ver", "os")
assert.Equal(t, " (os)", userAgent)
}
func TestUpdateCurrentUserAgentClientName(t *testing.T) {
userAgent := formatUserAgent("mail", "", "os")
assert.Equal(t, "mail (os)", userAgent)
}
func TestUpdateCurrentUserAgentClientNameAndVersion(t *testing.T) {
userAgent := formatUserAgent("mail", "ver", "os")
assert.Equal(t, "mail/ver (os)", userAgent)
}
func TestRemoveBrackets(t *testing.T) {
userAgent := formatUserAgent("mail (submail)", "ver (subver)", "os (subos)")
assert.Equal(t, "mail-submail/ver-subver (os subos)", userAgent)
}

View File

@ -1,3 +1,37 @@
## v1.6.5
- 2021-02-22
### 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
- NoGUI bulid
- Background of GUI welcome message
- Incorrect total mailbox size displayed in Apple Mail
## v1.6.3
- 2021-02-16
### New
- Added desktop files and icon in Bridge repo
- Better detection of MacOS version to improve automatic AppleMail configuration
- Clearing cache after switching early access off
### Fixed
- Better poor connection handling - added retries for starting IMAP server after the connection was down
- Excluding updates from 'clearing cache'
- Not allowing copying from Inbox to Sent and vice versa
- Improvements to moving messages (unlabelling folders)
- Fixed the separation of release notes for 'early' and 'stable' channels
## v1.6.2 ## v1.6.2
- 2021-02-02 - 2021-02-02

View File

@ -1,3 +1,21 @@
## v1.6.3
- 2021-02-16
### New
- Added desktop files and icon in Bridge repo
- Better detection of MacOS version to improve automatic AppleMail configuration
- Clearing cache after switching early access off
### Fixed
- Better poor connection handling - added retries for starting IMAP server after the connection was down
- Excluding updates from 'clearing cache'
- Not allowing copying from Inbox to Sent and vice versa
- Improvements to moving messages (unlabelling folders)
- Fixed the separation of release notes for 'early' and 'stable' channels
## v1.6.2 ## v1.6.2
- 2021-02-02 - 2021-02-02

View File

@ -41,7 +41,7 @@ func APIChecksFeatureContext(s *godog.Suite) {
s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has (\d+) message(?:s)?$`, apiMailboxForAddressOfUserHasNumberOfMessages) s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has (\d+) message(?:s)?$`, apiMailboxForAddressOfUserHasNumberOfMessages)
s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages) s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages)
s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages) s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages)
s.Step(`^API client manager user-agent is "([^"]*)"$`, clientManagerUserAgent) s.Step(`^API user-agent is "([^"]*)"$`, userAgent)
} }
func apiIsCalled(endpoint string) error { func apiIsCalled(endpoint string) error {
@ -187,12 +187,11 @@ func getPMAPIMessages(account *accounts.TestAccount, mailboxName string) ([]*pma
return ctx.GetPMAPIController().GetMessages(account.Username(), labelID) return ctx.GetPMAPIController().GetMessages(account.Username(), labelID)
} }
func clientManagerUserAgent(expectedUserAgent string) error { func userAgent(expectedUserAgent string) error {
expectedUserAgent = strings.ReplaceAll(expectedUserAgent, "[GOOS]", runtime.GOOS) expectedUserAgent = strings.ReplaceAll(expectedUserAgent, "[GOOS]", runtime.GOOS)
assert.Eventually(ctx.GetTestingT(), func() bool { assert.Eventually(ctx.GetTestingT(), func() bool {
userAgent := ctx.GetClientManager().GetUserAgent() return ctx.GetUserAgent() == expectedUserAgent
return userAgent == expectedUserAgent
}, 5*time.Second, time.Second) }, 5*time.Second, time.Second)
return nil return nil

View File

@ -21,6 +21,9 @@ import (
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
) )
@ -67,8 +70,9 @@ func newBridgeInstance(
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, clientManager users.ClientManager,
) *bridge.Bridge { ) *bridge.Bridge {
sentryReporter := sentry.NewReporter("bridge", constants.Version, useragent.New())
panicHandler := &panicHandler{t: t} panicHandler := &panicHandler{t: t}
updater := newFakeUpdater() updater := newFakeUpdater()
versioner := newFakeVersioner() versioner := newFakeVersioner()
return bridge.New(locations, cache, settings, panicHandler, eventListener, clientManager, credStore, updater, versioner) return bridge.New(locations, cache, settings, sentryReporter, panicHandler, eventListener, clientManager, credStore, updater, versioner)
} }

View File

@ -22,6 +22,7 @@ import (
"sync" "sync"
"github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/importexport" "github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/transfer" "github.com/ProtonMail/proton-bridge/internal/transfer"
@ -46,6 +47,7 @@ type TestContext struct {
locations *fakeLocations locations *fakeLocations
settings *fakeSettings settings *fakeSettings
listener listener.Listener listener listener.Listener
userAgent *useragent.UserAgent
testAccounts *accounts.TestAccounts testAccounts *accounts.TestAccounts
// pmapiController is used to control real or fake pmapi clients. // pmapiController is used to control real or fake pmapi clients.
@ -95,11 +97,12 @@ type TestContext struct {
func New(app string) *TestContext { func New(app string) *TestContext {
setLogrusVerbosityFromEnv() setLogrusVerbosityFromEnv()
configName := app userAgent := useragent.New()
if app == "ie" {
configName = "importExport" cm := pmapi.NewClientManager(
} pmapi.GetAPIConfig(getConfigName(app), constants.Version),
cm := pmapi.NewClientManager(pmapi.GetAPIConfig(configName, constants.Version)) userAgent,
)
ctx := &TestContext{ ctx := &TestContext{
t: &bddT{}, t: &bddT{},
@ -107,6 +110,7 @@ func New(app string) *TestContext {
locations: newFakeLocations(), locations: newFakeLocations(),
settings: newFakeSettings(), settings: newFakeSettings(),
listener: listener.New(), listener: listener.New(),
userAgent: userAgent,
pmapiController: newPMAPIController(cm), pmapiController: newPMAPIController(cm),
clientManager: cm, clientManager: cm,
testAccounts: newTestAccounts(), testAccounts: newTestAccounts(),
@ -137,6 +141,14 @@ func New(app string) *TestContext {
return ctx return ctx
} }
func getConfigName(app string) string {
if app == "ie" {
return "importExport"
}
return app
}
// Cleanup runs through all cleanup steps. // Cleanup runs through all cleanup steps.
// This can be a deferred call so that it is run even if the test steps failed the test. // This can be a deferred call so that it is run even if the test steps failed the test.
func (ctx *TestContext) Cleanup() *TestContext { func (ctx *TestContext) Cleanup() *TestContext {
@ -156,6 +168,11 @@ func (ctx *TestContext) GetClientManager() *pmapi.ClientManager {
return ctx.clientManager return ctx.clientManager
} }
// GetUserAgent returns the current user agent.
func (ctx *TestContext) GetUserAgent() string {
return ctx.userAgent.String()
}
// GetTestingT returns testing.T compatible struct. // GetTestingT returns testing.T compatible struct.
func (ctx *TestContext) GetTestingT() *bddT { //nolint[golint] func (ctx *TestContext) GetTestingT() *bddT { //nolint[golint]
return ctx.t return ctx.t

View File

@ -59,7 +59,7 @@ func (ctx *TestContext) withIMAPServer() {
tls, _ := tls.New(settingsPath).GetConfig() tls, _ := tls.New(settingsPath).GetConfig()
backend := imap.NewIMAPBackend(ph, ctx.listener, ctx.cache, ctx.bridge) backend := imap.NewIMAPBackend(ph, ctx.listener, ctx.cache, ctx.bridge)
server := imap.NewIMAPServer(ph, true, true, port, tls, backend, ctx.listener) server := imap.NewIMAPServer(ph, true, true, port, tls, backend, ctx.userAgent, ctx.listener)
go server.ListenAndServe() go server.ListenAndServe()
require.NoError(ctx.t, waitForPort(port, 5*time.Second)) require.NoError(ctx.t, waitForPort(port, 5*time.Second))

View File

@ -4,21 +4,23 @@ Feature: User agent
Scenario: Get user agent Scenario: Get user agent
Given there is IMAP client logged in as "user" Given there is IMAP client logged in as "user"
Then API user-agent is "UnknownClient/0.0.1 ([GOOS])"
When IMAP client sends ID with argument: When IMAP client sends ID with argument:
""" """
"name" "Foo" "version" "1.4.0" "name" "Foo" "version" "1.4.0"
""" """
Then API client manager user-agent is "Foo/1.4.0 ([GOOS])" Then API user-agent is "Foo/1.4.0 ([GOOS])"
Scenario: Update user agent Scenario: Update user agent
Given there is IMAP client logged in as "user" Given there is IMAP client logged in as "user"
Then API user-agent is "UnknownClient/0.0.1 ([GOOS])"
When IMAP client sends ID with argument: When IMAP client sends ID with argument:
""" """
"name" "Foo" "version" "1.4.0" "name" "Foo" "version" "1.4.0"
""" """
Then API client manager user-agent is "Foo/1.4.0 ([GOOS])" Then API user-agent is "Foo/1.4.0 ([GOOS])"
When IMAP client sends ID with argument: When IMAP client sends ID with argument:
""" """
"name" "Bar" "version" "4.2.0" "name" "Bar" "version" "4.2.0"
""" """
Then API client manager user-agent is "Bar/4.2.0 ([GOOS])" Then API user-agent is "Bar/4.2.0 ([GOOS])"

Some files were not shown because too many files have changed in this diff Show More