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
frontend/qml/*.qmlc
# Credits files (generated).
internal/**/credits.go
# Build files
/launcher-*
/bridge_*_*.tgz

View File

@ -2,6 +2,56 @@
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
### Added
@ -10,15 +60,15 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Changed
* 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-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-1033 Retry starting IMAP server after connection was down
* 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-1033 Retry starting IMAP server after connection was down.
### Fixed
* 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-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

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
# 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
APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico
@ -33,7 +33,7 @@ BUILD_TIME:=$(shell date +%FT%T%z)
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
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})
ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+=${BUILD_LDFLAGS}
@ -45,7 +45,7 @@ ifeq "${TARGET_OS}" "windows"
endif
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_NOGUI+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_GUI+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
@ -83,8 +83,8 @@ build: ${TGZ_TARGET}
build-ie:
TARGET_CMD=Import-Export $(MAKE) build
build-nogui:
go build ${BUILD_FLAGS_NOGUI} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
build-nogui: gofiles
go build ${BUILD_FLAGS} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui
@ -137,7 +137,7 @@ endif
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} ${VENDOR_TARGET}
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
cp cmd/${TARGET_CMD}/main.go .
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
qtdeploy ${BUILD_FLAGS_GUI} ${QT_BUILD_TARGET}
mv deploy cmd/${TARGET_CMD}
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
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)
## 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"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
@ -197,6 +197,9 @@ install-linter: check-has-go
install-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
.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/pmapi Client > pkg/pmapi/mocks/mocks.go
lint: lint-golang lint-license lint-changelog
lint: gofiles lint-golang lint-license lint-changelog
lint-license:
./utils/missing_license.sh check
lint-changelog:
./utils/changelog_linter.sh Changelog.md
./utils/changelog_linter.sh unreleased.md
lint-golang:
which golangci-lint || $(MAKE) install-linter
@ -301,12 +303,12 @@ run-qt-cli: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
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
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:
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:
$(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview

View File

@ -25,13 +25,14 @@ import (
"runtime"
"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/crash"
"github.com/ProtonMail/proton-bridge/internal/locations"
"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/versioner"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -44,7 +45,7 @@ var (
)
func main() { // nolint[funlen]
reporter := sentry.NewReporter(appName, constants.Version)
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
crashHandler := crash.NewHandler(reporter.ReportException)
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.
require (
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
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-idle v0.0.0-20200601154248-f05f54664cc4
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-mbox v1.0.2
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/olekukonko/tablewriter v0.0.4 // indirect
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/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
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-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-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU=
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 h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
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/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=

View File

@ -43,25 +43,41 @@ import (
"github.com/ProtonMail/proton-bridge/internal/config/cache"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"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/cookies"
"github.com/ProtonMail/proton-bridge/internal/crash"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/locations"
"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/users/credentials"
"github.com/ProtonMail/proton-bridge/internal/versioner"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus"
"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 {
SentryReporter *sentry.Reporter
CrashHandler *crash.Handler
Locations *locations.Locations
Settings *settings.Settings
@ -71,6 +87,7 @@ type Base struct {
Creds *credentials.Store
CM *pmapi.ClientManager
CookieJar *cookies.Jar
UserAgent *useragent.UserAgent
Updater *updater.Updater
Versioner *versioner.Versioner
TLS *tls.TLS
@ -92,7 +109,10 @@ func New( // nolint[funlen]
keychainName,
cacheVersion string,
) (*Base, error) {
sentryReporter := sentry.NewReporter(appName, constants.Version)
userAgent := useragent.New()
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
crashHandler := crash.NewHandler(
sentryReporter.ReportException,
crash.ShowErrorNotification(appName),
@ -166,20 +186,9 @@ func New( // nolint[funlen]
return nil, err
}
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, "")
}
cm := pmapi.NewClientManager(apiConfig)
cm := pmapi.NewClientManager(getAPIConfig(configName, listener), userAgent)
cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener))
cm.SetCookieJar(jar)
sentryReporter.SetUserAgentProvider(cm)
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil {
@ -220,6 +229,7 @@ func New( // nolint[funlen]
}
return &Base{
SentryReporter: sentryReporter,
CrashHandler: crashHandler,
Locations: locations,
Settings: settingsObj,
@ -229,6 +239,7 @@ func New( // nolint[funlen]
Creds: credentials.NewStore(kc),
CM: cm,
CookieJar: jar,
UserAgent: userAgent,
Updater: updater,
Versioner: versioner,
TLS: tls.New(settingsPath),
@ -252,32 +263,32 @@ func (b *Base) NewApp(action func(*Base, *cli.Context) error) *cli.App {
app.Action = b.run(action)
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: "cpu-prof",
Aliases: []string{"p"},
Name: flagCPUProfile,
Aliases: []string{flagCPUProfileShort},
Usage: "Generate CPU profile",
},
&cli.BoolFlag{
Name: "mem-prof",
Aliases: []string{"m"},
Name: flagMemProfile,
Aliases: []string{flagMemProfileShort},
Usage: "Generate memory profile",
},
&cli.StringFlag{
Name: "log-level",
Aliases: []string{"l"},
Name: flagLogLevel,
Aliases: []string{flagLogLevelShort},
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
},
&cli.BoolFlag{
Name: "cli",
Aliases: []string{"c"},
Name: FlagCLI,
Aliases: []string{flagCLIShort},
Usage: "Use command line interface",
},
&cli.StringFlag{
Name: "restart",
Name: flagRestart,
Usage: "The number of times the application has already restarted",
Hidden: true,
},
&cli.StringFlag{
Name: "launcher",
Name: flagLauncher,
Usage: "The launcher to use to restart the application",
Hidden: true,
},
@ -302,21 +313,21 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
defer func() { _ = b.Lock.Close() }()
// 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.command = launcher
}
if doCPUProfile := c.Bool("cpu-prof"); doCPUProfile {
if c.Bool(flagCPUProfile) {
startCPUProfile()
defer pprof.StopCPUProfile()
}
if doMemoryProfile := c.Bool("mem-prof"); doMemoryProfile {
if c.Bool(flagMemProfile) {
defer makeMemoryProfile()
}
logging.SetLevel(c.String("log-level"))
logging.SetLevel(c.String(flagLogLevel))
logrus.
WithField("appName", b.Name).
@ -328,7 +339,7 @@ func (b *Base) run(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc {
Info("Run app")
b.CrashHandler.AddRecoveryAction(func(interface{}) error {
if c.Int("restart") > maxAllowedRestarts {
if c.Int(flagRestart) > maxAllowedRestarts {
logrus.
WithField("restart", c.Int("restart")).
Warn("Not restarting, already restarted too many times")
@ -364,3 +375,13 @@ func (b *Base) doTeardown() error {
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"
)
const (
flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp"
flagNoWindow = "no-window"
flagNonInteractive = "noninteractive"
)
func New(base *base.Base) *cli.App {
app := base.NewApp(run)
app.Flags = append(app.Flags, []cli.Flag{
&cli.StringFlag{
Name: "log-imap",
Name: flagLogIMAP,
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)"},
&cli.BoolFlag{
Name: "log-smtp",
Name: flagLogSMTP,
Usage: "Enable logging of SMTP communications (may contain decrypted data!)"},
&cli.BoolFlag{
Name: "no-window",
Name: flagNoWindow,
Usage: "Don't show window after start"},
&cli.BoolFlag{
Name: "noninteractive",
Name: flagNonInteractive,
Usage: "Start Bridge entirely noninteractively"},
}...)
@ -64,8 +71,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
if err != nil {
logrus.WithError(err).Fatal("Failed to load TLS config")
}
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.SentryReporter, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner)
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, 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)
imap.NewIMAPServer(
b.CrashHandler,
c.String("log-imap") == "client" || c.String("log-imap") == "all",
c.String("log-imap") == "server" || c.String("log-imap") == "all",
imapPort, tlsConfig, imapBackend, b.Listener).ListenAndServe()
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe()
}()
go func() {
@ -89,12 +95,12 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
smtp.NewSMTPServer(
c.Bool("log-smtp"),
c.Bool(flagLogSMTP),
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
}()
// 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.
b.AddTeardownAction(b.Versioner.RemoveOldVersions)
@ -105,9 +111,9 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
var frontendMode string
switch {
case c.Bool("cli"):
case c.Bool(base.FlagCLI):
frontendMode = "cli"
case c.Bool("noninteractive"):
case c.Bool(flagNonInteractive):
return <-(make(chan error)) // Block forever.
default:
frontendMode = "qt"
@ -118,12 +124,13 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
constants.BuildVersion,
b.Name,
frontendMode,
!c.Bool("no-window"),
!c.Bool(flagNoWindow),
b.CrashHandler,
b.Locations,
b.Settings,
b.Listener,
b.Updater,
b.UserAgent,
bridge,
smtpBackend,
b.Autostart,
@ -132,7 +139,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen]
// Watch for updates routine
go func() {
ticker := time.NewTicker(time.Hour)
ticker := time.NewTicker(constants.UpdateCheckInterval)
for {
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
switch {
case c.Bool("cli"):
case c.Bool(base.FlagCLI):
frontendMode = "cli"
default:
frontendMode = "qt"

View File

@ -26,6 +26,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/constants"
"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/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -46,16 +47,13 @@ type Bridge struct {
clientManager users.ClientManager
updater Updater
versioner Versioner
userAgentClientName string
userAgentClientVersion string
userAgentOS string
}
func New(
locations Locator,
cache Cacher,
s SettingsProvider,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler,
eventListener listener.Listener,
clientManager users.ClientManager,
@ -69,7 +67,7 @@ func New(
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)
b := &Bridge{
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.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
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)
// requires clearing all data including update files due to possibility of
// 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))
version, err := b.updater.Check()
if err != nil {
return err
return false, err
}
// We have to deal right away only with downgrade - that action needs to
// clear data and updates, and install bridge right away. But regular
// upgrade can be leaved out for periodic check.
if !b.updater.IsDowngrade(version) {
return false, nil
}
if b.updater.IsDowngrade(version) {
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 {
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,14 +21,15 @@ import (
"fmt"
"path/filepath"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
type storeFactory struct {
cache Cacher
sentryReporter *sentry.Reporter
panicHandler users.PanicHandler
clientManager users.ClientManager
eventListener listener.Listener
@ -37,12 +38,14 @@ type storeFactory struct {
func newStoreFactory(
cache Cacher,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler,
clientManager users.ClientManager,
eventListener listener.Listener,
) *storeFactory {
return &storeFactory{
cache: cache,
sentryReporter: sentryReporter,
panicHandler: panicHandler,
clientManager: clientManager,
eventListener: eventListener,
@ -53,7 +56,7 @@ func newStoreFactory(
// New creates new store for given user.
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
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.

View File

@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strconv"
"sync"
@ -73,13 +74,12 @@ func (p *keyValueStore) save() error {
p.lock.Lock()
defer p.lock.Unlock()
f, err := os.Create(p.path)
b, err := json.MarshalIndent(p.cache, "", "\t")
if err != nil {
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) {

View File

@ -72,20 +72,20 @@ func TestKeyValueStoreSetDefault(t *testing.T) {
func TestKeyValueStoreSet(t *testing.T) {
pref := newTestEmptyKeyValueStore(t)
pref.Set("str", "value")
checkSavedKeyValueStore(t, "{\"str\":\"value\"}")
checkSavedKeyValueStore(t, "{\n\t\"str\": \"value\"\n}")
}
func TestKeyValueStoreSetInt(t *testing.T) {
pref := newTestEmptyKeyValueStore(t)
pref.SetInt("int", 42)
checkSavedKeyValueStore(t, "{\"int\":\"42\"}")
checkSavedKeyValueStore(t, "{\n\t\"int\": \"42\"\n}")
}
func TestKeyValueStoreSetBool(t *testing.T) {
pref := newTestEmptyKeyValueStore(t)
pref.SetBool("trueBool", true)
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 {
@ -101,5 +101,5 @@ func newTestKeyValueStore(t *testing.T) *keyValueStore {
func checkSavedKeyValueStore(t *testing.T, expected string) {
data, err := ioutil.ReadFile(testPrefFilePath)
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"
)
// 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 {
if runtime.GOOS != "darwin" {
return false
}
return isVersionCatalinaOrNewer(getMacVersion())
}
func getMacVersion() string {
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)
rawVersion, err := exec.Command("sw_vers", "-productVersion").Output()
if err != nil {
return false
}
catalina := semver.MustParse("10.15.0")
return v.GreaterThan(catalina) || v.Equal(catalina)
return isVersionCatalinaOrNewer(strings.TrimSpace(string(rawVersion)))
}
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
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package pmapi
package useragent
import (
"fmt"
"regexp"
"runtime"
"strings"
)
// removeBrackets handle unwanted brackets in client identification string and join with given joinBy parameter.
// Mac OS X Mail/13.0 (3601.0.4) -> Mac OS X Mail/13.0-3601.0.4 (joinBy = "-")
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
type UserAgent struct {
client, platform string
}
func formatUserAgent(clientName, clientVersion, os string) string {
client := ""
if clientName != "" {
client = removeBrackets(clientName, "-")
if clientVersion != "" {
client += "/" + removeBrackets(clientVersion, "-")
func New() *UserAgent {
return &UserAgent{
client: "",
platform: runtime.GOOS,
}
}
if os == "" {
os = runtime.GOOS
}
os = removeBrackets(os, " ")
return fmt.Sprintf("%s (%s)", client, os)
}
func (ua *UserAgent) SetClient(name, version string) {
ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2"))
}
func (ua *UserAgent) HasClient() bool {
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
import (
"github.com/ProtonMail/proton-bridge/pkg/sentry"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/sirupsen/logrus"
)

View File

@ -102,10 +102,6 @@ func New( //nolint[funlen]
Aliases: []string{"p"},
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",
Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)",
Aliases: []string{"ssl", "starttls"},
@ -113,13 +109,56 @@ func New( //nolint[funlen]
})
fe.AddCmd(changeCmd)
// Check commands.
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"},
// 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.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, 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) {
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()
f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.")
return
}
} 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") {
f.settings.SetBool(settings.AllowProxyKey, true)
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"
"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"
)
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
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) {
@ -33,3 +45,68 @@ func (f *frontendCLI) printCredits(c *ishell.Context) {
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/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
@ -60,6 +61,7 @@ func New(
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
userAgent *useragent.UserAgent,
bridge *bridge.Bridge,
noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App,
@ -77,6 +79,7 @@ func New(
settings,
eventListener,
updater,
userAgent,
bridgeWrap,
noEncConfirmator,
autostart,
@ -95,6 +98,7 @@ func newBridgeFrontend(
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App,
@ -122,6 +126,7 @@ func newBridgeFrontend(
settings,
eventListener,
updater,
userAgent,
bridge,
noEncConfirmator,
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
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")
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")
}
},
@ -310,7 +310,7 @@ Dialog {
target: root
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.")
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")
answer : qsTr("Disabling early access...")
}

View File

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

View File

@ -239,6 +239,25 @@ Item {
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
Credits 1.0 Credits.qml
DialogFirstStart 1.0 DialogFirstStart.qml
DialogKeychainChange 1.0 DialogKeychainChange.qml
DialogPortChange 1.0 DialogPortChange.qml
DialogYesNo 1.0 DialogYesNo.qml
DialogTLSCertInfo 1.0 DialogTLSCertInfo.qml

View File

@ -107,17 +107,53 @@ Item {
gui.openMainWindow(false)
if (go.isConnectionOK) {
if( winMain.updateState=="noInternet") {
go.setUpdateState("upToDate")
go.updateState = "upToDate"
}
} 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
if (winMain.updateState != "forceUpdate") {
winMain.updateState = updateState
winMain.updateState = go.updateState
}
}
@ -129,15 +165,14 @@ Item {
}
onNotifyManualUpdate: {
go.setUpdateState("oldVersion")
go.updateState = "oldVersion"
}
onNotifyManualUpdateRestartNeeded: {
if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show()
}
go.setUpdateState("updateRestart")
go.updateState = "updateRestart"
winMain.dialogUpdate.finished(false)
// after manual update - just retart immidiatly
@ -147,28 +182,25 @@ Item {
onNotifyManualUpdateError: {
if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show()
}
go.setUpdateState("updateError")
go.updateState = "updateError"
winMain.dialogUpdate.finished(true)
}
onNotifyForceUpdate : {
go.setUpdateState("forceUpdate")
go.updateState = "forceUpdate"
if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
winMain.dialogUpdate.show()
}
}
onNotifySilentUpdateRestartNeeded: {
go.setUpdateState("updateRestart")
go.updateState = "updateRestart"
}
onNotifySilentUpdateError: {
go.setUpdateState("updateError")
gui.openMainWindow(true)
go.updateState = "updateError"
}
onNotifyLogout : {
@ -287,9 +319,17 @@ Item {
if (showAndRise) {
gui.winMain.showAndRise()
}
// restore update notification bar: trigger updateStateChanged
var tmp = go.updateState
go.updateState = ""
go.updateState = tmp
}
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.destroy(5000)
gui.winMain = null

View File

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

View File

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

View File

@ -66,14 +66,15 @@ Rectangle {
ClickIconText {
id: linkText
anchors.verticalCenter : message.verticalCenter
iconText : ""
iconText : " "
fontSize : root.fontSize
textUnderline: true
}
ClickIconText {
id: actionText
anchors.verticalCenter : message.verticalCenter
iconText : ""
iconText : " "
fontSize : root.fontSize
textUnderline: true
}
@ -109,26 +110,20 @@ Rectangle {
case "internetCheck":
break;
case "noInternet" :
gui.warningFlags |= Style.warnInfoBar
retryInternet.start()
secLeft=checkInterval[iTry]
break;
case "oldVersion":
gui.warningFlags |= Style.warnInfoBar
break;
case "forceUpdate":
gui.warningFlags |= Style.errorInfoBar
break;
case "upToDate":
gui.warningFlags &= ~Style.warnInfoBar
iTry = 0
secLeft=checkInterval[iTry]
break;
case "updateRestart":
gui.warningFlags |= Style.warnInfoBar
break;
case "updateError":
gui.warningFlags |= Style.errorInfoBar
break;
default :
break;
@ -247,7 +242,7 @@ Rectangle {
PropertyChanges {
target: linkText
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()
}
PropertyChanges {
@ -270,7 +265,7 @@ Rectangle {
target: closeSign
visible: true
onClicked: {
root.state = "upToDate"
go.updateState = "upToDate"
}
}
},

View File

@ -24,7 +24,7 @@ import QtQuick.Window 2.2
Window {
id: testroot
width : 150
width : 250
height : 600
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
visible : true
@ -60,7 +60,7 @@ Window {
Text {
id: systrText
anchors {
right : test_systray.right
horizontalCenter: parent.horizontalCenter
verticalCenter: test_systray.verticalCenter
}
text: "unset"
@ -281,6 +281,9 @@ Window {
property bool hasNoKeychain : true
property var availableKeychain: ["pass-app", "gnome-keyring"]
property var selectedKeychain: "gnome-keyring"
property string wrongCredentials
property string wrongMailboxPassword
property string canNotReachAPI
@ -296,6 +299,7 @@ Window {
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 updateState
property string updateVersion : "QA.1.0"
property bool updateCanInstall: true
property string updateLandingPage : "https://protonmail.com/bridge/download/"
@ -337,7 +341,6 @@ Window {
signal notifyPortIssue(bool busyPortIMAP, bool busyPortSMTP)
signal notifyVersionIsTheLatest()
signal setUpdateState(string updateState)
signal notifyKeychainRebuild()
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 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 bool updateCanInstall: true
property string updateLandingPage : "https://protonmail.com/import-export/download/"
@ -900,7 +901,6 @@ Window {
signal showQuit()
signal notifyVersionIsTheLatest()
signal setUpdateState(string updateState)
signal showMainWin()
signal hideMainWin()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@
// 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 !nogui
// +build build_qt
package qt
@ -84,7 +84,7 @@ func (s *FrontendQt) clearCache() {
channel := s.bridge.GetUpdateChannel()
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()
return
}

View File

@ -15,7 +15,7 @@
// 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 !nogui
// +build build_qt
// Package qt is the Qt User interface for Desktop bridge.
//
@ -39,16 +39,17 @@ import (
"github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"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/frontend/autoconfig"
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations"
"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/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/pkg/useragent"
"github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
"github.com/therecipe/qt/core"
@ -74,6 +75,7 @@ type FrontendQt struct {
settings *settings.Settings
eventListener listener.Listener
updater types.Updater
userAgent *useragent.UserAgent
bridge types.Bridger
noEncConfirmator types.NoEncConfirmator
@ -113,12 +115,15 @@ func New(
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App,
restarter types.Restarter,
) *FrontendQt {
tmp := &FrontendQt{
userAgent.SetPlatform(core.QSysInfo_PrettyProductName())
f := &FrontendQt{
version: version,
buildVersion: buildVersion,
programName: programName,
@ -128,6 +133,7 @@ func New(
settings: settings,
eventListener: eventListener,
updater: updater,
userAgent: userAgent,
bridge: bridge,
noEncConfirmator: noEncConfirmator,
programVer: "v" + version,
@ -137,13 +143,9 @@ func New(
// Initializing.Done is only called sync.Once. Please keep the increment
// set to 1
tmp.initializing.Add(1)
f.initializing.Add(1)
// Nicer string for OS.
currentOS := core.QSysInfo_PrettyProductName()
bridge.SetCurrentOS(currentOS)
return tmp
return f
}
// 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.SetFullversion(s.buildVersion)
// Autostart.
if s.Qml.IsFirstStart() {
if s.autostart.IsEnabled() {
// Autostart: rewrite the current definition of autostart
// - when it is the first time
// - 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 {
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.toggleAutoStart()
if err := s.autostart.Enable(); err != nil {
log.
WithField("first", s.Qml.IsFirstStart()).
WithField("wasEnabled", isAutoStartEnabled).
WithError(err).
Error("Enable on start failed.")
s.autostartError(err)
}
if s.autostart.IsEnabled() {
s.Qml.SetIsAutoStart(true)
} else {
s.Qml.SetIsAutoStart(false)
}
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) {
s.Qml.SetIsProxyAllowed(true)
@ -372,6 +382,14 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error {
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.
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 {
return s.bridge.GetCurrentClient()
return s.userAgent.String()
}
func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
@ -547,20 +565,22 @@ func (s *FrontendQt) configureAppleMail(iAccount, iAddress int) {
func (s *FrontendQt) toggleAutoStart() {
defer s.Qml.ProcessFinished()
var err error
if s.autostart.IsEnabled() {
wasEnabled := s.autostart.IsEnabled()
if wasEnabled {
err = s.autostart.Disable()
} else {
err = s.autostart.Enable()
}
isEnabled := s.autostart.IsEnabled()
if err != nil {
log.Error("Enable autostart: ", err)
log.
WithField("wasEnabled", wasEnabled).
WithField("isEnabled", isEnabled).
WithError(err).
Error("Autostart change failed.")
s.autostartError(err)
}
if s.autostart.IsEnabled() {
s.Qml.SetIsAutoStart(true)
} else {
s.Qml.SetIsAutoStart(false)
}
s.Qml.SetIsAutoStart(isEnabled)
}
func (s *FrontendQt) toggleAutoUpdate() {
@ -585,14 +605,16 @@ func (s *FrontendQt) toggleEarlyAccess() {
channel = updater.EarlyChannel
}
err := s.bridge.SetUpdateChannel(channel)
needRestart, err := s.bridge.SetUpdateChannel(channel)
s.Qml.SetIsEarlyAccess(channel == updater.EarlyChannel)
if err != nil {
s.Qml.NotifyManualUpdateError()
return
}
if needRestart {
s.restarter.SetToRestart()
s.App.Quit()
}
}
func (s *FrontendQt) toggleAllowProxy() {
@ -711,3 +733,16 @@ func (s *FrontendQt) setGUIIsReady() {
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
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build nogui
// +build !build_qt
package qt
@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/go-autostart"
"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/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
@ -71,6 +72,7 @@ func New(
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App,

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@
// 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 !nogui
// +build build_qt
package qt
@ -50,6 +50,7 @@ type GoQMLInterface struct {
_ string `property:"fullversion"`
_ string `property:"downloadLink"`
_ string `property:"updateState"`
_ string `property:"updateVersion"`
_ bool `property:"updateCanInstall"`
_ string `property:"updateLandingPage"`
@ -66,6 +67,9 @@ type GoQMLInterface struct {
_ func() `slot:"startManualUpdate"`
_ func() `slot:"guiIsReady"`
_ []string `property:"availableKeychain"`
_ string `property:"selectedKeychain"`
// Translations.
_ string `property:"wrongCredentials"`
_ string `property:"wrongMailboxPassword"`
@ -80,7 +84,6 @@ type GoQMLInterface struct {
_ string `property:"progressDescription"`
_ func(isAvailable bool) `signal:"setConnectionStatus"`
_ func(updateState string) `signal:"setUpdateState"`
_ func() `slot:"checkInternet"`
_ func() `slot:"setToRestart"`
@ -209,4 +212,7 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc)
s.ConnectShouldSendAnswer(f.shouldSendAnswer)
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="Credits.qml" >./qml/BridgeUI/Credits.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="DialogYesNo.qml" >./qml/BridgeUI/DialogYesNo.qml</file>
<file alias="DialogTLSCertInfo.qml" >./qml/BridgeUI/DialogTLSCertInfo.qml</file>

View File

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

View File

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

View File

@ -23,13 +23,13 @@ import (
)
type currentClientSetter interface {
SetCurrentClient(name, version string)
SetClient(name, version string)
}
// Extension for IMAP server
type extension struct {
extID imapserver.ConnExtension
setter currentClientSetter
clientSetter currentClientSetter
}
func (ext *extension) Capabilities(conn imapserver.Conn) []string {
@ -45,7 +45,7 @@ func (ext *extension) Command(name string) imapserver.HandlerFactory {
if hdlrID, ok := newIDHandler().(*imapid.Handler); ok {
return &handler{
hdlrID: hdlrID,
setter: ext.setter,
clientSetter: ext.clientSetter,
}
}
return nil
@ -58,7 +58,7 @@ func (ext *extension) NewConn(conn imapserver.Conn) imapserver.Conn {
type handler struct {
hdlrID *imapid.Handler
setter currentClientSetter
clientSetter currentClientSetter
}
func (hdlr *handler) Parse(fields []interface{}) error {
@ -69,21 +69,18 @@ func (hdlr *handler) Handle(conn imapserver.Conn) error {
err := hdlr.hdlrID.Handle(conn)
if err == nil {
id := hdlr.hdlrID.Command.ID
hdlr.setter.SetCurrentClient(
id[imapid.FieldName],
id[imapid.FieldVersion],
)
hdlr.clientSetter.SetClient(id[imapid.FieldName], id[imapid.FieldVersion])
}
return err
}
// NewExtension returns extension which is adding RFC2871 ID capability, with
// 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 {
return &extension{
extID: conExtID,
setter: setter,
clientSetter: clientSetter,
}
}
return nil

View File

@ -19,6 +19,7 @@ package imap
import (
"strings"
"time"
"github.com/ProtonMail/proton-bridge/pkg/message"
"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.
func (im *imapMailbox) Name() string {
// 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
// from the currently selected mailbox.
func (im *imapMailbox) Expunge() error {
// Wait for any APPENDS to finish in order to avoid data loss when
// Outlook sends commands too quickly STORE \Deleted, APPEND, EXPUNGE,
// APPEND FINISHED:
//
// 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()
// See comment of appendExpungeLock.
if im.storeMailbox.LabelID() == pmapi.TrashLabel || im.storeMailbox.LabelID() == pmapi.SpamLabel {
im.user.appendExpungeLock.Lock()
defer im.user.appendExpungeLock.Unlock()
}
return im.logCommand(im.expunge, "EXPUNGE")
}
func (im *imapMailbox) expunge() error {
im.user.backend.updates.block(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
// and UID passed from SeqSet from the currently selected mailbox.
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)
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
// 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.
defer im.panicHandler.HandlePanic()
im.user.appendStarted()
defer im.user.appendFinished()
m, _, _, readers, err := message.Parse(body)
if err != nil {
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
// new label instead. This always happens with Outlook (it uses APPEND
// instead of COPY).

View File

@ -38,6 +38,12 @@ import (
// If the Backend implements Updater, it must notify the client immediately
// via a message update.
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{
"flags": flags,
"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
// be preserved, and the Recent flag SHOULD be set, in the copy.
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.
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
// responses.
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.
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.
//
// 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() {
close(msgResponse)
if err != nil {

View File

@ -28,6 +28,7 @@ import (
imapid "github.com/ProtonMail/go-imap-id"
"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/imap/id"
"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
@ -39,6 +40,7 @@ import (
imapmove "github.com/emersion/go-imap-move"
imapquota "github.com/emersion/go-imap-quota"
imapunselect "github.com/emersion/go-imap-unselect"
"github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl"
"github.com/sirupsen/logrus"
@ -47,6 +49,7 @@ import (
type imapServer struct {
panicHandler panicHandler
server *imapserver.Server
userAgent *useragent.UserAgent
eventListener listener.Listener
debugClient bool
debugServer bool
@ -55,7 +58,7 @@ type imapServer struct {
}
// 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.Addr = fmt.Sprintf("%v:%v", bridge.Host, port)
s.TLSConfig = tls
@ -93,7 +96,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
s.Enable(
imapidle.NewExtension(),
imapmove.NewExtension(),
id.NewExtension(serverID, imapBackend.bridge),
id.NewExtension(serverID, userAgent),
imapquota.NewExtension(),
imapappendlimit.NewExtension(),
imapunselect.NewExtension(),
@ -103,6 +106,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por
server := &imapServer{
panicHandler: panicHandler,
server: s,
userAgent: userAgent,
eventListener: eventListener,
debugClient: debugClient,
debugServer: debugServer,
@ -144,9 +148,10 @@ func (s *imapServer) listenAndServe(retries int) {
return
}
err = s.server.Serve(&debugListener{
err = s.server.Serve(&connListener{
Listener: l,
server: s,
userAgent: s.userAgent,
})
// Serve returns error every time, even after closing the server.
// 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.
type debugListener struct {
type connListener struct {
net.Listener
server *imapServer
userAgent *useragent.UserAgent
}
func (dl *debugListener) Accept() (net.Conn, error) {
conn, err := dl.Listener.Accept()
func (l *connListener) Accept() (net.Conn, error) {
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
if addr := conn.LocalAddr(); addr != nil {
debugLog = debugLog.WithField("loc", addr.String())
@ -254,14 +260,18 @@ func (dl *debugListener) Accept() (net.Conn, error) {
}
var localDebug, remoteDebug io.Writer
if dl.server.debugServer {
if l.server.debugServer {
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)
}
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

View File

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

View File

@ -41,7 +41,22 @@ type imapUser struct {
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.
@ -218,8 +233,9 @@ func (iu *imapUser) GetQuota(name string) (*imapquota.Status, error) {
resources := make(map[string][2]uint32)
var list [2]uint32
list[0] = uint32(usedSpace / 1000)
list[1] = uint32(maxSpace / 1000)
// Quota is "in units of 1024 octets" (or KB) and PM returns bytes.
list[0] = uint32(usedSpace / 1024)
list[1] = uint32(maxSpace / 1024)
resources[imapquota.ResourceStorage] = list
status := &imapquota.Status{
Name: "",
@ -250,15 +266,3 @@ func (iu *imapUser) CreateMessageLimit() *uint32 {
upload := uint32(maxUpload)
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 {
appName string
appVersion string
uap userAgentProvider
userAgent fmt.Stringer
}
// 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{
appName: appName,
appVersion: appVersion,
userAgent: userAgent,
}
}
func (r *Reporter) SetUserAgentProvider(uap userAgentProvider) {
r.uap = uap
}
func (r *Reporter) ReportException(i interface{}) error {
err := fmt.Errorf("recover: %v", i)
@ -97,19 +90,11 @@ func (r *Reporter) scopedReport(doReport func()) error {
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{
"OS": runtime.GOOS,
"Client": r.appName,
"Version": r.appVersion,
"UserAgent": userAgent,
"UserAgent": r.userAgent.String(),
"UserID": "",
}

View File

@ -35,8 +35,8 @@ func TestSkipDuringUnwind(t *testing.T) {
}()
wantSkippedFunctions := []string{
"github.com/ProtonMail/proton-bridge/pkg/sentry.TestSkipDuringUnwind",
"github.com/ProtonMail/proton-bridge/pkg/sentry.TestSkipDuringUnwind.func1",
"github.com/ProtonMail/proton-bridge/internal/sentry.TestSkipDuringUnwind",
"github.com/ProtonMail/proton-bridge/internal/sentry.TestSkipDuringUnwind.func1",
}
r.Equal(t, wantSkippedFunctions, skippedFunctions)
}
@ -45,8 +45,8 @@ func TestFilterOutPanicHandlers(t *testing.T) {
skippedFunctions = []string{
"github.com/ProtonMail/proton-bridge/pkg/config.(*PanicHandler).HandlePanic",
"github.com/ProtonMail/proton-bridge/pkg/config.HandlePanic",
"github.com/ProtonMail/proton-bridge/pkg/sentry.ReportSentryCrash",
"github.com/ProtonMail/proton-bridge/pkg/sentry.ReportSentryCrash.func1",
"github.com/ProtonMail/proton-bridge/internal/sentry.ReportSentryCrash",
"github.com/ProtonMail/proton-bridge/internal/sentry.ReportSentryCrash.func1",
}
frames := []sentry.Frame{
@ -57,8 +57,8 @@ func TestFilterOutPanicHandlers(t *testing.T) {
{Module: "main", Function: "run"},
{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/sentry", Function: "ReportSentryCrash"},
{Module: "github.com/ProtonMail/proton-bridge/pkg/sentry", Function: "ReportSentryCrash.func1"},
{Module: "github.com/ProtonMail/proton-bridge/internal/sentry", Function: "ReportSentryCrash"},
{Module: "github.com/ProtonMail/proton-bridge/internal/sentry", Function: "ReportSentryCrash.func1"},
}
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.
defer su.panicHandler.HandlePanic()
b := new(bytes.Buffer)
messageReader = io.TeeReader(messageReader, b)
mailSettings, err := su.client().GetMailSettings()
if err != nil {
return err
@ -405,6 +409,8 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader
req.PreparePackages()
dumpMessageData(b.Bytes(), message.Subject)
return su.storeUser.SendMessage(message.ID, req)
}

View File

@ -28,8 +28,13 @@ import (
"github.com/sirupsen/logrus"
)
const pollInterval = 30 * time.Second
const pollIntervalSpread = 5 * time.Second
const (
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 {
cache *Cache
@ -41,6 +46,7 @@ type eventLoop struct {
isRunning bool // The whole event loop is running.
pollCounter int
errCounter int
log *logrus.Entry
@ -227,9 +233,18 @@ func (loop *eventLoop) processNextEvent() (more bool, err error) { // nolint[fun
_, 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.
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
}
}()
@ -283,6 +298,10 @@ func (loop *eventLoop) processEvent(event *pmapi.Event) (err error) {
eventLog.Info("Processing refresh event")
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
}

View File

@ -24,6 +24,7 @@ import (
"sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/hashicorp/go-multierror"
@ -95,6 +96,7 @@ var (
// Store is local user storage, which handles the synchronization between IMAP and PM API.
type Store struct {
sentryReporter *sentry.Reporter
panicHandler PanicHandler
eventLoop *eventLoop
user BridgeUser
@ -115,7 +117,8 @@ type Store struct {
}
// New creates or opens a store for the given `user`.
func New(
func New( // nolint[funlen]
sentryReporter *sentry.Reporter,
panicHandler PanicHandler,
user BridgeUser,
clientManager ClientManager,
@ -145,6 +148,7 @@ func New(
}
store = &Store{
sentryReporter: sentryReporter,
panicHandler: panicHandler,
clientManager: clientManager,
user: user,

View File

@ -125,6 +125,7 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool, msgs ...*pmapi.M
var err error
mocks.store, err = New(
nil, // Sentry reporter is not used under unit tests.
mocks.panicHandler,
mocks.user,
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)
}
// 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
type MockCredentialsStorer struct {
ctrl *gomock.Controller

View File

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

View File

@ -26,6 +26,7 @@ import (
"time"
"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/users/credentials"
usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
@ -185,9 +186,10 @@ func initMocks(t *testing.T) mocks {
// Set up store factory.
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")
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()
m.storeMaker.EXPECT().Remove(gomock.Any()).AnyTimes()

View File

@ -19,10 +19,12 @@ package keychain
import (
"os/exec"
"reflect"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/pass"
"github.com/docker/docker-credential-helpers/secretservice"
"github.com/sirupsen/logrus"
)
const (
@ -43,9 +45,9 @@ func init() { // nolint[noinit]
// If Pass 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
} else if _, ok := Helpers[GnomeKeyring]; ok {
} else if _, ok := Helpers[GnomeKeyring]; ok && isUsable(newGnomeKeyringHelper("")) {
defaultHelper = GnomeKeyring
}
}
@ -57,3 +59,36 @@ func newPassHelper(string) (credentials.Helper, error) {
func newGnomeKeyringHelper(string) (credentials.Helper, error) {
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.
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.
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]
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)
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.
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
for {
_, err := io.CopyN(&buffer, data, c.cm.config.MinBytesPerSecond*speedCheckSeconds)
timer.Stop()
timer.Reset(speedCheckSeconds * time.Second)
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
timer.Reset(speedCheckSeconds * time.Second)
}
return ioutil.ReadAll(&buffer)

View File

@ -172,7 +172,7 @@ func TestClient_FirstReadTimeout(t *testing.T) {
func TestClient_MinSpeedTimeout(t *testing.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()

View File

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

View File

@ -17,8 +17,10 @@
package pmapi
import "github.com/ProtonMail/proton-bridge/internal/config/useragent"
func newTestClientManager(cfg *ClientConfig) *ClientManager {
cm := NewClientManager(cfg)
cm := NewClientManager(cfg, useragent.New())
go func() {
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[:]))
}
type clientConfigProvider interface {
GetClientConfig() *ClientConfig
type clientInfoProvider interface {
GetAppVersion() string
GetUserAgent() string
}
type tlsReporter struct {
cm clientConfigProvider
cm clientInfoProvider
p *pinChecker
sentReports []sentReport
}
func newTLSReporter(p *pinChecker, cm clientConfigProvider) *tlsReporter {
func newTLSReporter(p *pinChecker, cm clientInfoProvider) *tlsReporter {
return &tlsReporter{
cm: cm,
p: p,
@ -102,13 +103,14 @@ func (r *tlsReporter) reportCertIssue(remoteURI, host, port string, connState tl
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) {
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"
)
type fakeClientConfigProvider struct {
type fakeClientInfoProvider struct {
version, useragent string
}
func (c *fakeClientConfigProvider) GetClientConfig() *ClientConfig {
return &ClientConfig{AppVersion: c.version, UserAgent: c.useragent}
func (c *fakeClientInfoProvider) GetAppVersion() string {
return c.version
}
func (c *fakeClientInfoProvider) GetUserAgent() string {
return c.useragent
}
func TestPinCheckerDoubleReport(t *testing.T) {
@ -42,7 +46,7 @@ func TestPinCheckerDoubleReport(t *testing.T) {
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.
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
- 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
- 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 "([^"]*)" has messages$`, apiMailboxForUserHasMessages)
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 {
@ -187,12 +187,11 @@ func getPMAPIMessages(account *accounts.TestAccount, mailboxName string) ([]*pma
return ctx.GetPMAPIController().GetMessages(account.Username(), labelID)
}
func clientManagerUserAgent(expectedUserAgent string) error {
func userAgent(expectedUserAgent string) error {
expectedUserAgent = strings.ReplaceAll(expectedUserAgent, "[GOOS]", runtime.GOOS)
assert.Eventually(ctx.GetTestingT(), func() bool {
userAgent := ctx.GetClientManager().GetUserAgent()
return userAgent == expectedUserAgent
return ctx.GetUserAgent() == expectedUserAgent
}, 5*time.Second, time.Second)
return nil

View File

@ -21,6 +21,9 @@ import (
"time"
"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/pkg/listener"
)
@ -67,8 +70,9 @@ func newBridgeInstance(
eventListener listener.Listener,
clientManager users.ClientManager,
) *bridge.Bridge {
sentryReporter := sentry.NewReporter("bridge", constants.Version, useragent.New())
panicHandler := &panicHandler{t: t}
updater := newFakeUpdater()
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"
"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/importexport"
"github.com/ProtonMail/proton-bridge/internal/transfer"
@ -46,6 +47,7 @@ type TestContext struct {
locations *fakeLocations
settings *fakeSettings
listener listener.Listener
userAgent *useragent.UserAgent
testAccounts *accounts.TestAccounts
// pmapiController is used to control real or fake pmapi clients.
@ -95,11 +97,12 @@ type TestContext struct {
func New(app string) *TestContext {
setLogrusVerbosityFromEnv()
configName := app
if app == "ie" {
configName = "importExport"
}
cm := pmapi.NewClientManager(pmapi.GetAPIConfig(configName, constants.Version))
userAgent := useragent.New()
cm := pmapi.NewClientManager(
pmapi.GetAPIConfig(getConfigName(app), constants.Version),
userAgent,
)
ctx := &TestContext{
t: &bddT{},
@ -107,6 +110,7 @@ func New(app string) *TestContext {
locations: newFakeLocations(),
settings: newFakeSettings(),
listener: listener.New(),
userAgent: userAgent,
pmapiController: newPMAPIController(cm),
clientManager: cm,
testAccounts: newTestAccounts(),
@ -137,6 +141,14 @@ func New(app string) *TestContext {
return ctx
}
func getConfigName(app string) string {
if app == "ie" {
return "importExport"
}
return app
}
// 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.
func (ctx *TestContext) Cleanup() *TestContext {
@ -156,6 +168,11 @@ func (ctx *TestContext) GetClientManager() *pmapi.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.
func (ctx *TestContext) GetTestingT() *bddT { //nolint[golint]
return ctx.t

View File

@ -59,7 +59,7 @@ func (ctx *TestContext) withIMAPServer() {
tls, _ := tls.New(settingsPath).GetConfig()
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()
require.NoError(ctx.t, waitForPort(port, 5*time.Second))

View File

@ -4,21 +4,23 @@ Feature: User agent
Scenario: Get user agent
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:
"""
"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
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:
"""
"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:
"""
"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