diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ddb16c05..7062ae78 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,7 +16,7 @@
# along with ProtonMail Bridge. If not, see .
---
-image: harbor.protontech.ch/docker.io/library/golang:1.18
+image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
variables:
GOPRIVATE: gitlab.protontech.ch
@@ -99,7 +99,7 @@ test-linux:
- .rules-branch-manual-MR-and-devel-always
- .after-script-code-coverage
tags:
- - medium
+ - large
test-linux-race:
extends:
@@ -126,10 +126,10 @@ test-integration-race:
.windows-base:
before_script:
- - export GOROOT=/c/Go1.18
+ - export GOROOT=/c/Go1.20
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- - export GOPATH=~/go18
+ - export GOPATH=~/go1.20
- export GO111MODULE=on
- export PATH=$GOPATH/bin:$PATH
- export MSYSTEM=
@@ -172,7 +172,7 @@ test-windows:
.linux-build-setup:
- image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
+ image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
@@ -209,7 +209,7 @@ build-linux-qa:
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- - export GOPATH=~/go
+ - export GOPATH=~/go1.20
- export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
@@ -231,10 +231,10 @@ build-darwin-qa:
.windows-build-setup:
before_script:
- - export GOROOT=/c/Go1.18/
+ - export GOROOT=/c/Go1.20/
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- - export GOPATH=~/go18
+ - export GOPATH=~/go1.20
- export GO111MODULE=on
- export PATH="${GOPATH}/bin:${PATH}"
- export MSYSTEM=
diff --git a/.golangci.yml b/.golangci.yml
index 6149b688..b44fe6d5 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -48,16 +48,13 @@ linters:
disable-all: true
enable:
- - deadcode # Finds unused code [fast: true, auto-fix: false]
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
- - structcheck # Finds unused struct fields [fast: true, auto-fix: false]
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
- - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false]
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
@@ -119,3 +116,8 @@ linters:
# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
# - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
+
+ # Deprecated:
+ # - structcheck # Finds unused struct fields [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
+ # - deadcode # Finds unused code [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
+ # - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
\ No newline at end of file
diff --git a/BUILDS.md b/BUILDS.md
index eb4abc7a..237694bd 100644
--- a/BUILDS.md
+++ b/BUILDS.md
@@ -3,7 +3,7 @@
## Prerequisites
* 64-bit OS:
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
-* Go 1.18
+* Go 1.20
* Bash with basic build utils: make, gcc, sed, find, grep, ...
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (Linux), msvc (Windows) or Xcode (macOS)
diff --git a/Makefile b/Makefile
index 4fbd1e79..7d021279 100644
--- a/Makefile
+++ b/Makefile
@@ -183,7 +183,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
-LINTVER:="v1.50.0"
+LINTVER:="v1.52.2"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@@ -228,13 +228,13 @@ change-copyright-year:
./utils/missing_license.sh change-year
test: gofiles
- go test -v -timeout=10m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
+ go test -v -timeout=20m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
test-race: gofiles
- go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
+ go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
test-integration: gofiles
- go test -v -timeout=20m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
+ go test -v -timeout=60m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
test-integration-debug: gofiles
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
diff --git a/extern/vcpkg b/extern/vcpkg
index f93ba152..d4d39d71 160000
--- a/extern/vcpkg
+++ b/extern/vcpkg
@@ -1 +1 @@
-Subproject commit f93ba152d55e1d243160e690bc302ffe8638358e
+Subproject commit d4d39d71b3e6dd7536592c36ab2f7e84a8a64942
diff --git a/go.mod b/go.mod
index 4819274a..d109e84c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,18 +1,18 @@
module github.com/ProtonMail/proton-bridge/v3
-go 1.18
+go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
- github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae
+ github.com/ProtonMail/gluon v0.16.1-0.20230523090642-633e61ce9bc2
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
- github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be
+ github.com/ProtonMail/go-proton-api v0.4.1-0.20230523092337-ea8de5f674b7
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
- github.com/bradenaw/juniper v0.10.2
+ github.com/bradenaw/juniper v0.12.0
github.com/cucumber/godog v0.12.5
github.com/cucumber/messages-go/v16 v16.0.1
github.com/docker/docker-credential-helpers v0.6.3
@@ -37,15 +37,15 @@ require (
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.7.0
- github.com/sirupsen/logrus v1.9.0
+ github.com/sirupsen/logrus v1.9.2
github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.24.4
github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1
- golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
- golang.org/x/net v0.8.0
- golang.org/x/sys v0.6.0
- golang.org/x/text v0.8.0
+ golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
+ golang.org/x/net v0.10.0
+ golang.org/x/sys v0.8.0
+ golang.org/x/text v0.9.0
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
howett.net/plist v1.0.0
@@ -55,17 +55,17 @@ require (
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
entgo.io/ent v0.11.8 // indirect
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
- github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800 // indirect
+ github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
- github.com/ProtonMail/go-srp v0.0.5 // indirect
+ github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/agext/levenshtein v1.2.3 // indirect
- github.com/andybalholm/cascadia v1.3.1 // indirect
+ github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/bytedance/sonic v1.8.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chzyer/test v1.0.0 // indirect
- github.com/cloudflare/circl v1.3.2 // indirect
+ github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
@@ -73,7 +73,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/go-windows v1.0.1 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
- github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
+ github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
github.com/felixge/fgprof v0.9.3 // indirect
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
@@ -116,9 +116,9 @@ require (
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zclconf/go-cty v1.12.1 // indirect
golang.org/x/arch v0.2.0 // indirect
- golang.org/x/crypto v0.7.0 // indirect
+ golang.org/x/crypto v0.9.0 // indirect
golang.org/x/mod v0.8.0 // indirect
- golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sync v0.2.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -127,5 +127,5 @@ require (
replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
- github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe
+ github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
)
diff --git a/go.sum b/go.sum
index d4dba3bc..198714dd 100644
--- a/go.sum
+++ b/go.sum
@@ -28,21 +28,22 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
-github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae h1:3p8P21+BoAYj1nSswdwQvc7jr2lixuVFpWE4QlvA8f0=
-github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI=
+github.com/ProtonMail/gluon v0.16.1-0.20230523090642-633e61ce9bc2 h1:EFmaapQ2BM5OZ16+/c03108+wAt5nq1m/eCzHMl2Vg4=
+github.com/ProtonMail/gluon v0.16.1-0.20230523090642-633e61ce9bc2/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
-github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800 h1:o8/VQLSiuRkkSAfVOpFCG1GnTsWxFIOPLvJ2O7hJcFg=
+github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
+github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
+github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
-github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be h1:TNHnEyUQDf97CRGCFWLxg7I5ASSEMO3TN2lbNw2cD6U=
-github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28=
-github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
-github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
+github.com/ProtonMail/go-proton-api v0.4.1-0.20230523092337-ea8de5f674b7 h1:LL+cERFLR5m3AKr6G58AVpsSuQQXulYf9WWWJ+2HUkY=
+github.com/ProtonMail/go-proton-api v0.4.1-0.20230523092337-ea8de5f674b7/go.mod h1:e3EhDR9nqGf4sR6OLTBuJ9JmPnB/RLC/U7q0mN11Vmo=
+github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
+github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
@@ -57,8 +58,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
-github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
+github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
+github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -68,8 +70,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/bradenaw/juniper v0.10.2 h1:EY7r8SJJrigJ7lvWk6ews3K5RD4XTG9z+WSwHJKijP4=
-github.com/bradenaw/juniper v0.10.2/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
+github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
+github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
@@ -87,8 +89,8 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
-github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk=
-github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
+github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
+github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -106,8 +108,8 @@ github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
-github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe h1:KRj3wdvA9yE92prNmOjS7x5DOqoyjxqdE30qnrmTasc=
-github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
+github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768 h1:Jrcoxtrk4qpuzKIYPlEkjIK0M+bABs0oW2QzrOuwlzk=
+github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
@@ -131,8 +133,8 @@ github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
-github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik=
-github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
+github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
+github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@@ -362,8 +364,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
+github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@@ -430,17 +432,17 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
-golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -480,8 +482,10 @@ golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -492,8 +496,9 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -524,12 +529,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -538,8 +546,9 @@ golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhO
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/internal/app/app.go b/internal/app/app.go
index 16487e51..f2c15178 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -19,14 +19,12 @@ package app
import (
"fmt"
- "math/rand"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"runtime"
- "time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
@@ -160,9 +158,6 @@ func New() *cli.App {
}
func run(c *cli.Context) error {
- // Seed the default RNG from the math/rand package.
- rand.Seed(time.Now().UnixNano())
-
// Get the current bridge version.
version, err := semver.NewVersion(constants.Version)
if err != nil {
diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go
index 29a87b1f..79453989 100644
--- a/internal/bridge/bridge.go
+++ b/internal/bridge/bridge.go
@@ -29,7 +29,6 @@ import (
"time"
"github.com/Masterminds/semver/v3"
- "github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/async"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/imap"
@@ -45,7 +44,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/bradenaw/juniper/xslices"
- "github.com/emersion/go-smtp"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
)
@@ -67,13 +65,7 @@ type Bridge struct {
tlsConfig *tls.Config
// imapServer is the bridge's IMAP server.
- imapServer *gluon.Server
- imapListener net.Listener
- imapEventCh chan imapEvents.Event
-
- // smtpServer is the bridge's SMTP server.
- smtpServer *smtp.Server
- smtpListener net.Listener
+ imapEventCh chan imapEvents.Event
// updater is the bridge's updater.
updater Updater
@@ -134,6 +126,8 @@ type Bridge struct {
goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator
+
+ serverManager *ServerManager
}
// New creates a new bridge.
@@ -224,16 +218,6 @@ func newBridge(
return nil, fmt.Errorf("failed to load TLS config: %w", err)
}
- gluonCacheDir, err := getGluonDir(vault)
- if err != nil {
- return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
- }
-
- gluonDataDir, err := locator.ProvideGluonDataPath()
- if err != nil {
- return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
- }
-
firstStart := vault.GetFirstStart()
if err := vault.SetFirstStart(false); err != nil {
return nil, fmt.Errorf("failed to save first start indicator: %w", err)
@@ -246,23 +230,6 @@ func newBridge(
identifier.SetClientString(vault.GetLastUserAgent())
- imapServer, err := newIMAPServer(
- gluonCacheDir,
- gluonDataDir,
- curVersion,
- tlsConfig,
- reporter,
- logIMAPClient,
- logIMAPServer,
- imapEventCh,
- tasks,
- uidValidityGenerator,
- panicHandler,
- )
- if err != nil {
- return nil, fmt.Errorf("failed to create IMAP server: %w", err)
- }
-
focusService, err := focus.NewService(locator, curVersion, panicHandler)
if err != nil {
return nil, fmt.Errorf("failed to create focus service: %w", err)
@@ -279,7 +246,6 @@ func newBridge(
identifier: identifier,
tlsConfig: tlsConfig,
- imapServer: imapServer,
imapEventCh: imapEventCh,
updater: updater,
@@ -306,9 +272,13 @@ func newBridge(
tasks: tasks,
uidValidityGenerator: uidValidityGenerator,
+
+ serverManager: newServerManager(),
}
- bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
+ if err := bridge.serverManager.Init(bridge); err != nil {
+ return nil, err
+ }
return bridge, nil
}
@@ -381,10 +351,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
})
})
- // We need to load users before we can start the IMAP and SMTP servers.
- // We must only start the servers once.
- var once sync.Once
-
// Attempt to load users from the vault when triggered.
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
if err := bridge.loadUsers(ctx); err != nil {
@@ -396,17 +362,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
}
bridge.publish(events.AllUsersLoaded{})
-
- // Once all users have been loaded, start the bridge's IMAP and SMTP servers.
- once.Do(func() {
- if err := bridge.serveIMAP(); err != nil {
- logrus.WithError(err).Error("Failed to start IMAP server")
- }
-
- if err := bridge.serveSMTP(); err != nil {
- logrus.WithError(err).Error("Failed to start SMTP server")
- }
- })
})
defer bridge.goLoad()
@@ -452,18 +407,13 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge")
- // Close the IMAP server.
- if err := bridge.closeIMAP(ctx); err != nil {
- logrus.WithError(err).Error("Failed to close IMAP server")
- }
-
- // Close the SMTP server.
- if err := bridge.closeSMTP(); err != nil {
- logrus.WithError(err).Error("Failed to close SMTP server")
+ // Close the servers
+ if err := bridge.serverManager.CloseServers(ctx); err != nil {
+ logrus.WithError(err).Error("Failed to close servers")
}
// Close all users.
- safe.RLock(func() {
+ safe.Lock(func() {
for _, user := range bridge.users {
user.Close()
}
diff --git a/internal/bridge/bridge_test.go b/internal/bridge/bridge_test.go
index 65939daa..676eb612 100644
--- a/internal/bridge/bridge_test.go
+++ b/internal/bridge/bridge_test.go
@@ -50,7 +50,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id"
- "github.com/emersion/go-imap/client"
"github.com/stretchr/testify/require"
)
@@ -173,11 +172,27 @@ func TestBridge_UserAgent(t *testing.T) {
func TestBridge_UserAgent_Persistence(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
+ otherPassword := []byte("bar")
+ otherUser := "foo"
+ _, _, err := s.CreateUser(otherUser, otherPassword)
+ require.NoError(t, err)
+
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
+ imapWaiter := waitForIMAPServerReady(b)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(b)
+ defer smtpWaiter.Done()
+
+ require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
+
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+
currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, vault.DefaultUserAgent)
- imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
defer func() { _ = imapClient.Logout() }()
@@ -220,8 +235,24 @@ func TestBridge_UserAgentFromIMAPID(t *testing.T) {
calls = append(calls, call)
})
+ otherPassword := []byte("bar")
+ otherUser := "foo"
+ _, _, err := s.CreateUser(otherUser, otherPassword)
+ require.NoError(t, err)
+
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
- imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ imapWaiter := waitForIMAPServerReady(b)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(b)
+ defer smtpWaiter.Done()
+
+ require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
+
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+
+ imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
defer func() { _ = imapClient.Logout() }()
@@ -592,10 +623,22 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
func TestBridge_LoginFailed(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ imapWaiter := waitForIMAPServerReady(bridge)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
defer done()
- imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
+ _, err := bridge.LoginFull(ctx, username, password, nil, nil)
+ require.NoError(t, err)
+
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+
+ imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.Error(t, imapClient.Login("badUser", "badPass"))
@@ -622,6 +665,12 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
configDir, err := b.GetGluonDataDir()
require.NoError(t, err)
+ imapWaiter := waitForIMAPServerReady(b)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(b)
+ defer smtpWaiter.Done()
+
// Login the user.
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
@@ -655,7 +704,10 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@@ -695,7 +747,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@@ -716,7 +768,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@@ -778,6 +830,7 @@ func withBridgeNoMocks(
locator bridge.Locator,
vaultKey []byte,
tests func(*bridge.Bridge),
+ waitOnServers bool,
) {
// Bridge will disable the proxy by default at startup.
mocks.ProxyCtl.EXPECT().DisallowProxy()
@@ -828,14 +881,17 @@ func withBridgeNoMocks(
// Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{})
- // Wait for bridge to start the IMAP server.
- waitForEvent(t, eventCh, events.IMAPServerReady{})
- // Wait for bridge to start the SMTP server.
- waitForEvent(t, eventCh, events.SMTPServerReady{})
// Set random IMAP and SMTP ports for the tests.
- require.NoError(t, bridge.SetIMAPPort(0))
- require.NoError(t, bridge.SetSMTPPort(0))
+ require.NoError(t, bridge.SetIMAPPort(ctx, 0))
+ require.NoError(t, bridge.SetSMTPPort(ctx, 0))
+
+ if waitOnServers {
+ // Wait for bridge to start the IMAP server.
+ waitForEvent(t, eventCh, events.IMAPServerReady{})
+ // Wait for bridge to start the SMTP server.
+ waitForEvent(t, eventCh, events.SMTPServerReady{})
+ }
// Close the bridge when done.
defer bridge.Close(ctx)
@@ -857,7 +913,24 @@ func withBridge(
withMocks(t, func(mocks *bridge.Mocks) {
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
tests(bridge, mocks)
- })
+ }, false)
+ })
+}
+
+// withBridgeWaitForServers is the same as withBridge, but it will wait until IMAP & SMTP servers are ready.
+func withBridgeWaitForServers(
+ ctx context.Context,
+ t *testing.T,
+ apiURL string,
+ netCtl *proton.NetCtl,
+ locator bridge.Locator,
+ vaultKey []byte,
+ tests func(*bridge.Bridge, *bridge.Mocks),
+) {
+ withMocks(t, func(mocks *bridge.Mocks) {
+ withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
+ tests(bridge, mocks)
+ }, true)
})
}
@@ -910,3 +983,48 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
return outCh, done
}
+
+type eventWaiter struct {
+ evtCh <-chan events.Event
+ cancel func()
+}
+
+func (e *eventWaiter) Done() {
+ e.cancel()
+}
+
+func (e *eventWaiter) Wait() {
+ <-e.evtCh
+}
+
+func waitForSMTPServerReady(b *bridge.Bridge) *eventWaiter {
+ evtCh, cancel := b.GetEvents(events.SMTPServerReady{})
+ return &eventWaiter{
+ evtCh: evtCh,
+ cancel: cancel,
+ }
+}
+
+func waitForSMTPServerStopped(b *bridge.Bridge) *eventWaiter {
+ evtCh, cancel := b.GetEvents(events.SMTPServerStopped{})
+ return &eventWaiter{
+ evtCh: evtCh,
+ cancel: cancel,
+ }
+}
+
+func waitForIMAPServerReady(b *bridge.Bridge) *eventWaiter {
+ evtCh, cancel := b.GetEvents(events.IMAPServerReady{})
+ return &eventWaiter{
+ evtCh: evtCh,
+ cancel: cancel,
+ }
+}
+
+func waitForIMAPServerStopped(b *bridge.Bridge) *eventWaiter {
+ evtCh, cancel := b.GetEvents(events.IMAPServerStopped{})
+ return &eventWaiter{
+ evtCh: evtCh,
+ cancel: cancel,
+ }
+}
diff --git a/internal/bridge/configure.go b/internal/bridge/configure.go
index 5b323b0a..6becbff1 100644
--- a/internal/bridge/configure.go
+++ b/internal/bridge/configure.go
@@ -18,6 +18,7 @@
package bridge
import (
+ "context"
"strings"
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
@@ -31,7 +32,7 @@ import (
// ConfigureAppleMail configures apple mail for the given userID and address.
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
-func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
+func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
logrus.WithFields(logrus.Fields{
"userID": userID,
"address": logging.Sensitive(address),
@@ -56,7 +57,7 @@ func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
}
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
- if err := bridge.SetSMTPSSL(true); err != nil {
+ if err := bridge.SetSMTPSSL(ctx, true); err != nil {
return err
}
}
diff --git a/internal/bridge/files.go b/internal/bridge/files.go
index 475b1925..cb6a8cfc 100644
--- a/internal/bridge/files.go
+++ b/internal/bridge/files.go
@@ -58,11 +58,7 @@ func moveFile(from, to string) error {
return err
}
- if err := os.Rename(from, to); err != nil {
- return err
- }
-
- return nil
+ return os.Rename(from, to)
}
func copyDir(from, to string) error {
diff --git a/internal/bridge/imap.go b/internal/bridge/imap.go
index a5d94fe0..cf43c619 100644
--- a/internal/bridge/imap.go
+++ b/internal/bridge/imap.go
@@ -20,7 +20,6 @@ package bridge
import (
"context"
"crypto/tls"
- "fmt"
"io"
"os"
"path/filepath"
@@ -37,203 +36,21 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
- "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus"
)
-func (bridge *Bridge) serveIMAP() error {
- port, err := func() (int, error) {
- if bridge.imapServer == nil {
- return 0, fmt.Errorf("no IMAP server instance running")
- }
-
- logrus.WithFields(logrus.Fields{
- "port": bridge.vault.GetIMAPPort(),
- "ssl": bridge.vault.GetIMAPSSL(),
- }).Info("Starting IMAP server")
-
- imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
- if err != nil {
- return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
- }
-
- bridge.imapListener = imapListener
-
- if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
- return 0, fmt.Errorf("failed to serve IMAP: %w", err)
- }
-
- if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
- return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
- }
-
- return getPort(imapListener.Addr()), nil
- }()
-
- if err != nil {
- bridge.publish(events.IMAPServerError{
- Error: err,
- })
-
- return err
- }
-
- bridge.publish(events.IMAPServerReady{
- Port: port,
- })
-
- return nil
-}
-
-func (bridge *Bridge) restartIMAP() error {
- logrus.Info("Restarting IMAP server")
-
- if bridge.imapListener != nil {
- if err := bridge.imapListener.Close(); err != nil {
- return fmt.Errorf("failed to close IMAP listener: %w", err)
- }
-
- bridge.publish(events.IMAPServerStopped{})
- }
-
- return bridge.serveIMAP()
-}
-
-func (bridge *Bridge) closeIMAP(ctx context.Context) error {
- logrus.Info("Closing IMAP server")
-
- if bridge.imapServer != nil {
- if err := bridge.imapServer.Close(ctx); err != nil {
- return fmt.Errorf("failed to close IMAP server: %w", err)
- }
-
- bridge.imapServer = nil
- }
-
- if bridge.imapListener != nil {
- if err := bridge.imapListener.Close(); err != nil {
- return fmt.Errorf("failed to close IMAP listener: %w", err)
- }
- }
-
- bridge.publish(events.IMAPServerStopped{})
-
- return nil
+func (bridge *Bridge) restartIMAP(ctx context.Context) error {
+ return bridge.serverManager.RestartIMAP(ctx)
}
// addIMAPUser connects the given user to gluon.
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
- if bridge.imapServer == nil {
- return fmt.Errorf("no imap server instance running")
- }
-
- imapConn, err := user.NewIMAPConnectors()
- if err != nil {
- return fmt.Errorf("failed to create IMAP connectors: %w", err)
- }
-
- for addrID, imapConn := range imapConn {
- log := logrus.WithFields(logrus.Fields{
- "userID": user.ID(),
- "addrID": addrID,
- })
-
- if gluonID, ok := user.GetGluonID(addrID); ok {
- log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
-
- // Load the user, checking whether the DB was newly created.
- isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
- if err != nil {
- return fmt.Errorf("failed to load IMAP user: %w", err)
- }
-
- if isNew {
- // If the DB was newly created, clear the sync status; gluon's DB was not found.
- logrus.Warn("IMAP user DB was newly created, clearing sync status")
-
- // Remove the user from IMAP so we can clear the sync status.
- if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
- return fmt.Errorf("failed to remove IMAP user: %w", err)
- }
-
- // Clear the sync status -- we need to resync all messages.
- if err := user.ClearSyncStatus(); err != nil {
- return fmt.Errorf("failed to clear sync status: %w", err)
- }
-
- // Add the user back to the IMAP server.
- if isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
- return fmt.Errorf("failed to add IMAP user: %w", err)
- } else if isNew {
- panic("IMAP user should already have a database")
- }
- } else if status := user.GetSyncStatus(); !status.HasLabels {
- // Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
- if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
- return fmt.Errorf("failed to remove old IMAP user: %w", err)
- }
-
- if err := user.RemoveGluonID(addrID, gluonID); err != nil {
- return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
- }
-
- gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
- if err != nil {
- return fmt.Errorf("failed to add IMAP user: %w", err)
- }
-
- if err := user.SetGluonID(addrID, gluonID); err != nil {
- return fmt.Errorf("failed to set IMAP user ID: %w", err)
- }
-
- log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
- }
- } else {
- log.Info("Creating new IMAP user")
-
- gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
- if err != nil {
- return fmt.Errorf("failed to add IMAP user: %w", err)
- }
-
- if err := user.SetGluonID(addrID, gluonID); err != nil {
- return fmt.Errorf("failed to set IMAP user ID: %w", err)
- }
-
- log.WithField("gluonID", gluonID).Info("Created new IMAP user")
- }
- }
-
- // Trigger a sync for the user, if needed.
- user.TriggerSync()
-
- return nil
+ return bridge.serverManager.AddIMAPUser(ctx, user)
}
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files.
func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
- if bridge.imapServer == nil {
- return fmt.Errorf("no imap server instance running")
- }
-
- logrus.WithFields(logrus.Fields{
- "userID": user.ID(),
- "withData": withData,
- }).Debug("Removing IMAP user")
-
- for addrID, gluonID := range user.GetGluonIDs() {
- if err := bridge.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
- return fmt.Errorf("failed to remove IMAP user: %w", err)
- }
-
- if withData {
- if err := user.RemoveGluonID(addrID, gluonID); err != nil {
- return fmt.Errorf("failed to remove IMAP user ID: %w", err)
- }
- }
- }
-
- return nil
+ return bridge.serverManager.RemoveIMAPUser(ctx, user, withData)
}
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
@@ -262,19 +79,12 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
logrus.WithFields(logrus.Fields{
"sessionID": event.SessionID,
"username": event.Username,
- }).Info("Received IMAP login failure notification")
+ "pkg": "imap",
+ }).Error("Incorrect login credentials.")
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
}
}
-func getGluonDir(encVault *vault.Vault) (string, error) {
- if err := os.MkdirAll(encVault.GetGluonCacheDir(), 0o700); err != nil {
- return "", fmt.Errorf("failed to create gluon dir: %w", err)
- }
-
- return encVault.GetGluonCacheDir(), nil
-}
-
func ApplyGluonCachePathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "store")
}
diff --git a/internal/bridge/mocks.go b/internal/bridge/mocks.go
index 1c7bf934..63b16b8f 100644
--- a/internal/bridge/mocks.go
+++ b/internal/bridge/mocks.go
@@ -144,13 +144,13 @@ func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Versio
}
}
-func (testUpdater *TestUpdater) GetVersionInfo(ctx context.Context, downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) {
+func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfo, error) {
testUpdater.lock.RLock()
defer testUpdater.lock.RUnlock()
return testUpdater.latest, nil
}
-func (testUpdater *TestUpdater) InstallUpdate(ctx context.Context, downloader updater.Downloader, update updater.VersionInfo) error {
+func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
return nil
}
diff --git a/internal/bridge/refresh_test.go b/internal/bridge/refresh_test.go
index ad4192f6..e8edcd7d 100644
--- a/internal/bridge/refresh_test.go
+++ b/internal/bridge/refresh_test.go
@@ -28,7 +28,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/bradenaw/juniper/iterator"
- "github.com/emersion/go-imap/client"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
@@ -66,7 +65,7 @@ func TestBridge_Refresh(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@@ -99,7 +98,7 @@ func TestBridge_Refresh(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
diff --git a/internal/bridge/send_test.go b/internal/bridge/send_test.go
index c3b5e1ca..a334db15 100644
--- a/internal/bridge/send_test.go
+++ b/internal/bridge/send_test.go
@@ -34,7 +34,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/emersion/go-imap"
- "github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/stretchr/testify/require"
@@ -46,12 +45,17 @@ func TestBridge_Send(t *testing.T) {
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
+ smtpWaiter.Wait()
+
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
@@ -91,13 +95,13 @@ func TestBridge_Send(t *testing.T) {
}
// Connect the sender IMAP client.
- senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
+ senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client.
- recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
+ recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
@@ -135,13 +139,13 @@ func TestBridge_SendDraftFlags(t *testing.T) {
})
// Start the bridge.
- withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
+ withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
// Get the sender user info.
userInfo, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
// Connect the sender IMAP client.
- imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
+ imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck
@@ -245,13 +249,13 @@ func TestBridge_SendInvite(t *testing.T) {
})
// Start the bridge.
- withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
+ withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
// Get the sender user info.
userInfo, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
// Connect the sender IMAP client.
- imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
+ imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck
@@ -401,6 +405,9 @@ SGVsbG8gd29ybGQK
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
@@ -420,6 +427,8 @@ SGVsbG8gd29ybGQK
messageMultipartWithoutTextWithTextAttachment,
}
+ smtpWaiter.Wait()
+
for _, m := range messages {
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
@@ -444,13 +453,13 @@ SGVsbG8gd29ybGQK
}
// Connect the sender IMAP client.
- senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
+ senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client.
- recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
+ recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
diff --git a/internal/bridge/sentry_test.go b/internal/bridge/sentry_test.go
index afff01bc..291493f8 100644
--- a/internal/bridge/sentry_test.go
+++ b/internal/bridge/sentry_test.go
@@ -36,6 +36,9 @@ import (
func TestBridge_Report(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
+ imapWaiter := waitForIMAPServerReady(b)
+ defer imapWaiter.Done()
+
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
@@ -51,6 +54,8 @@ func TestBridge_Report(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
+ imapWaiter.Wait()
+
// Dial the IMAP port.
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
diff --git a/internal/bridge/server_manager.go b/internal/bridge/server_manager.go
new file mode 100644
index 00000000..9e06a430
--- /dev/null
+++ b/internal/bridge/server_manager.go
@@ -0,0 +1,696 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Proton Mail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Proton Mail Bridge. If not, see .
+
+package bridge
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "path/filepath"
+
+ "github.com/ProtonMail/gluon"
+ "github.com/ProtonMail/gluon/connector"
+ "github.com/ProtonMail/gluon/logging"
+ "github.com/ProtonMail/proton-bridge/v3/internal/events"
+ "github.com/ProtonMail/proton-bridge/v3/internal/safe"
+ "github.com/ProtonMail/proton-bridge/v3/internal/user"
+ "github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
+ "github.com/emersion/go-smtp"
+ "github.com/sirupsen/logrus"
+)
+
+// ServerManager manages the IMAP & SMTP servers and their listeners.
+type ServerManager struct {
+ requests *cpc.CPC
+
+ imapServer *gluon.Server
+ imapListener net.Listener
+
+ smtpServer *smtp.Server
+ smtpListener net.Listener
+
+ loadedUserCount int
+}
+
+func newServerManager() *ServerManager {
+ return &ServerManager{
+ requests: cpc.NewCPC(),
+ }
+}
+
+func (sm *ServerManager) Init(bridge *Bridge) error {
+ imapServer, err := createIMAPServer(bridge)
+ if err != nil {
+ return err
+ }
+
+ smtpServer := createSMTPServer(bridge)
+
+ sm.imapServer = imapServer
+ sm.smtpServer = smtpServer
+
+ bridge.tasks.Once(func(ctx context.Context) {
+ logging.DoAnnotated(ctx, func(ctx context.Context) {
+ sm.run(ctx, bridge)
+ }, logging.Labels{
+ "service": "server-manager",
+ })
+ })
+
+ return nil
+}
+
+func (sm *ServerManager) CloseServers(ctx context.Context) error {
+ defer sm.requests.Close()
+ _, err := sm.requests.Send(ctx, &smRequestClose{})
+
+ return err
+}
+
+func (sm *ServerManager) RestartIMAP(ctx context.Context) error {
+ _, err := sm.requests.Send(ctx, &smRequestRestartIMAP{})
+
+ return err
+}
+
+func (sm *ServerManager) RestartSMTP(ctx context.Context) error {
+ _, err := sm.requests.Send(ctx, &smRequestRestartSMTP{})
+
+ return err
+}
+
+func (sm *ServerManager) AddIMAPUser(ctx context.Context, user *user.User) error {
+ _, err := sm.requests.Send(ctx, &smRequestAddIMAPUser{user: user})
+
+ return err
+}
+
+func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
+ _, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
+ user: user,
+ withData: withData,
+ })
+
+ return err
+}
+
+func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error {
+ _, err := sm.requests.Send(ctx, &smRequestSetGluonDir{
+ dir: gluonDir,
+ })
+
+ return err
+}
+
+func (sm *ServerManager) AddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
+ reply, err := cpc.SendTyped[string](ctx, sm.requests, &smRequestAddGluonUser{
+ conn: conn,
+ passphrase: passphrase,
+ })
+
+ return reply, err
+}
+
+func (sm *ServerManager) RemoveGluonUser(ctx context.Context, gluonID string) error {
+ _, err := sm.requests.Send(ctx, &smRequestRemoveGluonUser{
+ userID: gluonID,
+ })
+
+ return err
+}
+
+func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
+ eventCh, cancel := bridge.GetEvents()
+ defer cancel()
+
+ for {
+ select {
+ case <-ctx.Done():
+ sm.handleClose(ctx, bridge)
+ return
+
+ case evt := <-eventCh:
+ switch evt.(type) {
+ case events.ConnStatusDown:
+ logrus.Info("Server Manager, network down stopping listeners")
+ if err := sm.closeSMTPServer(bridge); err != nil {
+ logrus.WithError(err).Error("Failed to close SMTP server")
+ }
+
+ if err := sm.stopIMAPListener(bridge); err != nil {
+ logrus.WithError(err)
+ }
+ case events.ConnStatusUp:
+ logrus.Info("Server Manager, network up starting listeners")
+ sm.handleLoadedUserCountChange(ctx, bridge)
+ }
+
+ case request, ok := <-sm.requests.ReceiveCh():
+ if !ok {
+ return
+ }
+
+ switch r := request.Value().(type) {
+ case *smRequestClose:
+ sm.handleClose(ctx, bridge)
+ request.Reply(ctx, nil, nil)
+ return
+
+ case *smRequestRestartSMTP:
+ err := sm.restartSMTP(bridge)
+ request.Reply(ctx, nil, err)
+
+ case *smRequestRestartIMAP:
+ err := sm.restartIMAP(ctx, bridge)
+ request.Reply(ctx, nil, err)
+
+ case *smRequestAddIMAPUser:
+ err := sm.handleAddIMAPUser(ctx, r.user)
+ request.Reply(ctx, nil, err)
+ if err == nil {
+ sm.loadedUserCount++
+ sm.handleLoadedUserCountChange(ctx, bridge)
+ }
+
+ case *smRequestRemoveIMAPUser:
+ err := sm.handleRemoveIMAPUser(ctx, r.user, r.withData)
+ request.Reply(ctx, nil, err)
+ if err == nil {
+ sm.loadedUserCount--
+ sm.handleLoadedUserCountChange(ctx, bridge)
+ }
+
+ case *smRequestSetGluonDir:
+ err := sm.handleSetGluonDir(ctx, bridge, r.dir)
+ request.Reply(ctx, nil, err)
+
+ case *smRequestAddGluonUser:
+ id, err := sm.handleAddGluonUser(ctx, r.conn, r.passphrase)
+ request.Reply(ctx, id, err)
+
+ case *smRequestRemoveGluonUser:
+ err := sm.handleRemoveGluonUser(ctx, r.userID)
+ request.Reply(ctx, nil, err)
+ }
+ }
+ }
+}
+
+func (sm *ServerManager) handleLoadedUserCountChange(ctx context.Context, bridge *Bridge) {
+ logrus.Infof("Validating Listener State %v", sm.loadedUserCount)
+ if sm.shouldStartServers() {
+ if sm.imapListener == nil {
+ if err := sm.serveIMAP(ctx, bridge); err != nil {
+ logrus.WithError(err).Error("Failed to start IMAP server")
+ }
+ }
+
+ if sm.smtpListener == nil {
+ if err := sm.restartSMTP(bridge); err != nil {
+ logrus.WithError(err).Error("Failed to start SMTP server")
+ }
+ }
+ } else {
+ if sm.imapListener != nil {
+ if err := sm.stopIMAPListener(bridge); err != nil {
+ logrus.WithError(err).Error("Failed to stop IMAP server")
+ }
+ }
+
+ if sm.smtpListener != nil {
+ if err := sm.closeSMTPServer(bridge); err != nil {
+ logrus.WithError(err).Error("Failed to stop SMTP server")
+ }
+ }
+ }
+}
+
+func (sm *ServerManager) handleClose(ctx context.Context, bridge *Bridge) {
+ // Close the IMAP server.
+ if err := sm.closeIMAPServer(ctx, bridge); err != nil {
+ logrus.WithError(err).Error("Failed to close IMAP server")
+ }
+
+ // Close the SMTP server.
+ if err := sm.closeSMTPServer(bridge); err != nil {
+ logrus.WithError(err).Error("Failed to close SMTP server")
+ }
+}
+
+func (sm *ServerManager) handleAddIMAPUser(ctx context.Context, user *user.User) error {
+ if sm.imapServer == nil {
+ return fmt.Errorf("no imap server instance running")
+ }
+
+ imapConn, err := user.NewIMAPConnectors()
+ if err != nil {
+ return fmt.Errorf("failed to create IMAP connectors: %w", err)
+ }
+
+ for addrID, imapConn := range imapConn {
+ log := logrus.WithFields(logrus.Fields{
+ "userID": user.ID(),
+ "addrID": addrID,
+ })
+
+ if gluonID, ok := user.GetGluonID(addrID); ok {
+ log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
+
+ // Load the user, checking whether the DB was newly created.
+ isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
+ if err != nil {
+ return fmt.Errorf("failed to load IMAP user: %w", err)
+ }
+
+ if isNew {
+ // If the DB was newly created, clear the sync status; gluon's DB was not found.
+ logrus.Warn("IMAP user DB was newly created, clearing sync status")
+
+ // Remove the user from IMAP so we can clear the sync status.
+ if err := sm.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
+ return fmt.Errorf("failed to remove IMAP user: %w", err)
+ }
+
+ // Clear the sync status -- we need to resync all messages.
+ if err := user.ClearSyncStatus(); err != nil {
+ return fmt.Errorf("failed to clear sync status: %w", err)
+ }
+
+ // Add the user back to the IMAP server.
+ if isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
+ return fmt.Errorf("failed to add IMAP user: %w", err)
+ } else if isNew {
+ panic("IMAP user should already have a database")
+ }
+ } else if status := user.GetSyncStatus(); !status.HasLabels {
+ // Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
+ if err := sm.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
+ return fmt.Errorf("failed to remove old IMAP user: %w", err)
+ }
+
+ if err := user.RemoveGluonID(addrID, gluonID); err != nil {
+ return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
+ }
+
+ gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
+ if err != nil {
+ return fmt.Errorf("failed to add IMAP user: %w", err)
+ }
+
+ if err := user.SetGluonID(addrID, gluonID); err != nil {
+ return fmt.Errorf("failed to set IMAP user ID: %w", err)
+ }
+
+ log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
+ }
+ } else {
+ log.Info("Creating new IMAP user")
+
+ gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
+ if err != nil {
+ return fmt.Errorf("failed to add IMAP user: %w", err)
+ }
+
+ if err := user.SetGluonID(addrID, gluonID); err != nil {
+ return fmt.Errorf("failed to set IMAP user ID: %w", err)
+ }
+
+ log.WithField("gluonID", gluonID).Info("Created new IMAP user")
+ }
+ }
+
+ // Trigger a sync for the user, if needed.
+ user.TriggerSync()
+
+ return nil
+}
+
+func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
+ if sm.imapServer == nil {
+ return fmt.Errorf("no imap server instance running")
+ }
+
+ logrus.WithFields(logrus.Fields{
+ "userID": user.ID(),
+ "withData": withData,
+ }).Debug("Removing IMAP user")
+
+ for addrID, gluonID := range user.GetGluonIDs() {
+ if err := sm.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
+ return fmt.Errorf("failed to remove IMAP user: %w", err)
+ }
+
+ if withData {
+ if err := user.RemoveGluonID(addrID, gluonID); err != nil {
+ return fmt.Errorf("failed to remove IMAP user ID: %w", err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func createIMAPServer(bridge *Bridge) (*gluon.Server, error) {
+ gluonDataDir, err := bridge.GetGluonDataDir()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
+ }
+
+ return newIMAPServer(
+ bridge.vault.GetGluonCacheDir(),
+ gluonDataDir,
+ bridge.curVersion,
+ bridge.tlsConfig,
+ bridge.reporter,
+ bridge.logIMAPClient,
+ bridge.logIMAPServer,
+ bridge.imapEventCh,
+ bridge.tasks,
+ bridge.uidValidityGenerator,
+ bridge.panicHandler,
+ )
+}
+
+func createSMTPServer(bridge *Bridge) *smtp.Server {
+ return newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
+}
+
+func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error {
+ // We close the listener ourselves even though it's also closed by smtpServer.Close().
+ // This is because smtpServer.Serve() is called in a separate goroutine and might be executed
+ // after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
+ // even after the server has been closed. So we close the listener ourselves to unblock it.
+
+ if sm.smtpListener != nil {
+ logrus.Info("Closing SMTP Listener")
+ if err := sm.smtpListener.Close(); err != nil {
+ return fmt.Errorf("failed to close SMTP listener: %w", err)
+ }
+
+ sm.smtpListener = nil
+ }
+
+ if sm.smtpServer != nil {
+ logrus.Info("Closing SMTP server")
+ if err := sm.smtpServer.Close(); err != nil {
+ logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
+ }
+
+ sm.smtpServer = nil
+
+ bridge.publish(events.SMTPServerStopped{})
+ }
+
+ return nil
+}
+
+func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error {
+ if sm.imapListener != nil {
+ logrus.Info("Closing IMAP Listener")
+
+ if err := sm.imapListener.Close(); err != nil {
+ return fmt.Errorf("failed to close IMAP listener: %w", err)
+ }
+
+ sm.imapListener = nil
+
+ bridge.publish(events.IMAPServerStopped{})
+ }
+
+ if sm.imapServer != nil {
+ logrus.Info("Closing IMAP server")
+ if err := sm.imapServer.Close(ctx); err != nil {
+ return fmt.Errorf("failed to close IMAP server: %w", err)
+ }
+
+ sm.imapServer = nil
+ }
+
+ return nil
+}
+
+func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error {
+ logrus.Info("Restarting IMAP server")
+
+ if sm.imapListener != nil {
+ if err := sm.imapListener.Close(); err != nil {
+ return fmt.Errorf("failed to close IMAP listener: %w", err)
+ }
+
+ sm.imapListener = nil
+
+ bridge.publish(events.IMAPServerStopped{})
+ }
+
+ if sm.shouldStartServers() {
+ return sm.serveIMAP(ctx, bridge)
+ }
+
+ return nil
+}
+
+func (sm *ServerManager) restartSMTP(bridge *Bridge) error {
+ logrus.Info("Restarting SMTP server")
+
+ if err := sm.closeSMTPServer(bridge); err != nil {
+ return fmt.Errorf("failed to close SMTP: %w", err)
+ }
+
+ bridge.publish(events.SMTPServerStopped{})
+
+ sm.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
+
+ if sm.shouldStartServers() {
+ return sm.serveSMTP(bridge)
+ }
+
+ return nil
+}
+
+func (sm *ServerManager) serveSMTP(bridge *Bridge) error {
+ port, err := func() (int, error) {
+ logrus.WithFields(logrus.Fields{
+ "port": bridge.vault.GetSMTPPort(),
+ "ssl": bridge.vault.GetSMTPSSL(),
+ }).Info("Starting SMTP server")
+
+ smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
+ if err != nil {
+ return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
+ }
+
+ sm.smtpListener = smtpListener
+
+ bridge.tasks.Once(func(context.Context) {
+ if err := sm.smtpServer.Serve(smtpListener); err != nil {
+ logrus.WithError(err).Info("SMTP server stopped")
+ }
+ })
+
+ if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
+ return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
+ }
+
+ return getPort(smtpListener.Addr()), nil
+ }()
+
+ if err != nil {
+ bridge.publish(events.SMTPServerError{
+ Error: err,
+ })
+
+ return err
+ }
+
+ bridge.publish(events.SMTPServerReady{
+ Port: port,
+ })
+
+ return nil
+}
+
+func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error {
+ port, err := func() (int, error) {
+ if sm.imapServer == nil {
+ return 0, fmt.Errorf("no IMAP server instance running")
+ }
+
+ logrus.WithFields(logrus.Fields{
+ "port": bridge.vault.GetIMAPPort(),
+ "ssl": bridge.vault.GetIMAPSSL(),
+ }).Info("Starting IMAP server")
+
+ imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
+ if err != nil {
+ return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
+ }
+
+ sm.imapListener = imapListener
+
+ if err := sm.imapServer.Serve(ctx, sm.imapListener); err != nil {
+ return 0, fmt.Errorf("failed to serve IMAP: %w", err)
+ }
+
+ if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
+ return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
+ }
+
+ return getPort(imapListener.Addr()), nil
+ }()
+
+ if err != nil {
+ bridge.publish(events.IMAPServerError{
+ Error: err,
+ })
+
+ return err
+ }
+
+ bridge.publish(events.IMAPServerReady{
+ Port: port,
+ })
+
+ return nil
+}
+
+func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
+ logrus.Info("Stopping IMAP listener")
+ if sm.imapListener != nil {
+ if err := sm.imapListener.Close(); err != nil {
+ return err
+ }
+
+ sm.imapListener = nil
+
+ bridge.publish(events.IMAPServerStopped{})
+ }
+
+ return nil
+}
+
+func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error {
+ return safe.RLockRet(func() error {
+ currentGluonDir := bridge.GetGluonCacheDir()
+ newGluonDir = filepath.Join(newGluonDir, "gluon")
+ if newGluonDir == currentGluonDir {
+ return fmt.Errorf("new gluon dir is the same as the old one")
+ }
+
+ if err := sm.closeIMAPServer(context.Background(), bridge); err != nil {
+ return fmt.Errorf("failed to close IMAP: %w", err)
+ }
+
+ sm.loadedUserCount = 0
+
+ if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
+ logrus.WithError(err).Error("failed to move GluonCacheDir")
+
+ if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
+ return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
+ }
+ }
+
+ bridge.heartbeat.SetCacheLocation(newGluonDir)
+
+ gluonDataDir, err := bridge.GetGluonDataDir()
+ if err != nil {
+ return fmt.Errorf("failed to get Gluon Database directory: %w", err)
+ }
+
+ imapServer, err := newIMAPServer(
+ bridge.vault.GetGluonCacheDir(),
+ gluonDataDir,
+ bridge.curVersion,
+ bridge.tlsConfig,
+ bridge.reporter,
+ bridge.logIMAPClient,
+ bridge.logIMAPServer,
+ bridge.imapEventCh,
+ bridge.tasks,
+ bridge.uidValidityGenerator,
+ bridge.panicHandler,
+ )
+ if err != nil {
+ return fmt.Errorf("failed to create new IMAP server: %w", err)
+ }
+
+ sm.imapServer = imapServer
+ for _, bridgeUser := range bridge.users {
+ if err := sm.handleAddIMAPUser(ctx, bridgeUser); err != nil {
+ return fmt.Errorf("failed to add users to new IMAP server: %w", err)
+ }
+ sm.loadedUserCount++
+ }
+
+ if sm.shouldStartServers() {
+ if err := sm.serveIMAP(ctx, bridge); err != nil {
+ return fmt.Errorf("failed to serve IMAP: %w", err)
+ }
+ }
+
+ return nil
+ }, bridge.usersLock)
+}
+
+func (sm *ServerManager) handleAddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
+ if sm.imapServer == nil {
+ return "", fmt.Errorf("no imap server instance running")
+ }
+
+ return sm.imapServer.AddUser(ctx, conn, passphrase)
+}
+
+func (sm *ServerManager) handleRemoveGluonUser(ctx context.Context, userID string) error {
+ if sm.imapServer == nil {
+ return fmt.Errorf("no imap server instance running")
+ }
+
+ return sm.imapServer.RemoveUser(ctx, userID, true)
+}
+
+func (sm *ServerManager) shouldStartServers() bool {
+ return sm.loadedUserCount >= 1
+}
+
+type smRequestClose struct{}
+
+type smRequestRestartIMAP struct{}
+
+type smRequestRestartSMTP struct{}
+
+type smRequestAddIMAPUser struct {
+ user *user.User
+}
+
+type smRequestRemoveIMAPUser struct {
+ user *user.User
+ withData bool
+}
+
+type smRequestSetGluonDir struct {
+ dir string
+}
+
+type smRequestAddGluonUser struct {
+ conn connector.Connector
+ passphrase []byte
+}
+
+type smRequestRemoveGluonUser struct {
+ userID string
+}
diff --git a/internal/bridge/server_manager_test.go b/internal/bridge/server_manager_test.go
new file mode 100644
index 00000000..47f799e9
--- /dev/null
+++ b/internal/bridge/server_manager_test.go
@@ -0,0 +1,179 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Proton Mail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Proton Mail Bridge. If not, see .
+
+package bridge_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/ProtonMail/go-proton-api"
+ "github.com/ProtonMail/go-proton-api/server"
+ "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/v3/internal/constants"
+ "github.com/ProtonMail/proton-bridge/v3/internal/events"
+ "github.com/stretchr/testify/require"
+)
+
+func TestServerManager_NoLoadedUsersNoServers(t *testing.T) {
+ withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
+ withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ _, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
+ require.Error(t, err)
+ })
+ })
+}
+
+func TestServerManager_ServersStartAfterFirstConnectedUser(t *testing.T) {
+ withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
+ withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ imapWaiter := waitForIMAPServerReady(bridge)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
+ _, err := bridge.LoginFull(ctx, username, password, nil, nil)
+ require.NoError(t, err)
+
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+ })
+ })
+}
+
+func TestServerManager_ServersStopsAfterUserLogsOut(t *testing.T) {
+ withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
+ withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ imapWaiter := waitForIMAPServerReady(bridge)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
+ userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
+ require.NoError(t, err)
+
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+
+ imapWaiterStopped := waitForIMAPServerStopped(bridge)
+ defer imapWaiterStopped.Done()
+
+ require.NoError(t, bridge.LogoutUser(ctx, userID))
+
+ imapWaiterStopped.Wait()
+ })
+ })
+}
+
+func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.T) {
+ otherPassword := []byte("bar")
+ otherUser := "foo"
+ withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
+ _, _, err := s.CreateUser(otherUser, otherPassword)
+ require.NoError(t, err)
+
+ withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ imapWaiter := waitForIMAPServerReady(bridge)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
+ _, err := bridge.LoginFull(ctx, username, password, nil, nil)
+ require.NoError(t, err)
+
+ userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
+ require.NoError(t, err)
+
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+
+ evtCh, cancel := bridge.GetEvents(events.UserDeauth{})
+ defer cancel()
+
+ require.NoError(t, s.RevokeUser(userIDOther))
+
+ waitForEvent(t, evtCh, events.UserDeauth{})
+
+ imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
+ require.NoError(t, err)
+ require.NoError(t, imapClient.Logout())
+ })
+ })
+}
+
+func TestServerManager_ServersStartIfAtLeastOneUserIsLoggedIn(t *testing.T) {
+ otherPassword := []byte("bar")
+ otherUser := "foo"
+ withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
+ userIDOther, _, err := s.CreateUser(otherUser, otherPassword)
+ require.NoError(t, err)
+
+ withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ _, err := bridge.LoginFull(ctx, username, password, nil, nil)
+ require.NoError(t, err)
+
+ _, err = bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
+ require.NoError(t, err)
+ })
+
+ require.NoError(t, s.RevokeUser(userIDOther))
+
+ withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
+ require.NoError(t, err)
+ require.NoError(t, imapClient.Logout())
+ })
+ })
+}
+
+func TestServerManager_NetworkLossStopsServers(t *testing.T) {
+ withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
+ withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ imapWaiter := waitForIMAPServerReady(bridge)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
+ imapWaiterStop := waitForIMAPServerStopped(bridge)
+ defer imapWaiterStop.Done()
+
+ smtpWaiterStop := waitForSMTPServerStopped(bridge)
+ defer smtpWaiterStop.Done()
+
+ _, err := bridge.LoginFull(ctx, username, password, nil, nil)
+ require.NoError(t, err)
+
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+
+ netCtl.Disable()
+
+ imapWaiterStop.Wait()
+ smtpWaiterStop.Wait()
+
+ netCtl.Enable()
+
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+ })
+ })
+}
diff --git a/internal/bridge/settings.go b/internal/bridge/settings.go
index dd727316..dbbadecb 100644
--- a/internal/bridge/settings.go
+++ b/internal/bridge/settings.go
@@ -22,7 +22,6 @@ import (
"fmt"
"net"
"os"
- "path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
@@ -55,7 +54,7 @@ func (bridge *Bridge) GetIMAPPort() int {
return bridge.vault.GetIMAPPort()
}
-func (bridge *Bridge) SetIMAPPort(newPort int) error {
+func (bridge *Bridge) SetIMAPPort(ctx context.Context, newPort int) error {
if newPort == bridge.vault.GetIMAPPort() {
return nil
}
@@ -66,14 +65,14 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error {
bridge.heartbeat.SetIMAPPort(newPort)
- return bridge.restartIMAP()
+ return bridge.restartIMAP(ctx)
}
func (bridge *Bridge) GetIMAPSSL() bool {
return bridge.vault.GetIMAPSSL()
}
-func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
+func (bridge *Bridge) SetIMAPSSL(ctx context.Context, newSSL bool) error {
if newSSL == bridge.vault.GetIMAPSSL() {
return nil
}
@@ -84,14 +83,14 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
bridge.heartbeat.SetIMAPConnectionMode(newSSL)
- return bridge.restartIMAP()
+ return bridge.restartIMAP(ctx)
}
func (bridge *Bridge) GetSMTPPort() int {
return bridge.vault.GetSMTPPort()
}
-func (bridge *Bridge) SetSMTPPort(newPort int) error {
+func (bridge *Bridge) SetSMTPPort(ctx context.Context, newPort int) error {
if newPort == bridge.vault.GetSMTPPort() {
return nil
}
@@ -102,14 +101,14 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error {
bridge.heartbeat.SetSMTPPort(newPort)
- return bridge.restartSMTP()
+ return bridge.restartSMTP(ctx)
}
func (bridge *Bridge) GetSMTPSSL() bool {
return bridge.vault.GetSMTPSSL()
}
-func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
+func (bridge *Bridge) SetSMTPSSL(ctx context.Context, newSSL bool) error {
if newSSL == bridge.vault.GetSMTPSSL() {
return nil
}
@@ -120,7 +119,7 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
bridge.heartbeat.SetSMTPConnectionMode(newSSL)
- return bridge.restartSMTP()
+ return bridge.restartSMTP(ctx)
}
func (bridge *Bridge) GetGluonCacheDir() string {
@@ -132,63 +131,7 @@ func (bridge *Bridge) GetGluonDataDir() (string, error) {
}
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
- return safe.RLockRet(func() error {
- currentGluonDir := bridge.GetGluonCacheDir()
- newGluonDir = filepath.Join(newGluonDir, "gluon")
- if newGluonDir == currentGluonDir {
- return fmt.Errorf("new gluon dir is the same as the old one")
- }
-
- if err := bridge.closeIMAP(context.Background()); err != nil {
- return fmt.Errorf("failed to close IMAP: %w", err)
- }
-
- if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
- logrus.WithError(err).Error("failed to move GluonCacheDir")
-
- if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
- return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
- }
- }
-
- bridge.heartbeat.SetCacheLocation(newGluonDir)
-
- gluonDataDir, err := bridge.GetGluonDataDir()
- if err != nil {
- return fmt.Errorf("failed to get Gluon Database directory: %w", err)
- }
-
- imapServer, err := newIMAPServer(
- bridge.vault.GetGluonCacheDir(),
- gluonDataDir,
- bridge.curVersion,
- bridge.tlsConfig,
- bridge.reporter,
- bridge.logIMAPClient,
- bridge.logIMAPServer,
- bridge.imapEventCh,
- bridge.tasks,
- bridge.uidValidityGenerator,
- bridge.panicHandler,
- )
- if err != nil {
- return fmt.Errorf("failed to create new IMAP server: %w", err)
- }
-
- bridge.imapServer = imapServer
-
- for _, user := range bridge.users {
- if err := bridge.addIMAPUser(ctx, user); err != nil {
- return fmt.Errorf("failed to add users to new IMAP server: %w", err)
- }
- }
-
- if err := bridge.serveIMAP(); err != nil {
- return fmt.Errorf("failed to serve IMAP: %w", err)
- }
-
- return nil
- }, bridge.usersLock)
+ return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
}
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
diff --git a/internal/bridge/settings_test.go b/internal/bridge/settings_test.go
index 6a48c827..f10d1932 100644
--- a/internal/bridge/settings_test.go
+++ b/internal/bridge/settings_test.go
@@ -57,7 +57,7 @@ func TestBridge_Settings_IMAPPort(t *testing.T) {
curPort := bridge.GetIMAPPort()
// Set the port to 1144.
- require.NoError(t, bridge.SetIMAPPort(1144))
+ require.NoError(t, bridge.SetIMAPPort(ctx, 1144))
// Get the new setting.
require.Equal(t, 1144, bridge.GetIMAPPort())
@@ -75,7 +75,7 @@ func TestBridge_Settings_IMAPSSL(t *testing.T) {
require.False(t, bridge.GetIMAPSSL())
// Enable IMAP SSL.
- require.NoError(t, bridge.SetIMAPSSL(true))
+ require.NoError(t, bridge.SetIMAPSSL(ctx, true))
// Get the new setting.
require.True(t, bridge.GetIMAPSSL())
@@ -89,7 +89,7 @@ func TestBridge_Settings_SMTPPort(t *testing.T) {
curPort := bridge.GetSMTPPort()
// Set the port to 1024.
- require.NoError(t, bridge.SetSMTPPort(1024))
+ require.NoError(t, bridge.SetSMTPPort(ctx, 1024))
// Get the new setting.
require.Equal(t, 1024, bridge.GetSMTPPort())
@@ -107,7 +107,7 @@ func TestBridge_Settings_SMTPSSL(t *testing.T) {
require.False(t, bridge.GetSMTPSSL())
// Enable SMTP SSL.
- require.NoError(t, bridge.SetSMTPSSL(true))
+ require.NoError(t, bridge.SetSMTPSSL(ctx, true))
// Get the new setting.
require.True(t, bridge.GetSMTPSSL())
diff --git a/internal/bridge/smtp.go b/internal/bridge/smtp.go
index 9b88b811..3aeb4f8d 100644
--- a/internal/bridge/smtp.go
+++ b/internal/bridge/smtp.go
@@ -20,93 +20,16 @@ package bridge
import (
"context"
"crypto/tls"
- "fmt"
-
- "github.com/ProtonMail/proton-bridge/v3/internal/events"
- "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
+ "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
-func (bridge *Bridge) serveSMTP() error {
- port, err := func() (int, error) {
- logrus.WithFields(logrus.Fields{
- "port": bridge.vault.GetSMTPPort(),
- "ssl": bridge.vault.GetSMTPSSL(),
- }).Info("Starting SMTP server")
-
- smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
- if err != nil {
- return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
- }
-
- bridge.smtpListener = smtpListener
-
- bridge.tasks.Once(func(context.Context) {
- if err := bridge.smtpServer.Serve(smtpListener); err != nil {
- logrus.WithError(err).Info("SMTP server stopped")
- }
- })
-
- if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
- return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
- }
-
- return getPort(smtpListener.Addr()), nil
- }()
-
- if err != nil {
- bridge.publish(events.SMTPServerError{
- Error: err,
- })
-
- return err
- }
-
- bridge.publish(events.SMTPServerReady{
- Port: port,
- })
-
- return nil
-}
-
-func (bridge *Bridge) restartSMTP() error {
- logrus.Info("Restarting SMTP server")
-
- if err := bridge.closeSMTP(); err != nil {
- return fmt.Errorf("failed to close SMTP: %w", err)
- }
-
- bridge.publish(events.SMTPServerStopped{})
-
- bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
-
- return bridge.serveSMTP()
-}
-
-// We close the listener ourselves even though it's also closed by smtpServer.Close().
-// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
-// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
-// even after the server has been closed. So we close the listener ourselves to unblock it.
-func (bridge *Bridge) closeSMTP() error {
- logrus.Info("Closing SMTP server")
-
- if bridge.smtpListener != nil {
- if err := bridge.smtpListener.Close(); err != nil {
- return fmt.Errorf("failed to close SMTP listener: %w", err)
- }
- }
-
- if err := bridge.smtpServer.Close(); err != nil {
- logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
- }
-
- bridge.publish(events.SMTPServerStopped{})
-
- return nil
+func (bridge *Bridge) restartSMTP(ctx context.Context) error {
+ return bridge.serverManager.RestartSMTP(ctx)
}
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
diff --git a/internal/bridge/smtp_backend.go b/internal/bridge/smtp_backend.go
index d48f3923..996bf5be 100644
--- a/internal/bridge/smtp_backend.go
+++ b/internal/bridge/smtp_backend.go
@@ -58,6 +58,11 @@ func (s *smtpSession) AuthPlain(username, password string) error {
return nil
}
+ logrus.WithFields(logrus.Fields{
+ "username": username,
+ "pkg": "smtp",
+ }).Error("Incorrect login credentials.")
+
return fmt.Errorf("invalid username or password")
}, s.usersLock)
}
@@ -72,7 +77,7 @@ func (s *smtpSession) Logout() error {
return nil
}
-func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
+func (s *smtpSession) Mail(from string, _ *smtp.MailOptions) error {
s.from = from
return nil
}
diff --git a/internal/bridge/sync_test.go b/internal/bridge/sync_test.go
index a83dac42..00a0105e 100644
--- a/internal/bridge/sync_test.go
+++ b/internal/bridge/sync_test.go
@@ -80,7 +80,7 @@ func TestBridge_Sync(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@@ -112,15 +112,6 @@ func TestBridge_Sync(t *testing.T) {
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
-
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
- require.NoError(t, err)
- require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
- defer func() { _ = client.Logout() }()
-
- status, err := client.Select(`Folders/folder`, false)
- require.NoError(t, err)
- require.Less(t, status.Messages, uint32(numMsg))
}
// Remove the network limit, allowing the sync to finish.
@@ -136,7 +127,7 @@ func TestBridge_Sync(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@@ -187,7 +178,7 @@ func _TestBridge_Sync_BadMessage(t *testing.T) { //nolint:unused,deadcode
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@@ -273,15 +264,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
-
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
- require.NoError(t, err)
- require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
- defer func() { _ = client.Logout() }()
-
- status, err := client.Select(`Folders/folder`, false)
- require.NoError(t, err)
- require.Less(t, status.Messages, uint32(numMsg))
}
// Create a new mailbox and move that last 1/3 of the messages into it to simulate user
@@ -311,7 +293,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
+ client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
diff --git a/internal/bridge/sync_unix_test.go b/internal/bridge/sync_unix_test.go
new file mode 100644
index 00000000..5d046f97
--- /dev/null
+++ b/internal/bridge/sync_unix_test.go
@@ -0,0 +1,82 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Proton Mail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Proton Mail Bridge. If not, see .
+
+//go:build !windows
+
+package bridge_test
+
+import (
+ "context"
+ "syscall"
+ "testing"
+
+ "github.com/ProtonMail/go-proton-api"
+ "github.com/ProtonMail/go-proton-api/server"
+ "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/v3/internal/events"
+ "github.com/stretchr/testify/require"
+)
+
+// Disabled due to flakyness.
+func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
+ var rlimitCurrent syscall.Rlimit
+
+ require.NoError(t, syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent))
+
+ // Restore RLimit for Process at the end of this test
+ defer func() {
+ require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent))
+ }()
+
+ rlimit := syscall.Rlimit{
+ Max: 100,
+ Cur: 100,
+ }
+
+ require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimit))
+
+ numMsg := 1 << 8
+
+ withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
+ userID, addrID, err := s.CreateUser("imap", password)
+ require.NoError(t, err)
+
+ labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
+ require.NoError(t, err)
+
+ withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
+ createNumMessages(ctx, t, c, addrID, labelID, numMsg)
+ })
+
+ // The initial user should be fully synced.
+ withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
+ syncCh, done := bridge.GetEvents(events.SyncFailed{})
+ defer done()
+
+ userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
+ require.NoError(t, err)
+
+ evt := <-syncCh
+ switch e := evt.(type) {
+ case events.SyncFailed:
+ require.Equal(t, userID, e.UserID)
+ default:
+ require.Fail(t, "Expected events.SyncFailed{}")
+ }
+ })
+ }, server.WithTLS(false))
+}
diff --git a/internal/bridge/user.go b/internal/bridge/user.go
index 526850b1..3c8e15e4 100644
--- a/internal/bridge/user.go
+++ b/internal/bridge/user.go
@@ -584,29 +584,7 @@ func (bridge *Bridge) newVaultUser(
authUID, authRef string,
saltedKeyPass []byte,
) (*vault.User, bool, error) {
- if !bridge.vault.HasUser(apiUser.ID) {
- user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
- if err != nil {
- return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
- }
-
- return user, true, nil
- }
-
- user, err := bridge.vault.NewUser(apiUser.ID)
- if err != nil {
- return nil, false, err
- }
-
- if err := user.SetAuth(authUID, authRef); err != nil {
- return nil, false, err
- }
-
- if err := user.SetKeyPass(saltedKeyPass); err != nil {
- return nil, false, err
- }
-
- return user, false, nil
+ return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
}
// logout logs out the given user, optionally logging them out from the API too.
diff --git a/internal/bridge/user_event_test.go b/internal/bridge/user_event_test.go
index fa69ea1f..e31ae675 100644
--- a/internal/bridge/user_event_test.go
+++ b/internal/bridge/user_event_test.go
@@ -141,6 +141,9 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string
@@ -176,6 +179,8 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
userFeedback(t, ctx, bridge, badUserID)
+ smtpWaiter.Wait()
+
userContinueEventProcess(ctx, t, s, bridge)
})
})
@@ -194,6 +199,9 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string
@@ -217,6 +225,7 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
})
+ smtpWaiter.Wait()
userContinueEventProcess(ctx, t, s, bridge)
})
})
@@ -412,6 +421,17 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ var count int32
+ // The first 10 times bridge attempts to sync any of the messages, drop the connection.
+ s.AddStatusHook(func(req *http.Request) (int, bool) {
+ if strings.Contains(req.URL.Path, "/mail/v4/messages") {
+ if atomic.AddInt32(&count, 1) < 10 {
+ dropListener.DropAll()
+ }
+ }
+
+ return 0, false
+ })
userLoginAndSync(ctx, t, bridge, "user", password)
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
@@ -421,30 +441,17 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
})
- var count int
-
- // The first 10 times bridge attempts to sync any of the messages, drop the connection.
- s.AddStatusHook(func(req *http.Request) (int, bool) {
- if strings.Contains(req.URL.Path, "/mail/v4/messages") {
- if count++; count < 10 {
- dropListener.DropAll()
- }
- }
-
- return 0, false
- })
-
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
+ cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
- require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
- defer func() { _ = client.Logout() }()
+ require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
+ defer func() { _ = cli.Logout() }()
// The IMAP client will eventually see 20 messages.
require.Eventually(t, func() bool {
- status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
+ status, err := cli.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
return err == nil && status.Messages == 20
}, 10*time.Second, 100*time.Millisecond)
})
@@ -638,12 +645,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
+ cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
- require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
- defer func() { _ = client.Logout() }()
+ require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
+ defer func() { _ = cli.Logout() }()
- messages, err := clientFetch(client, "Drafts")
+ messages, err := clientFetch(cli, "Drafts")
require.NoError(t, err)
require.Len(t, messages, 1)
require.Contains(t, messages[0].Flags, imap.DraftFlag)
@@ -677,12 +684,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
+ cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
- require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
- defer func() { _ = client.Logout() }()
+ require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
+ defer func() { _ = cli.Logout() }()
- messages, err := clientFetch(client, "Sent")
+ messages, err := clientFetch(cli, "Sent")
require.NoError(t, err)
require.Len(t, messages, 1)
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
@@ -771,15 +778,24 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
+ imapWaiter := waitForIMAPServerReady(bridge)
+ defer imapWaiter.Done()
+
+ smtpWaiter := waitForSMTPServerReady(bridge)
+ defer smtpWaiter.Done()
+
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
info, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
+ imapWaiter.Wait()
+ smtpWaiter.Wait()
+
+ cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
- require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
- defer func() { _ = client.Logout() }()
+ require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
+ defer func() { _ = cli.Logout() }()
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
parentName := uuid.NewString()
@@ -795,7 +811,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be created.
require.Eventually(t, func() bool {
- return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
+ return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
@@ -812,7 +828,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be created.
require.Eventually(t, func() bool {
- return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
+ return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
@@ -827,14 +843,14 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be renamed.
require.Eventually(t, func() bool {
- return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
+ return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
// Wait for the child folder to be renamed.
require.Eventually(t, func() bool {
- return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
+ return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
@@ -843,48 +859,6 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
})
}
-// TBD: GODT-2527.
-func _TestBridge503DuringEventDoesNotCauseBadEvent(t *testing.T) { //nolint:unused,deadcode
- withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
- // Create a user.
- userID, addrID, err := s.CreateUser("user", password)
- require.NoError(t, err)
-
- labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
- require.NoError(t, err)
-
- // Create 10 messages for the user.
- withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
- createNumMessages(ctx, t, c, addrID, labelID, 10)
- })
-
- withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
- userLoginAndSync(ctx, t, bridge, "user", password)
-
- var messageIDs []string
-
- // Create 10 more messages for the user, generating events.
- withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
- messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
- })
-
- mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
-
- s.AddStatusHook(func(req *http.Request) (int, bool) {
- if xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string {
- return "/mail/v4/messages/" + messageID
- }), req.URL.Path) < 0 {
- return 0, false
- }
-
- return http.StatusServiceUnavailable, true
- })
-
- userContinueEventProcess(ctx, t, s, bridge)
- })
- })
-}
-
// userLoginAndSync logs in user and waits until user is fully synced.
func userLoginAndSync(
ctx context.Context,
@@ -928,10 +902,10 @@ func userContinueEventProcess(
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
- client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
+ cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
- require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
- defer func() { _ = client.Logout() }()
+ require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
+ defer func() { _ = cli.Logout() }()
randomLabel := uuid.NewString()
@@ -946,8 +920,21 @@ func userContinueEventProcess(
// Wait for the label to be created.
require.Eventually(t, func() bool {
- return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
+ return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == "Labels/"+randomLabel
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
}
+
+func eventuallyDial(addr string) (cli *client.Client, err error) {
+ var sleep = 1 * time.Second
+ for i := 0; i < 5; i++ {
+ cli, err := client.Dial(addr)
+ if err == nil {
+ return cli, nil
+ }
+ time.Sleep(sleep)
+ sleep *= 2
+ }
+ return nil, fmt.Errorf("after 5 attempts, last error: %s", err)
+}
diff --git a/internal/bridge/user_events.go b/internal/bridge/user_events.go
index d5a310df..8aee9e42 100644
--- a/internal/bridge/user_events.go
+++ b/internal/bridge/user_events.go
@@ -75,11 +75,7 @@ func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.U
return nil
}
- if bridge.imapServer == nil {
- return fmt.Errorf("no imap server instance running")
- }
-
- gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
+ gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
@@ -96,7 +92,7 @@ func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.U
return nil
}
- gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
+ gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
@@ -118,7 +114,7 @@ func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
- if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
+ if err := bridge.serverManager.RemoveGluonUser(ctx, gluonID); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
@@ -134,16 +130,12 @@ func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.U
return nil
}
- if bridge.imapServer == nil {
- return fmt.Errorf("no imap server instance running")
- }
-
gluonID, ok := user.GetGluonID(event.AddressID)
if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
- if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
+ if err := bridge.serverManager.handleRemoveGluonUser(ctx, gluonID); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
diff --git a/internal/bridge/user_test.go b/internal/bridge/user_test.go
index 417e7bd7..d8767110 100644
--- a/internal/bridge/user_test.go
+++ b/internal/bridge/user_test.go
@@ -708,7 +708,26 @@ func TestBridge_User_Refresh(t *testing.T) {
})
}
+func TestBridge_User_GetAddresses(t *testing.T) {
+ withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
+ // Create a user.
+ userID, _, err := s.CreateUser("user", password)
+ require.NoError(t, err)
+ addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password"))
+ require.NoError(t, err)
+ require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
+
+ withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
+ userLoginAndSync(ctx, t, bridge, "user", password)
+ info, err := bridge.GetUserInfo(userID)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(info.Addresses))
+ require.Equal(t, info.Addresses[0], "user@proton.local")
+ })
+ })
+}
+
// getErr returns the error that was passed to it.
-func getErr[T any](val T, err error) error {
+func getErr[T any](_ T, err error) error {
return err
}
diff --git a/internal/certs/cert_store_darwin.go b/internal/certs/cert_store_darwin.go
index 8e1f9570..97a79fa5 100644
--- a/internal/certs/cert_store_darwin.go
+++ b/internal/certs/cert_store_darwin.go
@@ -50,7 +50,7 @@ int installTrustedCert(char const *bytes, unsigned long long length) {
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
};
- status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainAdmin, (__bridge CFTypeRef)(trustSettings));
+ status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy);
CFRelease(cert);
@@ -72,7 +72,7 @@ int removeTrustedCert(char const *bytes, unsigned long long length) {
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified],
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
};
- OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainAdmin, (__bridge CFTypeRef)(trustSettings));
+ OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy);
if (errSecSuccess != status) {
CFRelease(cert);
@@ -107,7 +107,6 @@ const (
// certPEMToDER converts a certificate in PEM format to DER format, which is the format required by Apple's Security framework.
func certPEMToDER(certPEM []byte) ([]byte, error) {
-
block, left := pem.Decode(certPEM)
if block == nil {
return []byte{}, errors.New("invalid PEM certificate")
@@ -127,7 +126,7 @@ func installCert(certPEM []byte) error {
}
p := C.CBytes(certDER)
- defer C.free(unsafe.Pointer(p))
+ defer C.free(unsafe.Pointer(p)) //nolint:unconvert
errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER)))
switch errCode {
@@ -147,7 +146,7 @@ func uninstallCert(certPEM []byte) error {
}
p := C.CBytes(certDER)
- defer C.free(unsafe.Pointer(p))
+ defer C.free(unsafe.Pointer(p)) //nolint:unconvert
if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 {
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
diff --git a/internal/certs/cert_store_darwin_test.go b/internal/certs/cert_store_darwin_test.go
index 9ac9c698..3b1d419f 100644
--- a/internal/certs/cert_store_darwin_test.go
+++ b/internal/certs/cert_store_darwin_test.go
@@ -26,7 +26,7 @@ import (
)
// This test implies human interactions to enter password and is disabled by default.
-func _TestTrustedCertsDarwin(t *testing.T) {
+func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused
template, err := NewTLSTemplate()
require.NoError(t, err)
diff --git a/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt b/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt
index 77f06e49..2c14ed83 100644
--- a/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt
+++ b/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt
@@ -75,7 +75,7 @@ if(NOT UNIX)
set(CMAKE_INSTALL_BINDIR ".")
endif(NOT UNIX)
-find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets REQUIRED)
+find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets Svg REQUIRED)
qt_standard_project_setup()
set(CMAKE_AUTORCC ON)
message(STATUS "Using Qt ${Qt6_VERSION}")
@@ -147,6 +147,7 @@ target_link_libraries(bridge-gui
Qt6::Quick
Qt6::Qml
Qt6::QuickControls2
+ Qt6::Svg
sentry::sentry
bridgepp
)
diff --git a/internal/frontend/bridge-gui/bridge-gui/Pch.h b/internal/frontend/bridge-gui/bridge-gui/Pch.h
index b34e31d3..cb7bcb99 100644
--- a/internal/frontend/bridge-gui/bridge-gui/Pch.h
+++ b/internal/frontend/bridge-gui/bridge-gui/Pch.h
@@ -25,6 +25,7 @@
#include
#include
#include
+#include
#include
diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp
index 2435426c..b10002a4 100644
--- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp
+++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp
@@ -994,15 +994,44 @@ void QMLBackend::onUserBadEvent(QString const &userID, QString const &) {
void QMLBackend::onIMAPLoginFailed(QString const &username) {
HANDLE_EXCEPTION(
SPUser const user = users_->getUserWithUsernameOrEmail(username);
- if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected
+ if (!user) {
return;
}
- if (user->isInIMAPLoginFailureCooldown()) {
- return;
+
+ qint64 const cooldownDurationMs = 10 * 60 * 1000; // 10 minutes cooldown period for notifications
+ switch (user->state()) {
+ case UserState::SignedOut:
+ if (user->isNotificationInCooldown(User::ENotification::IMAPLoginWhileSignedOut)) {
+ return;
+ }
+ user->startNotificationCooldownPeriod(User::ENotification::IMAPLoginWhileSignedOut, cooldownDurationMs);
+ emit selectUser(user->id(), true);
+ emit imapLoginWhileSignedOut(username);
+ break;
+
+ case UserState::Connected:
+ if (user->isNotificationInCooldown(User::ENotification::IMAPPasswordFailure)) {
+ return;
+ }
+ user->startNotificationCooldownPeriod(User::ENotification::IMAPPasswordFailure, cooldownDurationMs);
+ emit selectUser(user->id(), false);
+ trayIcon_->showErrorPopupNotification(tr("Incorrect password"),
+ tr("Your email client can't connect to Proton Bridge. Make sure you are using the local Bridge password shown in Bridge."));
+ break;
+
+ case UserState::Locked:
+ if (user->isNotificationInCooldown(User::ENotification::IMAPLoginWhileLocked)) {
+ return;
+ }
+ user->startNotificationCooldownPeriod(User::ENotification::IMAPLoginWhileLocked, cooldownDurationMs);
+ emit selectUser(user->id(), false);
+ trayIcon_->showErrorPopupNotification(tr("Connection in progress"),
+ tr("Your Proton account in Bridge is being connected. Please wait or restart Bridge."));
+ break;
+
+ default:
+ break;
}
- user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again.
- emit selectUser(user->id());
- emit imapLoginWhileSignedOut(username);
)
}
@@ -1134,7 +1163,7 @@ void QMLBackend::displayBadEventDialog(QString const &userID) {
emit userBadEvent(userID,
tr("Bridge ran into an internal error and it is not able to proceed with the account %1. Synchronize your local database now or logout"
" to do it later. Synchronization time depends on the size of your mailbox.").arg(elideLongString(user->primaryEmailOrUsername(), 30)));
- emit selectUser(userID);
+ emit selectUser(userID, true);
emit showMainWindow();
)
}
diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h
index afde837e..7ee7e4f1 100644
--- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h
+++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h
@@ -180,6 +180,8 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void onVersionChanged(); ///< Slot for the version change signal.
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
+
+public slots: // slots for functions that need to be processed locally.
void setNormalTrayIcon(); ///< Set the tray icon to normal.
void setErrorTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'error' state.
void setWarnTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'warn' state.
@@ -245,7 +247,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
- void selectUser(QString const& userID); ///< Signal emitted in order to selected a user with a given ID in the list.
+ void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
diff --git a/internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp b/internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp
index b7d80720..685b1ffb 100644
--- a/internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp
+++ b/internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp
@@ -49,7 +49,7 @@ QString sentryAttachmentFilePath() {
//****************************************************************************************************************************************************
QByteArray getProtectedHostname() {
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
- return hostname.toHex();
+ return hostname.toBase64();
}
//****************************************************************************************************************************************************
diff --git a/internal/frontend/bridge-gui/bridge-gui/SentryUtils.h b/internal/frontend/bridge-gui/bridge-gui/SentryUtils.h
index 8d1d966e..7e918235 100644
--- a/internal/frontend/bridge-gui/bridge-gui/SentryUtils.h
+++ b/internal/frontend/bridge-gui/bridge-gui/SentryUtils.h
@@ -22,6 +22,7 @@
#include
void initSentry();
+QByteArray getProtectedHostname();
void setSentryReportScope();
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);
diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp
index bd00a9d1..86ba6b0b 100644
--- a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp
+++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp
@@ -43,12 +43,75 @@ qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds duri
QIcon loadIconFromImage(QString const &path) {
QPixmap const pixmap(path);
if (pixmap.isNull()) {
- throw Exception(QString("Could create icon from image '%1'.").arg(path));
+ throw Exception(QString("Could not create an icon from an image '%1'.").arg(path));
}
return QIcon(pixmap);
}
+//****************************************************************************************************************************************************
+/// \brief Generate an icon from a SVG renderer (a.k.a. path).
+///
+/// \param[in] renderer The SVG renderer.
+/// \param[in] color The color to use in case the SVG path is to be used as a mask.
+/// \return The icon.
+//****************************************************************************************************************************************************
+QIcon loadIconFromSVGRenderer(QSvgRenderer &renderer, QColor const &color = QColor()) {
+ if (!renderer.isValid()) {
+ return QIcon();
+ }
+ QIcon icon;
+ qint32 size = 256;
+
+ while (size >= 16) {
+ QPixmap pixmap(size, size);
+ pixmap.fill(QColor(0, 0, 0, 0));
+ QPainter painter(&pixmap);
+ renderer.render(&painter);
+ if (color.isValid()) {
+ painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ painter.fillRect(pixmap.rect(), color);
+ }
+ painter.end();
+ icon.addPixmap(pixmap);
+ size /= 2;
+ }
+
+ return icon;
+}
+
+
+//****************************************************************************************************************************************************
+/// \brief Load a multi-resolution icon from a SVG file. The image is assumed to be square. SVG is rasterized in 256, 128, 64, 32 and 16px.
+///
+/// Note: QPixmap can load SVG files directly, but our SVG file are defined in small shape size and QPixmap will rasterize them a very low resolution
+/// by default (eg. 16x16), which is insufficient for some uses. As a consequence, we manually generate a multi-resolution icon that render smoothly
+/// at any acceptable resolution for an icon.
+///
+/// \param[in] path The path of the SVG file.
+/// \return The icon.
+//****************************************************************************************************************************************************
+QIcon loadIconFromSVG(QString const &path, QColor const &color = QColor()) {
+ QSvgRenderer renderer(path);
+ QIcon const icon = loadIconFromSVGRenderer(renderer, color);
+ if (icon.isNull()) {
+ Exception(QString("Could not create an icon from a vector image '%1'.").arg(path));
+ }
+ return icon;
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+QIcon loadIcon(QString const &path) {
+ if (path.endsWith(".svg", Qt::CaseInsensitive)) {
+ return loadIconFromSVG(path);
+ }
+ return loadIconFromImage(path);
+}
+
+
//****************************************************************************************************************************************************
/// \brief Retrieve the color associated with a tray icon state.
///
@@ -95,6 +158,18 @@ QString stateText(TrayIcon::State state) {
}
+//****************************************************************************************************************************************************
+/// \brief converts a QML resource path to Qt resource path.
+/// QML resource paths are a bit different from qt resource paths
+/// \param[in] path The resource path.
+/// \return
+//****************************************************************************************************************************************************
+QString qmlResourcePathToQt(QString const &path) {
+ QString result = path;
+ result.replace(QRegularExpression(R"(^\.\/)"), ":/qml/");
+ return result;
+}
+
} // anonymous namespace
@@ -103,17 +178,17 @@ QString stateText(TrayIcon::State state) {
//****************************************************************************************************************************************************
TrayIcon::TrayIcon()
: QSystemTrayIcon()
- , menu_(new QMenu) {
-
+ , menu_(new QMenu)
+ , notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) {
this->generateDotIcons();
this->setContextMenu(menu_.get());
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser);
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated);
-
+ // some OSes/Desktop managers will automatically show main window when clicked, but not all, so we do it manually.
+ connect(this, &TrayIcon::messageClicked, &app().backend(), &QMLBackend::showMainWindow);
this->show();
- this->setState(State::Normal, QString(), QString());
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
for (QScreen *screen: QGuiApplication::screens()) {
@@ -151,7 +226,7 @@ void TrayIcon::onUserClicked() {
throw Exception("Could not retrieve context menu's selected user.");
}
- emit selectUser(userID);
+ emit selectUser(userID, true);
} catch (Exception const &e) {
app().log().error(e.qwhat());
}
@@ -212,18 +287,17 @@ void TrayIcon::onIconRefreshTimer() {
//
//****************************************************************************************************************************************************
void TrayIcon::generateDotIcons() {
- QPixmap dotSVG(":/qml/icons/ic-dot.svg");
+ QSvgRenderer dotSVG(QString(":/qml/icons/ic-dot.svg"));
+
struct IconColor {
QIcon &icon;
QColor color;
};
for (auto pair: QList {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) {
- QPixmap p = dotSVG;
- QPainter painter(&p);
- painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
- painter.fillRect(p.rect(), pair.color);
- painter.end();
- pair.icon = QIcon(p);
+ pair.icon = loadIconFromSVGRenderer(dotSVG, pair.color);
+ if (pair.icon.isNull()) {
+ throw Exception("Could not generate dot icon from vector file.");
+ }
}
}
@@ -242,26 +316,28 @@ void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QStri
}
+//****************************************************************************************************************************************************
+/// \param[in] title The title.
+/// \param[in] message The message.
+//****************************************************************************************************************************************************
+void TrayIcon::showErrorPopupNotification(QString const &title, QString const &message) {
+ this->showMessage(title, message, notificationErrorIcon_);
+}
+
+
//****************************************************************************************************************************************************
/// \param[in] svgPath The path of the SVG file for the icon.
/// \param[in] color The color to apply to the icon.
//****************************************************************************************************************************************************
void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) {
// We use the SVG path as pixmap mask and fill it with the appropriate color
- QString resourcePath = svgPath;
- resourcePath.replace(QRegularExpression(R"(^\.\/)"), ":/qml/"); // QML resource path are a bit different from the Qt resources path.
- QPixmap pixmap(resourcePath);
- QPainter painter(&pixmap);
- painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
- painter.fillRect(pixmap.rect(), color);
- painter.end();
- statusIcon_ = QIcon(pixmap);
+ statusIcon_ = loadIconFromSVG(qmlResourcePathToQt(svgPath), color);
}
-//**********************************************************************************************************************
+//****************************************************************************************************************************************************
//
-//**********************************************************************************************************************
+//****************************************************************************************************************************************************
void TrayIcon::refreshContextMenu() {
if (!menu_) {
app().log().error("Native tray icon context menu is null.");
@@ -297,3 +373,5 @@ void TrayIcon::refreshContextMenu() {
menu_->addSeparator();
menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit);
}
+
+
diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h
index 74cfdc94..058ca1c2 100644
--- a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h
+++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h
@@ -41,10 +41,10 @@ public: // data members
TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator.
TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator.
void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon
- void showNotificationPopup(QString const& title, QString const &message, QString const& iconPath); ///< Display a pop up notification.
+ void showErrorPopupNotification(QString const& title, QString const &message); ///< Display a pop up notification.
signals:
- void selectUser(QString const& userID); ///< Signal for selecting a user with a given userID
+ void selectUser(QString const& userID, bool forceShowWindow); ///< Signal for selecting a user with a given userID
private slots:
void onMenuAboutToShow(); ///< Slot called before the context menu is shown.
@@ -67,6 +67,7 @@ private: // data members
QIcon greenDot_; ///< The green dot icon.
QIcon greyDot_; ///< The grey dot icon.
QIcon orangeDot_; ///< The orange dot icon.
+ QIcon const notificationErrorIcon_; ///< The error icon used for notifications.
QTimer iconRefreshTimer_; ///< The timer used to periodically refresh the icon when DPI changes.
QDateTime iconRefreshDeadline_; ///< The deadline for refreshing the icon
diff --git a/internal/frontend/bridge-gui/bridge-gui/main.cpp b/internal/frontend/bridge-gui/bridge-gui/main.cpp
index b7f3225f..617d8b85 100644
--- a/internal/frontend/bridge-gui/bridge-gui/main.cpp
+++ b/internal/frontend/bridge-gui/bridge-gui/main.cpp
@@ -305,6 +305,8 @@ int main(int argc, char *argv[]) {
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
// these outputs and output them on the command-line.
log.setLevel(cliOptions.logLevel);
+ log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
+
QString bridgeexec;
if (!cliOptions.attach) {
if (isBridgeRunning()) {
diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml b/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml
index 9295d7f5..3de94f99 100644
--- a/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml
+++ b/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml
@@ -95,9 +95,11 @@ ApplicationWindow {
root.showAndRise()
}
- function onSelectUser(userID) {
+ function onSelectUser(userID, forceShowWindow) {
contentWrapper.selectUser(userID)
- root.showAndRise()
+ if (forceShowWindow) {
+ root.showAndRise()
+ }
}
}
diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml b/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml
index 1431aa36..96f13669 100644
--- a/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml
+++ b/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml
@@ -535,11 +535,12 @@ QtObject {
}
property Notification onlyPaidUsers: Notification {
- description: qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge.")
+ description: qsTr("Bridge is exclusive to our mail paid plans. Upgrade your account to use Bridge.")
brief: qsTr("Upgrade your account")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration
+ property var pricingLink: "https://proton.me/mail/pricing"
Connections {
target: Backend
@@ -550,8 +551,9 @@ QtObject {
action: [
Action {
- text: qsTr("OK")
+ text: qsTr("Upgrade")
onTriggered: {
+ Qt.openUrlExternally(root.onlyPaidUsers.pricingLink)
root.onlyPaidUsers.active = false
}
}
diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml b/internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml
index d3d9bef8..006eb543 100644
--- a/internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml
+++ b/internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml
@@ -344,7 +344,12 @@ FocusScope {
if (str.length === 0) {
return qsTr("Enter the 6-digit code")
}
- return
+ }
+
+ onTextChanged: {
+ if (text.length >= 6) {
+ twoFAButton.onClicked()
+ }
}
onAccepted: {
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.cc b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.cc
index 55d3e961..68bc4e56 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.cc
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.cc
@@ -6,19 +6,19 @@
#include "focus.grpc.pb.h"
#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
-#include
-#include
-#include
+#include
+#include
+#include
namespace focus {
static const char* Focus_method_names[] = {
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.h
index be4b3441..bc59c52d 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.h
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.h
@@ -25,23 +25,23 @@
#include "focus.pb.h"
#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
-#include
-#include
+#include
+#include
#include
-#include
-#include
+#include
+#include
#include
-#include
-#include
+#include
+#include
namespace focus {
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.pb.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.pb.h
index 53a7184f..cd281b7f 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.pb.h
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.pb.h
@@ -13,7 +13,7 @@
#error incompatible with your Protocol Buffer headers. Please update
#error your headers.
#endif
-#if 3021003 < PROTOBUF_MIN_PROTOC_VERSION
+#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION
#error This file was generated by an older version of protoc which is
#error incompatible with your Protocol Buffer headers. Please
#error regenerate this file with a newer version of protoc.
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc
index 53beac31..a032ba05 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc
@@ -6,19 +6,19 @@
#include "bridge.grpc.pb.h"
#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
-#include
-#include
-#include
+#include
+#include
+#include
namespace grpc {
static const char* Bridge_method_names[] = {
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.h
index 86387cc3..092e37b5 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.h
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.h
@@ -25,23 +25,23 @@
#include "bridge.pb.h"
#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
-#include
-#include
+#include
+#include
#include
-#include
-#include
+#include
+#include
#include
-#include
-#include
+#include
+#include
namespace grpc {
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.pb.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.pb.h
index ea2f44df..b32f4612 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.pb.h
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.pb.h
@@ -13,7 +13,7 @@
#error incompatible with your Protocol Buffer headers. Please update
#error your headers.
#endif
-#if 3021003 < PROTOBUF_MIN_PROTOC_VERSION
+#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION
#error This file was generated by an older version of protoc which is
#error incompatible with your Protocol Buffer headers. Please
#error regenerate this file with a newer version of protoc.
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.cpp
index 041a4d12..0059f46a 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.cpp
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.cpp
@@ -34,9 +34,7 @@ SPUser User::newUser(QObject *parent) {
/// \param[in] parent The parent object.
//****************************************************************************************************************************************************
User::User(QObject *parent)
- : QObject(parent)
- , imapFailureCooldownEndTime_(QDateTime::currentDateTime()) {
-
+ : QObject(parent) {
}
@@ -355,22 +353,18 @@ QString User::stateToString(UserState state) {
//****************************************************************************************************************************************************
-/// We display a notification and pop the application window if an IMAP client tries to connect to a signed out account, but we do not want to
-/// do it repeatedly, as it's an intrusive action. This function let's you define a period of time during which the notification should not be
-/// displayed.
-///
-/// \param durationMSecs The duration of the period in milliseconds.
+/// \param[in] durationMSecs The duration of the period in milliseconds.
//****************************************************************************************************************************************************
-void User::startImapLoginFailureCooldown(qint64 durationMSecs) {
- imapFailureCooldownEndTime_ = QDateTime::currentDateTime().addMSecs(durationMSecs);
+void User::startNotificationCooldownPeriod(User::ENotification notification, qint64 durationMSecs) {
+ notificationCooldownList_[notification] = QDateTime::currentDateTime().addMSecs(durationMSecs);
}
//****************************************************************************************************************************************************
-/// \return true if we currently are in a cooldown period for the notification
+/// \return true iff the notification is currently in a cooldown period.
//****************************************************************************************************************************************************
-bool User::isInIMAPLoginFailureCooldown() const {
- return QDateTime::currentDateTime() < imapFailureCooldownEndTime_;
+bool User::isNotificationInCooldown(User::ENotification notification) const {
+ return notificationCooldownList_.contains(notification) && (QDateTime::currentDateTime() < notificationCooldownList_[notification]);
}
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h
index 78cfe8c9..b63e51d6 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h
@@ -62,6 +62,13 @@ typedef std::shared_ptr SPUser; ///< Type definition for shared poin
class User : public QObject {
Q_OBJECT
+public: // data types
+ enum class ENotification {
+ IMAPLoginWhileSignedOut, ///< An IMAP client tried to login while the user is signed out.
+ IMAPPasswordFailure, ///< An IMAP client provided an invalid password for the user.
+ IMAPLoginWhileLocked, ///< An IMAP client tried to connect while the user is locked.
+ };
+
public: // static member function
static SPUser newUser(QObject *parent); ///< Create a new user
static QString stateToString(UserState state); ///< Return a string describing a user state.
@@ -74,8 +81,8 @@ public: // member functions.
User &operator=(User &&) = delete; ///< Disabled move assignment operator.
void update(User const &user); ///< Update the user.
Q_INVOKABLE QString primaryEmailOrUsername() const; ///< Return the user primary email, or, if unknown its username.
- void startImapLoginFailureCooldown(qint64 durationMSecs); ///< Start the user cooldown period for the IMAP login attempt while signed-out notification.
- bool isInIMAPLoginFailureCooldown() const; ///< Check if the user in a IMAP login failure notification.
+ void startNotificationCooldownPeriod(ENotification notification, qint64 durationMSecs); ///< Start the user cooldown period for a notification.
+ bool isNotificationInCooldown(ENotification notification) const; ///< Return true iff the notification is in a cooldown period.
public slots:
// slots for QML generated calls
@@ -147,7 +154,7 @@ private: // member functions.
User(QObject *parent); ///< Default constructor.
private: // data members.
- QDateTime imapFailureCooldownEndTime_; ///< The end date/time for the IMAP login failure notification cooldown period.
+ QMap notificationCooldownList_; ///< A list of cooldown period end time for notifications.
QString id_; ///< The userID.
QString username_; ///< The username
QString password_; ///< The IMAP password of the user.
diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go
index 09e3c15a..cabb7413 100644
--- a/internal/frontend/cli/accounts.go
+++ b/internal/frontend/cli/accounts.go
@@ -297,7 +297,7 @@ func (f *frontendCLI) configureAppleMail(c *ishell.Context) {
return
}
- if err := f.bridge.ConfigureAppleMail(user.UserID, user.Addresses[0]); err != nil {
+ if err := f.bridge.ConfigureAppleMail(context.Background(), user.UserID, user.Addresses[0]); err != nil {
f.printAndLogError(err)
return
}
@@ -305,11 +305,11 @@ func (f *frontendCLI) configureAppleMail(c *ishell.Context) {
f.Printf("Apple Mail configured for %v with address %v\n", user.Username, user.Addresses[0])
}
-func (f *frontendCLI) badEventSynchronize(c *ishell.Context) {
+func (f *frontendCLI) badEventSynchronize(_ *ishell.Context) {
f.badEventFeedback(true)
}
-func (f *frontendCLI) badEventLogout(c *ishell.Context) {
+func (f *frontendCLI) badEventLogout(_ *ishell.Context) {
f.badEventFeedback(false)
}
diff --git a/internal/frontend/cli/frontend.go b/internal/frontend/cli/frontend.go
index 4422e427..03d66e16 100644
--- a/internal/frontend/cli/frontend.go
+++ b/internal/frontend/cli/frontend.go
@@ -20,6 +20,7 @@ package cli
import (
"errors"
+ "os"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
@@ -60,6 +61,11 @@ func New(
panicHandler: panicHandler,
}
+ // We want to exit at the first Ctrl+C. By default, ishell requires two.
+ fe.Interrupt(func(_ *ishell.Context, _ int, _ string) {
+ os.Exit(1)
+ })
+
// Clear commands.
clearCmd := &ishell.Cmd{
Name: "clear",
diff --git a/internal/frontend/cli/system.go b/internal/frontend/cli/system.go
index 1290d0fa..d163db1f 100644
--- a/internal/frontend/cli/system.go
+++ b/internal/frontend/cli/system.go
@@ -31,7 +31,7 @@ import (
"github.com/abiosoft/ishell"
)
-func (f *frontendCLI) printLogDir(c *ishell.Context) {
+func (f *frontendCLI) printLogDir(_ *ishell.Context) {
if path, err := f.bridge.GetLogsPath(); err != nil {
f.Println("Failed to determine location of log files")
} else {
@@ -39,17 +39,17 @@ func (f *frontendCLI) printLogDir(c *ishell.Context) {
}
}
-func (f *frontendCLI) printManual(c *ishell.Context) {
+func (f *frontendCLI) printManual(_ *ishell.Context) {
f.Println("More instructions about the Bridge can be found at\n\n https://proton.me/mail/bridge")
}
-func (f *frontendCLI) printCredits(c *ishell.Context) {
+func (f *frontendCLI) printCredits(_ *ishell.Context) {
for _, pkg := range strings.Split(bridge.Credits, ";") {
f.Println(pkg)
}
}
-func (f *frontendCLI) changeIMAPSecurity(c *ishell.Context) {
+func (f *frontendCLI) changeIMAPSecurity(_ *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
@@ -61,14 +61,14 @@ func (f *frontendCLI) changeIMAPSecurity(c *ishell.Context) {
msg := fmt.Sprintf("Are you sure you want to change IMAP setting to %q", newSecurity)
if f.yesNoQuestion(msg) {
- if err := f.bridge.SetIMAPSSL(!f.bridge.GetIMAPSSL()); err != nil {
+ if err := f.bridge.SetIMAPSSL(context.Background(), !f.bridge.GetIMAPSSL()); err != nil {
f.printAndLogError(err)
return
}
}
}
-func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
+func (f *frontendCLI) changeSMTPSecurity(_ *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
@@ -80,7 +80,7 @@ func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) {
msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q", newSecurity)
if f.yesNoQuestion(msg) {
- if err := f.bridge.SetSMTPSSL(!f.bridge.GetSMTPSSL()); err != nil {
+ if err := f.bridge.SetSMTPSSL(context.Background(), !f.bridge.GetSMTPSSL()); err != nil {
f.printAndLogError(err)
return
}
@@ -103,7 +103,7 @@ func (f *frontendCLI) changeIMAPPort(c *ishell.Context) {
return
}
- if err := f.bridge.SetIMAPPort(newIMAPPortInt); err != nil {
+ if err := f.bridge.SetIMAPPort(context.Background(), newIMAPPortInt); err != nil {
f.printAndLogError(err)
return
}
@@ -125,13 +125,13 @@ func (f *frontendCLI) changeSMTPPort(c *ishell.Context) {
return
}
- if err := f.bridge.SetSMTPPort(newSMTPPortInt); err != nil {
+ if err := f.bridge.SetSMTPPort(context.Background(), newSMTPPortInt); err != nil {
f.printAndLogError(err)
return
}
}
-func (f *frontendCLI) allowProxy(c *ishell.Context) {
+func (f *frontendCLI) allowProxy(_ *ishell.Context) {
if f.bridge.GetProxyAllowed() {
f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.")
return
@@ -147,7 +147,7 @@ func (f *frontendCLI) allowProxy(c *ishell.Context) {
}
}
-func (f *frontendCLI) disallowProxy(c *ishell.Context) {
+func (f *frontendCLI) disallowProxy(_ *ishell.Context) {
if !f.bridge.GetProxyAllowed() {
f.Println("Bridge is already set to NOT use alternative routing to connect to Proton if it is being blocked.")
return
@@ -163,7 +163,7 @@ func (f *frontendCLI) disallowProxy(c *ishell.Context) {
}
}
-func (f *frontendCLI) hideAllMail(c *ishell.Context) {
+func (f *frontendCLI) hideAllMail(_ *ishell.Context) {
if !f.bridge.GetShowAllMail() {
f.Println("All Mail folder is not listed in your local client.")
return
@@ -179,7 +179,7 @@ func (f *frontendCLI) hideAllMail(c *ishell.Context) {
}
}
-func (f *frontendCLI) showAllMail(c *ishell.Context) {
+func (f *frontendCLI) showAllMail(_ *ishell.Context) {
if f.bridge.GetShowAllMail() {
f.Println("All Mail folder is listed in your local client.")
return
diff --git a/internal/frontend/cli/updates.go b/internal/frontend/cli/updates.go
index baf253c8..2952b56e 100644
--- a/internal/frontend/cli/updates.go
+++ b/internal/frontend/cli/updates.go
@@ -23,7 +23,7 @@ import (
"github.com/abiosoft/ishell"
)
-func (f *frontendCLI) checkUpdates(c *ishell.Context) {
+func (f *frontendCLI) checkUpdates(_ *ishell.Context) {
updateCh, done := f.bridge.GetEvents(events.UpdateAvailable{}, events.UpdateNotAvailable{})
defer done()
@@ -38,7 +38,7 @@ func (f *frontendCLI) checkUpdates(c *ishell.Context) {
}
}
-func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) {
+func (f *frontendCLI) enableAutoUpdates(_ *ishell.Context) {
if f.bridge.GetAutoUpdate() {
f.Println("Bridge is already set to automatically install updates.")
return
@@ -54,7 +54,7 @@ func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) {
}
}
-func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) {
+func (f *frontendCLI) disableAutoUpdates(_ *ishell.Context) {
if !f.bridge.GetAutoUpdate() {
f.Println("Bridge is already set to NOT automatically install updates.")
return
@@ -70,7 +70,7 @@ func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) {
}
}
-func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) {
+func (f *frontendCLI) selectEarlyChannel(_ *ishell.Context) {
if f.bridge.GetUpdateChannel() == updater.EarlyChannel {
f.Println("Bridge is already on the early-access update channel.")
return
@@ -86,7 +86,7 @@ func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) {
}
}
-func (f *frontendCLI) selectStableChannel(c *ishell.Context) {
+func (f *frontendCLI) selectStableChannel(_ *ishell.Context) {
if f.bridge.GetUpdateChannel() == updater.StableChannel {
f.Println("Bridge is already on the stable update channel.")
return
diff --git a/internal/frontend/grpc/service_methods.go b/internal/frontend/grpc/service_methods.go
index a49e698a..941d0a6f 100644
--- a/internal/frontend/grpc/service_methods.go
+++ b/internal/frontend/grpc/service_methods.go
@@ -47,7 +47,7 @@ import (
)
// CheckTokens implements the CheckToken gRPC service call.
-func (s *Service) CheckTokens(ctx context.Context, clientConfigPath *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
+func (s *Service) CheckTokens(_ context.Context, clientConfigPath *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
s.log.Debug("CheckTokens")
path := clientConfigPath.Value
@@ -65,7 +65,7 @@ func (s *Service) CheckTokens(ctx context.Context, clientConfigPath *wrapperspb.
return &wrapperspb.StringValue{Value: clientConfig.Token}, nil
}
-func (s *Service) AddLogEntry(ctx context.Context, request *AddLogEntryRequest) (*emptypb.Empty, error) {
+func (s *Service) AddLogEntry(_ context.Context, request *AddLogEntryRequest) (*emptypb.Empty, error) {
entry := s.log
if len(request.Package) > 0 {
@@ -93,7 +93,7 @@ func (s *Service) AddLogEntry(ctx context.Context, request *AddLogEntryRequest)
}
// GuiReady implement the GuiReady gRPC service call.
-func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*GuiReadyResponse, error) {
+func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyResponse, error) {
s.log.Debug("GuiReady")
s.initializationDone.Do(s.initializing.Done)
@@ -107,7 +107,7 @@ func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*GuiReadyResp
}
// Quit implement the Quit gRPC service call.
-func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
+func (s *Service) Quit(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Debug("Quit")
return &emptypb.Empty{}, s.quit()
}
@@ -143,13 +143,13 @@ func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.E
return s.Quit(ctx, empty)
}
-func (s *Service) ShowOnStartup(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
+func (s *Service) ShowOnStartup(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Debug("ShowOnStartup")
return wrapperspb.Bool(s.showOnStartup), nil
}
-func (s *Service) SetIsAutostartOn(ctx context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
+func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("show", isOn.Value).Debug("SetIsAutostartOn")
defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }()
@@ -169,13 +169,13 @@ func (s *Service) SetIsAutostartOn(ctx context.Context, isOn *wrapperspb.BoolVal
return &emptypb.Empty{}, nil
}
-func (s *Service) IsAutostartOn(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
+func (s *Service) IsAutostartOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Debug("IsAutostartOn")
return wrapperspb.Bool(s.bridge.GetAutostart()), nil
}
-func (s *Service) SetIsBetaEnabled(ctx context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
+func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsBetaEnabled")
channel := updater.StableChannel
@@ -191,13 +191,13 @@ func (s *Service) SetIsBetaEnabled(ctx context.Context, isEnabled *wrapperspb.Bo
return &emptypb.Empty{}, nil
}
-func (s *Service) IsBetaEnabled(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
+func (s *Service) IsBetaEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Debug("IsBetaEnabled")
return wrapperspb.Bool(s.bridge.GetUpdateChannel() == updater.EarlyChannel), nil
}
-func (s *Service) SetIsAllMailVisible(ctx context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) {
+func (s *Service) SetIsAllMailVisible(_ context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("isVisible", isVisible.Value).Debug("SetIsAllMailVisible")
if err := s.bridge.SetShowAllMail(isVisible.Value); err != nil {
@@ -208,7 +208,7 @@ func (s *Service) SetIsAllMailVisible(ctx context.Context, isVisible *wrapperspb
return &emptypb.Empty{}, nil
}
-func (s *Service) IsAllMailVisible(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
+func (s *Service) IsAllMailVisible(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Debug("IsAllMailVisible")
return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil
@@ -231,13 +231,13 @@ func (s *Service) IsTelemetryDisabled(_ context.Context, _ *emptypb.Empty) (*wra
return wrapperspb.Bool(s.bridge.GetTelemetryDisabled()), nil
}
-func (s *Service) GoOs(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) GoOs(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name
return wrapperspb.String(runtime.GOOS), nil
}
-func (s *Service) TriggerReset(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
+func (s *Service) TriggerReset(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Debug("TriggerReset")
go func() {
@@ -248,13 +248,13 @@ func (s *Service) TriggerReset(ctx context.Context, _ *emptypb.Empty) (*emptypb.
return &emptypb.Empty{}, nil
}
-func (s *Service) Version(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) Version(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("Version")
return wrapperspb.String(s.bridge.GetCurrentVersion().Original()), nil
}
-func (s *Service) LogsPath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) LogsPath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("LogsPath")
path, err := s.bridge.GetLogsPath()
@@ -265,7 +265,7 @@ func (s *Service) LogsPath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.S
return wrapperspb.String(path), nil
}
-func (s *Service) LicensePath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) LicensePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("LicensePath")
return wrapperspb.String(s.bridge.GetLicenseFilePath()), nil
@@ -275,7 +275,7 @@ func (s *Service) DependencyLicensesLink(_ context.Context, _ *emptypb.Empty) (*
return wrapperspb.String(s.bridge.GetDependencyLicensesLink()), nil
}
-func (s *Service) ReleaseNotesPageLink(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) ReleaseNotesPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.latestLock.RLock()
defer s.latestLock.RUnlock()
@@ -289,7 +289,7 @@ func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapper
return wrapperspb.String(s.latest.LandingPage), nil
}
-func (s *Service) SetColorSchemeName(ctx context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
+func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("ColorSchemeName", name.Value).Debug("SetColorSchemeName")
if !theme.IsAvailable(theme.Theme(name.Value)) {
@@ -305,7 +305,7 @@ func (s *Service) SetColorSchemeName(ctx context.Context, name *wrapperspb.Strin
return &emptypb.Empty{}, nil
}
-func (s *Service) ColorSchemeName(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) ColorSchemeName(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("ColorSchemeName")
current := s.bridge.GetColorScheme()
@@ -320,13 +320,13 @@ func (s *Service) ColorSchemeName(ctx context.Context, _ *emptypb.Empty) (*wrapp
return wrapperspb.String(current), nil
}
-func (s *Service) CurrentEmailClient(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) CurrentEmailClient(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("CurrentEmailClient")
return wrapperspb.String(s.bridge.GetCurrentUserAgent()), nil
}
-func (s *Service) ReportBug(ctx context.Context, report *ReportBugRequest) (*emptypb.Empty, error) {
+func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*emptypb.Empty, error) {
s.log.WithFields(logrus.Fields{
"osType": report.OsType,
"osVersion": report.OsVersion,
@@ -382,7 +382,7 @@ func (s *Service) ExportTLSCertificates(_ context.Context, folderPath *wrappersp
return &emptypb.Empty{}, nil
}
-func (s *Service) ForceLauncher(ctx context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) {
+func (s *Service) ForceLauncher(_ context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("launcher", launcher.Value).Debug("ForceLauncher")
s.restarter.Override(launcher.Value)
@@ -390,7 +390,7 @@ func (s *Service) ForceLauncher(ctx context.Context, launcher *wrapperspb.String
return &emptypb.Empty{}, nil
}
-func (s *Service) SetMainExecutable(ctx context.Context, exe *wrapperspb.StringValue) (*emptypb.Empty, error) {
+func (s *Service) SetMainExecutable(_ context.Context, exe *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("executable", exe.Value).Debug("SetMainExecutable")
s.restarter.AddFlags("--wait", exe.Value)
@@ -398,7 +398,7 @@ func (s *Service) SetMainExecutable(ctx context.Context, exe *wrapperspb.StringV
return &emptypb.Empty{}, nil
}
-func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) {
+func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
s.log.WithField("username", login.Username).Debug("Login")
go func() {
@@ -454,7 +454,7 @@ func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empt
return &emptypb.Empty{}, nil
}
-func (s *Service) Login2FA(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) {
+func (s *Service) Login2FA(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
s.log.WithField("username", login.Username).Debug("Login2FA")
go func() {
@@ -499,7 +499,7 @@ func (s *Service) Login2FA(ctx context.Context, login *LoginRequest) (*emptypb.E
return &emptypb.Empty{}, nil
}
-func (s *Service) Login2Passwords(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) {
+func (s *Service) Login2Passwords(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
s.log.WithField("username", login.Username).Debug("Login2Passwords")
go func() {
@@ -521,7 +521,7 @@ func (s *Service) Login2Passwords(ctx context.Context, login *LoginRequest) (*em
return &emptypb.Empty{}, nil
}
-func (s *Service) LoginAbort(ctx context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) {
+func (s *Service) LoginAbort(_ context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) {
s.log.WithField("username", loginAbort.Username).Debug("LoginAbort")
go func() {
@@ -565,7 +565,7 @@ func (s *Service) CheckUpdate(context.Context, *emptypb.Empty) (*emptypb.Empty,
return &emptypb.Empty{}, nil
}
-func (s *Service) InstallUpdate(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
+func (s *Service) InstallUpdate(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Debug("InstallUpdate")
go func() {
@@ -579,7 +579,7 @@ func (s *Service) InstallUpdate(ctx context.Context, _ *emptypb.Empty) (*emptypb
return &emptypb.Empty{}, nil
}
-func (s *Service) SetIsAutomaticUpdateOn(ctx context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
+func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("isOn", isOn.Value).Debug("SetIsAutomaticUpdateOn")
if currentlyOn := s.bridge.GetAutoUpdate(); currentlyOn == isOn.Value {
@@ -594,19 +594,19 @@ func (s *Service) SetIsAutomaticUpdateOn(ctx context.Context, isOn *wrapperspb.B
return &emptypb.Empty{}, nil
}
-func (s *Service) IsAutomaticUpdateOn(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
+func (s *Service) IsAutomaticUpdateOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Debug("IsAutomaticUpdateOn")
return wrapperspb.Bool(s.bridge.GetAutoUpdate()), nil
}
-func (s *Service) DiskCachePath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) DiskCachePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("DiskCachePath")
return wrapperspb.String(s.bridge.GetGluonCacheDir()), nil
}
-func (s *Service) SetDiskCachePath(ctx context.Context, newPath *wrapperspb.StringValue) (*emptypb.Empty, error) {
+func (s *Service) SetDiskCachePath(_ context.Context, newPath *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("path", newPath.Value).Debug("setDiskCachePath")
go func() {
@@ -637,7 +637,7 @@ func (s *Service) SetDiskCachePath(ctx context.Context, newPath *wrapperspb.Stri
return &emptypb.Empty{}, nil
}
-func (s *Service) SetIsDoHEnabled(ctx context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
+func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsDohEnabled")
if err := s.bridge.SetProxyAllowed(isEnabled.Value); err != nil {
@@ -648,7 +648,7 @@ func (s *Service) SetIsDoHEnabled(ctx context.Context, isEnabled *wrapperspb.Boo
return &emptypb.Empty{}, nil
}
-func (s *Service) IsDoHEnabled(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
+func (s *Service) IsDoHEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Debug("IsDohEnabled")
return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil
@@ -668,7 +668,7 @@ func (s *Service) MailServerSettings(_ context.Context, _ *emptypb.Empty) (*Imap
}, nil
}
-func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSettings) (*emptypb.Empty, error) {
+func (s *Service) SetMailServerSettings(ctx context.Context, settings *ImapSmtpSettings) (*emptypb.Empty, error) {
s.log.
WithField("ImapPort", settings.ImapPort).
WithField("SmtpPort", settings.SmtpPort).
@@ -682,28 +682,28 @@ func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSet
defer func() { _ = s.SendEvent(NewChangeMailServerSettingsFinishedEvent()) }()
if s.bridge.GetIMAPSSL() != settings.UseSSLForImap {
- if err := s.bridge.SetIMAPSSL(settings.UseSSLForImap); err != nil {
+ if err := s.bridge.SetIMAPSSL(ctx, settings.UseSSLForImap); err != nil {
s.log.WithError(err).Error("Failed to set IMAP SSL")
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_CONNECTION_MODE_CHANGE_ERROR))
}
}
if s.bridge.GetSMTPSSL() != settings.UseSSLForSmtp {
- if err := s.bridge.SetSMTPSSL(settings.UseSSLForSmtp); err != nil {
+ if err := s.bridge.SetSMTPSSL(ctx, settings.UseSSLForSmtp); err != nil {
s.log.WithError(err).Error("Failed to set SMTP SSL")
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_CONNECTION_MODE_CHANGE_ERROR))
}
}
if s.bridge.GetIMAPPort() != int(settings.ImapPort) {
- if err := s.bridge.SetIMAPPort(int(settings.ImapPort)); err != nil {
+ if err := s.bridge.SetIMAPPort(ctx, int(settings.ImapPort)); err != nil {
s.log.WithError(err).Error("Failed to set IMAP port")
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_PORT_CHANGE_ERROR))
}
}
if s.bridge.GetSMTPPort() != int(settings.SmtpPort) {
- if err := s.bridge.SetSMTPPort(int(settings.SmtpPort)); err != nil {
+ if err := s.bridge.SetSMTPPort(ctx, int(settings.SmtpPort)); err != nil {
s.log.WithError(err).Error("Failed to set SMTP port")
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_PORT_CHANGE_ERROR))
}
@@ -715,19 +715,19 @@ func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSet
return &emptypb.Empty{}, nil
}
-func (s *Service) Hostname(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) Hostname(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("Hostname")
return wrapperspb.String(constants.Host), nil
}
-func (s *Service) IsPortFree(ctx context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) {
+func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) {
s.log.Debug("IsPortFree")
return wrapperspb.Bool(ports.IsPortFree(int(port.Value))), nil
}
-func (s *Service) AvailableKeychains(ctx context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
+func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
s.log.Debug("AvailableKeychains")
return &AvailableKeychainsResponse{Keychains: maps.Keys(keychain.Helpers)}, nil
@@ -757,7 +757,7 @@ func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.S
return &emptypb.Empty{}, nil
}
-func (s *Service) CurrentKeychain(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
+func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("CurrentKeychain")
helper, err := s.bridge.GetKeychainApp()
diff --git a/internal/frontend/grpc/service_user.go b/internal/frontend/grpc/service_user.go
index 8a72c985..248acc4e 100644
--- a/internal/frontend/grpc/service_user.go
+++ b/internal/frontend/grpc/service_user.go
@@ -28,7 +28,7 @@ import (
"google.golang.org/protobuf/types/known/wrapperspb"
)
-func (s *Service) GetUserList(ctx context.Context, _ *emptypb.Empty) (*UserListResponse, error) {
+func (s *Service) GetUserList(_ context.Context, _ *emptypb.Empty) (*UserListResponse, error) {
s.log.Debug("GetUserList")
userIDs := s.bridge.GetUserIDs()
@@ -51,7 +51,7 @@ func (s *Service) GetUserList(ctx context.Context, _ *emptypb.Empty) (*UserListR
return &UserListResponse{Users: userList}, nil
}
-func (s *Service) GetUser(ctx context.Context, userID *wrapperspb.StringValue) (*User, error) {
+func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*User, error) {
s.log.WithField("userID", userID).Debug("GetUser")
user, err := s.bridge.GetUserInfo(userID.Value)
@@ -62,7 +62,7 @@ func (s *Service) GetUser(ctx context.Context, userID *wrapperspb.StringValue) (
return grpcUserFromInfo(user), nil
}
-func (s *Service) SetUserSplitMode(ctx context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) {
+func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) {
s.log.WithField("UserID", splitMode.UserID).WithField("Active", splitMode.Active).Debug("SetUserSplitMode")
user, err := s.bridge.GetUserInfo(splitMode.UserID)
@@ -96,7 +96,7 @@ func (s *Service) SetUserSplitMode(ctx context.Context, splitMode *UserSplitMode
return &emptypb.Empty{}, nil
}
-func (s *Service) SendBadEventUserFeedback(ctx context.Context, feedback *UserBadEventFeedbackRequest) (*emptypb.Empty, error) {
+func (s *Service) SendBadEventUserFeedback(_ context.Context, feedback *UserBadEventFeedbackRequest) (*emptypb.Empty, error) {
l := s.log.WithField("UserID", feedback.UserID).WithField("doResync", feedback.DoResync)
l.Debug("SendBadEventUserFeedback")
@@ -114,7 +114,7 @@ func (s *Service) SendBadEventUserFeedback(ctx context.Context, feedback *UserBa
return &emptypb.Empty{}, nil
}
-func (s *Service) LogoutUser(ctx context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
+func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("UserID", userID.Value).Debug("LogoutUser")
if _, err := s.bridge.GetUserInfo(userID.Value); err != nil {
@@ -132,7 +132,7 @@ func (s *Service) LogoutUser(ctx context.Context, userID *wrapperspb.StringValue
return &emptypb.Empty{}, nil
}
-func (s *Service) RemoveUser(ctx context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
+func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("UserID", userID.Value).Debug("RemoveUser")
go func() {
@@ -152,7 +152,7 @@ func (s *Service) ConfigureUserAppleMail(ctx context.Context, request *Configure
sslWasEnabled := s.bridge.GetSMTPSSL()
- if err := s.bridge.ConfigureAppleMail(request.UserID, request.Address); err != nil {
+ if err := s.bridge.ConfigureAppleMail(ctx, request.UserID, request.Address); err != nil {
s.log.WithField("userID", request.UserID).Error("Cannot configure AppleMail for user")
return nil, status.Error(codes.Internal, "Apple Mail config failed")
}
diff --git a/internal/logging/logging.go b/internal/logging/logging.go
index 667f29c4..483cf395 100644
--- a/internal/logging/logging.go
+++ b/internal/logging/logging.go
@@ -113,7 +113,7 @@ func Init(logsPath, level string) error {
// Debug or Trace.
func setLevel(level string) error {
if level == "" {
- return nil
+ level = "debug"
}
logLevel, err := logrus.ParseLevel(level)
diff --git a/internal/sentry/reporter.go b/internal/sentry/reporter.go
index 1ceb084c..c15832ec 100644
--- a/internal/sentry/reporter.go
+++ b/internal/sentry/reporter.go
@@ -96,6 +96,7 @@ func GetTimeZone() string {
// NewReporter creates new sentry reporter with appName and appVersion to report.
func NewReporter(appName string, identifier Identifier) *Reporter {
+ logrus.WithField("id", GetProtectedHostname()).Info("New sentry reporter")
return &Reporter{
appName: appName,
appVersion: constants.Revision,
@@ -203,7 +204,7 @@ func SkipDuringUnwind() {
}
// EnhanceSentryEvent swaps type with value and removes panic handlers from the stacktrace.
-func EnhanceSentryEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+func EnhanceSentryEvent(event *sentry.Event, _ *sentry.EventHint) *sentry.Event {
for idx, exception := range event.Exception {
exception.Type, exception.Value = exception.Value, exception.Type
if exception.Stacktrace != nil {
diff --git a/internal/updater/install_darwin.go b/internal/updater/install_darwin.go
index 45e07b58..c55420ca 100644
--- a/internal/updater/install_darwin.go
+++ b/internal/updater/install_darwin.go
@@ -62,6 +62,6 @@ func (i *InstallerDarwin) InstallUpdate(_ *semver.Version, r io.Reader) error {
return syncFolders(oldBundle, newBundle)
}
-func (i *InstallerDarwin) IsAlreadyInstalled(version *semver.Version) bool {
+func (i *InstallerDarwin) IsAlreadyInstalled(_ *semver.Version) bool {
return false
}
diff --git a/internal/user/events.go b/internal/user/events.go
index 35ee921c..b848a98e 100644
--- a/internal/user/events.go
+++ b/internal/user/events.go
@@ -217,7 +217,7 @@ func (user *User) handleCreateAddressEvent(ctx context.Context, event proton.Add
// If the address is enabled, we need to hook it up to the update channels.
switch user.vault.AddressMode() {
case vault.CombinedMode:
- primAddr, err := getAddrIdx(user.apiAddrs, 0)
+ primAddr, err := getPrimaryAddr(user.apiAddrs)
if err != nil {
return fmt.Errorf("failed to get primary address: %w", err)
}
@@ -276,7 +276,7 @@ func (user *User) handleUpdateAddressEvent(_ context.Context, event proton.Addre
case oldAddr.Status != proton.AddressStatusEnabled && event.Address.Status == proton.AddressStatusEnabled:
switch user.vault.AddressMode() {
case vault.CombinedMode:
- primAddr, err := getAddrIdx(user.apiAddrs, 0)
+ primAddr, err := getPrimaryAddr(user.apiAddrs)
if err != nil {
return fmt.Errorf("failed to get primary address: %w", err)
}
@@ -394,7 +394,7 @@ func (user *User) handleLabelEvents(ctx context.Context, labelEvents []proton.La
return nil
}
-func (user *User) handleCreateLabelEvent(ctx context.Context, event proton.LabelEvent) ([]imap.Update, error) { //nolint:unparam
+func (user *User) handleCreateLabelEvent(_ context.Context, event proton.LabelEvent) ([]imap.Update, error) { //nolint:unparam
return safe.LockRetErr(func() ([]imap.Update, error) {
var updates []imap.Update
@@ -480,7 +480,7 @@ func (user *User) handleUpdateLabelEvent(ctx context.Context, event proton.Label
}, user.apiLabelsLock, user.updateChLock)
}
-func (user *User) handleDeleteLabelEvent(ctx context.Context, event proton.LabelEvent) ([]imap.Update, error) { //nolint:unparam
+func (user *User) handleDeleteLabelEvent(_ context.Context, event proton.LabelEvent) ([]imap.Update, error) { //nolint:unparam
return safe.LockRetErr(func() ([]imap.Update, error) {
var updates []imap.Update
@@ -628,7 +628,14 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, message proton.M
}
update = imap.NewMessagesCreated(false, res.update)
- user.updateCh[full.AddressID].Enqueue(update)
+ didPublish, err := safePublishMessageUpdate(user, full.AddressID, update)
+ if err != nil {
+ return err
+ }
+
+ if !didPublish {
+ update = nil
+ }
return nil
}); err != nil {
@@ -643,7 +650,7 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, message proton.M
}, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock)
}
-func (user *User) handleUpdateMessageEvent(ctx context.Context, message proton.MessageMetadata) ([]imap.Update, error) { //nolint:unparam
+func (user *User) handleUpdateMessageEvent(_ context.Context, message proton.MessageMetadata) ([]imap.Update, error) { //nolint:unparam
return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{
"messageID": message.ID,
@@ -674,13 +681,20 @@ func (user *User) handleUpdateMessageEvent(ctx context.Context, message proton.M
flags,
)
- user.updateCh[message.AddressID].Enqueue(update)
+ didPublish, err := safePublishMessageUpdate(user, message.AddressID, update)
+ if err != nil {
+ return nil, err
+ }
+
+ if !didPublish {
+ return nil, nil
+ }
return []imap.Update{update}, nil
}, user.apiLabelsLock, user.updateChLock)
}
-func (user *User) handleDeleteMessageEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) { //nolint:unparam
+func (user *User) handleDeleteMessageEvent(_ context.Context, event proton.MessageEvent) ([]imap.Update, error) {
return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithField("messageID", event.ID).Info("Handling message deleted event")
@@ -696,7 +710,7 @@ func (user *User) handleDeleteMessageEvent(ctx context.Context, event proton.Mes
}, user.updateChLock)
}
-func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) { //nolint:unparam
+func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) {
return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{
"messageID": event.ID,
@@ -743,13 +757,24 @@ func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.Messa
true, // Is the message doesn't exist, silently create it.
)
- user.updateCh[full.AddressID].Enqueue(update)
+ didPublish, err := safePublishMessageUpdate(user, full.AddressID, update)
+ if err != nil {
+ return err
+ }
+
+ if !didPublish {
+ update = nil
+ }
return nil
}); err != nil {
return nil, err
}
+ if update == nil {
+ return nil, nil
+ }
+
return []imap.Update{update}, nil
}, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock)
}
@@ -816,3 +841,37 @@ func (user *User) reportErrorNoContextCancel(title string, err error, reportCont
}
}
}
+
+// safePublishMessageUpdate handles the rare case where the address' update channel may have been deleted in the same
+// event. This rare case can take place if in the same event fetch request there is an update for delete address and
+// create/update message.
+// If the user is in combined mode, we simply push the update to the primary address. If the user is in split mode
+// we do not publish the update as the address no longer exists.
+func safePublishMessageUpdate(user *User, addressID string, update imap.Update) (bool, error) {
+ v, ok := user.updateCh[addressID]
+ if !ok {
+ if user.GetAddressMode() == vault.CombinedMode {
+ primAddr, err := getPrimaryAddr(user.apiAddrs)
+ if err != nil {
+ return false, fmt.Errorf("failed to get primary address: %w", err)
+ }
+ primaryCh, ok := user.updateCh[primAddr.ID]
+ if !ok {
+ return false, fmt.Errorf("primary address channel is not available")
+ }
+
+ primaryCh.Enqueue(update)
+
+ return true, nil
+ }
+
+ logrus.Warnf("Update channel not found for address %v, it may have been already deleted", addressID)
+ _ = user.reporter.ReportMessage("Message Update channel does not exist")
+
+ return false, nil
+ }
+
+ v.Enqueue(update)
+
+ return true, nil
+}
diff --git a/internal/user/imap.go b/internal/user/imap.go
index 96476ae5..b6257cba 100644
--- a/internal/user/imap.go
+++ b/internal/user/imap.go
@@ -270,7 +270,7 @@ func (conn *imapConnector) CreateMessage(
mailboxID imap.MailboxID,
literal []byte,
flags imap.FlagSet,
- date time.Time,
+ _ time.Time,
) (imap.Message, []byte, error) {
defer conn.goPollAPIEvents(false)
@@ -459,11 +459,11 @@ func (conn *imapConnector) MoveMessages(ctx context.Context, messageIDs []imap.M
var result bool
if v, ok := conn.apiLabels[string(labelFromID)]; ok && v.Type == proton.LabelTypeLabel {
- result = result || true
+ result = true
}
if v, ok := conn.apiLabels[string(labelToID)]; ok && (v.Type == proton.LabelTypeFolder || v.Type == proton.LabelTypeSystem) {
- result = result || true
+ result = true
}
return result
@@ -529,7 +529,7 @@ func (conn *imapConnector) GetMailboxVisibility(_ context.Context, mailboxID ima
}
// Close the connector will no longer be used and all resources should be closed/released.
-func (conn *imapConnector) Close(ctx context.Context) error {
+func (conn *imapConnector) Close(_ context.Context) error {
return nil
}
@@ -544,7 +544,7 @@ func (conn *imapConnector) importMessage(
if err := safe.RLockRet(func() error {
return withAddrKR(conn.apiUser, conn.apiAddrs[conn.addrID], conn.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error {
- messageID := ""
+ var messageID string
if slices.Contains(labelIDs, proton.DraftsLabel) {
msg, err := conn.createDraft(ctx, literal, addrKR, conn.apiAddrs[conn.addrID])
diff --git a/internal/user/send_recorder.go b/internal/user/send_recorder.go
index fae307e0..306f1384 100644
--- a/internal/user/send_recorder.go
+++ b/internal/user/send_recorder.go
@@ -25,6 +25,7 @@ import (
"time"
"github.com/ProtonMail/gluon/rfc822"
+ "github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
@@ -34,22 +35,30 @@ const sendEntryExpiry = 30 * time.Minute
type sendRecorder struct {
expiry time.Duration
- entries map[string]*sendEntry
+ entries map[string][]*sendEntry
entriesLock sync.Mutex
}
func newSendRecorder(expiry time.Duration) *sendRecorder {
return &sendRecorder{
expiry: expiry,
- entries: make(map[string]*sendEntry),
+ entries: make(map[string][]*sendEntry),
}
}
type sendEntry struct {
- msgID string
- toList []string
- exp time.Time
- waitCh chan struct{}
+ msgID string
+ toList []string
+ exp time.Time
+ waitCh chan struct{}
+ waitChClosed bool
+}
+
+func (s *sendEntry) closeWaitChannel() {
+ if !s.waitChClosed {
+ close(s.waitCh)
+ s.waitChClosed = true
+ }
}
// tryInsertWait tries to insert the given message into the send recorder.
@@ -102,25 +111,40 @@ func (h *sendRecorder) hasEntryWait(ctx context.Context, hash string, deadline t
return h.hasEntryWait(ctx, hash, deadline)
}
+func (h *sendRecorder) removeExpiredUnsafe() {
+ for hash, entry := range h.entries {
+ remaining := xslices.Filter(entry, func(t *sendEntry) bool {
+ return !t.exp.Before(time.Now())
+ })
+
+ if len(remaining) == 0 {
+ delete(h.entries, hash)
+ } else {
+ h.entries[hash] = remaining
+ }
+ }
+}
+
func (h *sendRecorder) tryInsert(hash string, toList []string) bool {
h.entriesLock.Lock()
defer h.entriesLock.Unlock()
- for hash, entry := range h.entries {
- if entry.exp.Before(time.Now()) {
- delete(h.entries, hash)
+ h.removeExpiredUnsafe()
+
+ entries, ok := h.entries[hash]
+ if ok {
+ for _, entry := range entries {
+ if matchToList(entry.toList, toList) {
+ return false
+ }
}
}
- if _, ok := h.entries[hash]; ok && matchToList(h.entries[hash].toList, toList) {
- return false
- }
-
- h.entries[hash] = &sendEntry{
+ h.entries[hash] = append(entries, &sendEntry{
exp: time.Now().Add(h.expiry),
toList: toList,
waitCh: make(chan struct{}),
- }
+ })
return true
}
@@ -129,11 +153,7 @@ func (h *sendRecorder) hasEntry(hash string) bool {
h.entriesLock.Lock()
defer h.entriesLock.Unlock()
- for hash, entry := range h.entries {
- if entry.exp.Before(time.Now()) {
- delete(h.entries, hash)
- }
- }
+ h.removeExpiredUnsafe()
if _, ok := h.entries[hash]; ok {
return true
@@ -142,32 +162,46 @@ func (h *sendRecorder) hasEntry(hash string) bool {
return false
}
-func (h *sendRecorder) addMessageID(hash, msgID string) {
+// signalMessageSent should be called after a message has been successfully sent.
+func (h *sendRecorder) signalMessageSent(hash, msgID string, toList []string) {
h.entriesLock.Lock()
defer h.entriesLock.Unlock()
- entry, ok := h.entries[hash]
+ entries, ok := h.entries[hash]
if ok {
- entry.msgID = msgID
- } else {
- logrus.Warn("Cannot add message ID to send hash entry, it may have expired")
+ for _, entry := range entries {
+ if matchToList(entry.toList, toList) {
+ entry.msgID = msgID
+ entry.closeWaitChannel()
+ return
+ }
+ }
}
- close(entry.waitCh)
+ logrus.Warn("Cannot add message ID to send hash entry, it may have expired")
}
-func (h *sendRecorder) removeOnFail(hash string) {
+func (h *sendRecorder) removeOnFail(hash string, toList []string) {
h.entriesLock.Lock()
defer h.entriesLock.Unlock()
- entry, ok := h.entries[hash]
- if !ok || entry.msgID != "" {
+ entries, ok := h.entries[hash]
+ if !ok {
return
}
- close(entry.waitCh)
+ for idx, entry := range entries {
+ if entry.msgID == "" && matchToList(entry.toList, toList) {
+ entry.closeWaitChannel()
- delete(h.entries, hash)
+ remaining := xslices.Remove(entries, idx, 1)
+ if len(remaining) != 0 {
+ h.entries[hash] = remaining
+ } else {
+ delete(h.entries, hash)
+ }
+ }
+ }
}
func (h *sendRecorder) wait(ctx context.Context, hash string, deadline time.Time) (string, bool, error) {
@@ -191,7 +225,7 @@ func (h *sendRecorder) wait(ctx context.Context, hash string, deadline time.Time
defer h.entriesLock.Unlock()
if entry, ok := h.entries[hash]; ok {
- return entry.msgID, true, nil
+ return entry[0].msgID, true, nil
}
return "", false, nil
@@ -202,7 +236,7 @@ func (h *sendRecorder) getWaitCh(hash string) (<-chan struct{}, bool) {
defer h.entriesLock.Unlock()
if entry, ok := h.entries[hash]; ok {
- return entry.waitCh, true
+ return entry[0].waitCh, true
}
return nil, false
diff --git a/internal/user/send_recorder_test.go b/internal/user/send_recorder_test.go
index 075a3b52..942df28e 100644
--- a/internal/user/send_recorder_test.go
+++ b/internal/user/send_recorder_test.go
@@ -35,7 +35,7 @@ func TestSendHasher_Insert(t *testing.T) {
require.NotEmpty(t, hash1)
// Simulate successfully sending the message.
- h.addMessageID(hash1, "abc")
+ h.signalMessageSent(hash1, "abc", nil)
// Inserting a message with the same hash should return false.
_, ok, err = testTryInsert(h, literal1, time.Now().Add(time.Second))
@@ -59,7 +59,7 @@ func TestSendHasher_Insert_Expired(t *testing.T) {
require.NotEmpty(t, hash1)
// Simulate successfully sending the message.
- h.addMessageID(hash1, "abc")
+ h.signalMessageSent(hash1, "abc", nil)
// Wait for the entry to expire.
time.Sleep(time.Second)
@@ -106,7 +106,7 @@ func TestSendHasher_Wait_SendSuccess(t *testing.T) {
// Simulate successfully sending the message after half a second.
go func() {
time.Sleep(time.Millisecond * 500)
- h.addMessageID(hash, "abc")
+ h.signalMessageSent(hash, "abc", nil)
}()
// Inserting a message with the same hash should fail.
@@ -127,7 +127,7 @@ func TestSendHasher_Wait_SendFail(t *testing.T) {
// Simulate failing to send the message after half a second.
go func() {
time.Sleep(time.Millisecond * 500)
- h.removeOnFail(hash)
+ h.removeOnFail(hash, nil)
}()
// Inserting a message with the same hash should succeed because the first message failed to send.
@@ -163,7 +163,7 @@ func TestSendHasher_HasEntry(t *testing.T) {
require.NotEmpty(t, hash)
// Simulate successfully sending the message.
- h.addMessageID(hash, "abc")
+ h.signalMessageSent(hash, "abc", nil)
// The message was already sent; we should find it in the hasher.
messageID, ok, err := testHasEntry(h, literal1, time.Now().Add(time.Second))
@@ -184,7 +184,7 @@ func TestSendHasher_HasEntry_SendSuccess(t *testing.T) {
// Simulate successfully sending the message after half a second.
go func() {
time.Sleep(time.Millisecond * 500)
- h.addMessageID(hash, "abc")
+ h.signalMessageSent(hash, "abc", nil)
}()
// The message was already sent; we should find it in the hasher.
@@ -194,6 +194,47 @@ func TestSendHasher_HasEntry_SendSuccess(t *testing.T) {
require.Equal(t, "abc", messageID)
}
+func TestSendHasher_DualAddDoesNotCauseCrash(t *testing.T) {
+ // There may be a rare case where one 2 smtp connections attempt to send the same message, but if the first message
+ // is stuck long enough for it to expire, the second connection will remove it from the list and cause it to be
+ // inserted as a new entry. The two clients end up sending the message twice and calling the `signalMessageSent` x2,
+ // resulting in a crash.
+ h := newSendRecorder(sendEntryExpiry)
+
+ // Insert a message into the hasher.
+ hash, ok, err := testTryInsert(h, literal1, time.Now().Add(time.Second))
+ require.NoError(t, err)
+ require.True(t, ok)
+ require.NotEmpty(t, hash)
+
+ // Simulate successfully sending the message. We call this method twice as it possible for multiple SMTP connections
+ // to attempt to send the same message.
+ h.signalMessageSent(hash, "abc", nil)
+ h.signalMessageSent(hash, "abc", nil)
+
+ // The message was already sent; we should find it in the hasher.
+ messageID, ok, err := testHasEntry(h, literal1, time.Now().Add(time.Second))
+ require.NoError(t, err)
+ require.True(t, ok)
+ require.Equal(t, "abc", messageID)
+}
+
+func TestSendHashed_MessageWithSameHasButDifferentRecipientsIsInserted(t *testing.T) {
+ h := newSendRecorder(sendEntryExpiry)
+
+ // Insert a message into the hasher.
+ hash, ok, err := testTryInsert(h, literal1, time.Now().Add(time.Second), "Receiver ")
+ require.NoError(t, err)
+ require.True(t, ok)
+ require.NotEmpty(t, hash)
+
+ hash2, ok, err := testTryInsert(h, literal1, time.Now().Add(time.Second), "Receiver ", "Receiver2 ")
+ require.NoError(t, err)
+ require.True(t, ok)
+ require.NotEmpty(t, hash2)
+ require.Equal(t, hash, hash2)
+}
+
func TestSendHasher_HasEntry_SendFail(t *testing.T) {
h := newSendRecorder(sendEntryExpiry)
@@ -206,7 +247,7 @@ func TestSendHasher_HasEntry_SendFail(t *testing.T) {
// Simulate failing to send the message after half a second.
go func() {
time.Sleep(time.Millisecond * 500)
- h.removeOnFail(hash)
+ h.removeOnFail(hash, nil)
}()
// The message failed to send; we should not find it in the hasher.
@@ -240,7 +281,7 @@ func TestSendHasher_HasEntry_Expired(t *testing.T) {
require.NotEmpty(t, hash)
// Simulate successfully sending the message.
- h.addMessageID(hash, "abc")
+ h.signalMessageSent(hash, "abc", nil)
// Wait for the entry to expire.
time.Sleep(time.Second)
@@ -264,7 +305,6 @@ Content-Disposition: attachment; filename="attname.txt"
attachment
--longrandomstring--
`
-
const literal2 = `From: Sender
To: Receiver
Content-Type: multipart/mixed; boundary=longrandomstring
diff --git a/internal/user/smtp.go b/internal/user/smtp.go
index f5f6d2b8..58148e72 100644
--- a/internal/user/smtp.go
+++ b/internal/user/smtp.go
@@ -89,7 +89,7 @@ func (user *User) sendMail(authID string, from string, to []string, r io.Reader)
}
// If we fail to send this message, we should remove the hash from the send recorder.
- defer user.sendHash.removeOnFail(hash)
+ defer user.sendHash.removeOnFail(hash, to)
// Create a new message parser from the reader.
parser, err := parser.New(bytes.NewReader(b))
@@ -162,7 +162,7 @@ func (user *User) sendMail(authID string, from string, to []string, r io.Reader)
}
// If the message was successfully sent, we can update the message ID in the record.
- user.sendHash.addMessageID(hash, sent.ID)
+ user.sendHash.signalMessageSent(hash, sent.ID, to)
return nil
})
@@ -438,6 +438,10 @@ func (user *User) createAttachments(
}
}
+ // Exclude name from params since this is already provided using Filename.
+ delete(att.MIMEParams, "name")
+ delete(att.MIMEParams, "filename")
+
attachment, err := client.UploadAttachment(ctx, addrKR, proton.CreateAttachmentReq{
Filename: att.Name,
MessageID: draftID,
diff --git a/internal/user/smtp_default.go b/internal/user/smtp_default.go
index 0f048d10..11bb8ded 100644
--- a/internal/user/smtp_default.go
+++ b/internal/user/smtp_default.go
@@ -19,6 +19,6 @@
package user
-func debugDumpToDisk(b []byte) error {
+func debugDumpToDisk(_ []byte) error {
return nil
}
diff --git a/internal/user/sync.go b/internal/user/sync.go
index 16f179a0..677d0ffc 100644
--- a/internal/user/sync.go
+++ b/internal/user/sync.go
@@ -513,7 +513,16 @@ func (user *User) syncMessages(
result, err := parallel.MapContext(ctx, maxMessagesInParallel, chunk, func(ctx context.Context, msg proton.FullMessage) (*buildRes, error) {
defer async.HandlePanic(user.panicHandler)
- return buildRFC822(apiLabels, msg, addrKRs[msg.AddressID], new(bytes.Buffer)), nil
+ kr, ok := addrKRs[msg.AddressID]
+ if !ok {
+ return &buildRes{
+ messageID: msg.ID,
+ addressID: msg.AddressID,
+ err: fmt.Errorf("address does not have an unlocked keyring"),
+ }, nil
+ }
+
+ return buildRFC822(apiLabels, msg, kr, new(bytes.Buffer)), nil
})
if err != nil {
return
@@ -572,10 +581,10 @@ func (user *User) syncMessages(
// We could sync a placeholder message here, but for now we skip it entirely.
continue
- } else {
- if err := vault.RemFailedMessageID(res.messageID); err != nil {
- logrus.WithError(err).Error("Failed to remove failed message ID")
- }
+ }
+
+ if err := vault.RemFailedMessageID(res.messageID); err != nil {
+ logrus.WithError(err).Error("Failed to remove failed message ID")
}
targetInfo := addressToIndex[res.addressID]
diff --git a/internal/user/types.go b/internal/user/types.go
index fc6a2286..67fbdf04 100644
--- a/internal/user/types.go
+++ b/internal/user/types.go
@@ -83,6 +83,18 @@ func getAddrIdx(apiAddrs map[string]proton.Address, idx int) (proton.Address, er
return sorted[idx], nil
}
+func getPrimaryAddr(apiAddrs map[string]proton.Address) (proton.Address, error) {
+ sorted := sortSlice(maps.Values(apiAddrs), func(a, b proton.Address) bool {
+ return a.Order < b.Order
+ })
+
+ if len(sorted) == 0 {
+ return proton.Address{}, fmt.Errorf("no addresses available")
+ }
+
+ return sorted[0], nil
+}
+
// sortSlice returns the given slice sorted by the given comparator.
func sortSlice[Item any](items []Item, less func(Item, Item) bool) []Item {
sorted := make([]Item, len(items))
diff --git a/internal/user/user.go b/internal/user/user.go
index d8418330..ec010ba3 100644
--- a/internal/user/user.go
+++ b/internal/user/user.go
@@ -282,7 +282,7 @@ func (user *User) Match(query string) bool {
func (user *User) Emails() []string {
return safe.RLockRet(func() []string {
addresses := xslices.Filter(maps.Values(user.apiAddrs), func(addr proton.Address) bool {
- return addr.Status == proton.AddressStatusEnabled
+ return addr.Status == proton.AddressStatusEnabled && addr.Type != proton.AddressTypeExternal
})
slices.SortFunc(addresses, func(a, b proton.Address) bool {
@@ -586,6 +586,8 @@ func (user *User) Close() {
for _, updateCh := range xslices.Unique(maps.Values(user.updateCh)) {
updateCh.CloseAndDiscardQueued()
}
+
+ user.updateCh = make(map[string]*async.QueuedChannel[imap.Update])
}, user.updateChLock)
// Close the user's notify channel.
@@ -690,87 +692,89 @@ func (user *User) doEventPoll(ctx context.Context) error {
user.eventLock.Lock()
defer user.eventLock.Unlock()
- event, more, err := user.client.GetEvent(ctx, user.vault.EventID())
+ gpaEvents, more, err := user.client.GetEvent(ctx, user.vault.EventID())
if err != nil {
return fmt.Errorf("failed to get event (caused by %T): %w", internal.ErrCause(err), err)
}
// If the event ID hasn't changed, there are no new events.
- if event.EventID == user.vault.EventID() {
+ if gpaEvents[len(gpaEvents)-1].EventID == user.vault.EventID() {
user.log.Debug("No new API events")
return nil
}
- user.log.WithFields(logrus.Fields{
- "old": user.vault.EventID(),
- "new": event,
- }).Info("Received new API event")
+ for _, event := range gpaEvents {
+ user.log.WithFields(logrus.Fields{
+ "old": user.vault.EventID(),
+ "new": event,
+ }).Info("Received new API event")
- // Handle the event.
- if err := user.handleAPIEvent(ctx, event); err != nil {
- // If the error is a context cancellation, return error to retry later.
- if errors.Is(err, context.Canceled) {
- return fmt.Errorf("failed to handle event due to context cancellation: %w", err)
+ // Handle the event.
+ if err := user.handleAPIEvent(ctx, event); err != nil {
+ // If the error is a context cancellation, return error to retry later.
+ if errors.Is(err, context.Canceled) {
+ return fmt.Errorf("failed to handle event due to context cancellation: %w", err)
+ }
+
+ // If the error is a network error, return error to retry later.
+ if netErr := new(proton.NetError); errors.As(err, &netErr) {
+ return fmt.Errorf("failed to handle event due to network issue: %w", err)
+ }
+
+ // Catch all for uncategorized net errors that may slip through.
+ if netErr := new(net.OpError); errors.As(err, &netErr) {
+ return fmt.Errorf("failed to handle event due to network issues (uncategorized): %w", err)
+ }
+
+ // In case a json decode error slips through.
+ if jsonErr := new(json.UnmarshalTypeError); errors.As(err, &jsonErr) {
+ user.eventCh.Enqueue(events.UncategorizedEventError{
+ UserID: user.ID(),
+ Error: err,
+ })
+
+ return fmt.Errorf("failed to handle event due to JSON issue: %w", err)
+ }
+
+ // If the error is an unexpected EOF, return error to retry later.
+ if errors.Is(err, io.ErrUnexpectedEOF) {
+ return fmt.Errorf("failed to handle event due to EOF: %w", err)
+ }
+
+ // If the error is a server-side issue, return error to retry later.
+ if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status >= 500 {
+ return fmt.Errorf("failed to handle event due to server error: %w", err)
+ }
+
+ // Otherwise, the error is a client-side issue; notify bridge to handle it.
+ user.log.WithField("event", event).Warn("Failed to handle API event")
+
+ user.eventCh.Enqueue(events.UserBadEvent{
+ UserID: user.ID(),
+ OldEventID: user.vault.EventID(),
+ NewEventID: event.EventID,
+ EventInfo: event.String(),
+ Error: err,
+ })
+
+ return fmt.Errorf("failed to handle event due to client error: %w", err)
}
- // If the error is a network error, return error to retry later.
- if netErr := new(proton.NetError); errors.As(err, &netErr) {
- return fmt.Errorf("failed to handle event due to network issue: %w", err)
- }
+ user.log.WithField("event", event).Debug("Handled API event")
- // Catch all for uncategorized net errors that may slip through.
- if netErr := new(net.OpError); errors.As(err, &netErr) {
- return fmt.Errorf("failed to handle event due to network issues (uncategorized): %w", err)
- }
-
- // In case a json decode error slips through.
- if jsonErr := new(json.UnmarshalTypeError); errors.As(err, &jsonErr) {
- user.eventCh.Enqueue(events.UncategorizedEventError{
+ // Update the event ID in the vault. If this fails, notify bridge to handle it.
+ if err := user.vault.SetEventID(event.EventID); err != nil {
+ user.eventCh.Enqueue(events.UserBadEvent{
UserID: user.ID(),
Error: err,
})
- return fmt.Errorf("failed to handle event due to JSON issue: %w", err)
+ return fmt.Errorf("failed to update event ID: %w", err)
}
- // If the error is an unexpected EOF, return error to retry later.
- if errors.Is(err, io.ErrUnexpectedEOF) {
- return fmt.Errorf("failed to handle event due to EOF: %w", err)
- }
-
- // If the error is a server-side issue, return error to retry later.
- if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status >= 500 {
- return fmt.Errorf("failed to handle event due to server error: %w", err)
- }
-
- // Otherwise, the error is a client-side issue; notify bridge to handle it.
- user.log.WithField("event", event).Warn("Failed to handle API event")
-
- user.eventCh.Enqueue(events.UserBadEvent{
- UserID: user.ID(),
- OldEventID: user.vault.EventID(),
- NewEventID: event.EventID,
- EventInfo: event.String(),
- Error: err,
- })
-
- return fmt.Errorf("failed to handle event due to client error: %w", err)
+ user.log.WithField("eventID", event.EventID).Debug("Updated event ID in vault")
}
- user.log.WithField("event", event).Debug("Handled API event")
-
- // Update the event ID in the vault. If this fails, notify bridge to handle it.
- if err := user.vault.SetEventID(event.EventID); err != nil {
- user.eventCh.Enqueue(events.UserBadEvent{
- UserID: user.ID(),
- Error: err,
- })
-
- return fmt.Errorf("failed to update event ID: %w", err)
- }
-
- user.log.WithField("eventID", event.EventID).Debug("Updated event ID in vault")
-
if more {
user.goPollAPIEvents(false)
}
diff --git a/internal/vault/certs.go b/internal/vault/certs.go
index 24be5f38..ee48e203 100644
--- a/internal/vault/certs.go
+++ b/internal/vault/certs.go
@@ -30,7 +30,12 @@ import (
// If CertPEMPath is set, it will attempt to read the certificate from the file.
// Otherwise, or on read/validation failure, it will return the certificate from the vault.
func (vault *Vault) GetBridgeTLSCert() ([]byte, []byte) {
- if certPath, keyPath := vault.get().Certs.CustomCertPath, vault.get().Certs.CustomKeyPath; certPath != "" && keyPath != "" {
+ vault.lock.RLock()
+ defer vault.lock.RUnlock()
+
+ certs := vault.getUnsafe().Certs
+
+ if certPath, keyPath := certs.CustomCertPath, certs.CustomKeyPath; certPath != "" && keyPath != "" {
if certPEM, keyPEM, err := readPEMCert(certPath, keyPath); err == nil {
return certPEM, keyPEM
}
@@ -38,7 +43,7 @@ func (vault *Vault) GetBridgeTLSCert() ([]byte, []byte) {
logrus.Error("Failed to read certificate from file, using default")
}
- return vault.get().Certs.Bridge.Cert, vault.get().Certs.Bridge.Key
+ return certs.Bridge.Cert, certs.Bridge.Key
}
// SetBridgeTLSCertPath sets the path to PEM-encoded certificates for the bridge.
@@ -47,7 +52,7 @@ func (vault *Vault) SetBridgeTLSCertPath(certPath, keyPath string) error {
return fmt.Errorf("invalid certificate: %w", err)
}
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Certs.CustomCertPath = certPath
data.Certs.CustomKeyPath = keyPath
})
@@ -55,18 +60,18 @@ func (vault *Vault) SetBridgeTLSCertPath(certPath, keyPath string) error {
// SetBridgeTLSCertKey sets the path to PEM-encoded certificates for the bridge.
func (vault *Vault) SetBridgeTLSCertKey(cert, key []byte) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Certs.Bridge.Cert = cert
data.Certs.Bridge.Key = key
})
}
func (vault *Vault) GetCertsInstalled() bool {
- return vault.get().Certs.Installed
+ return vault.getSafe().Certs.Installed
}
func (vault *Vault) SetCertsInstalled(installed bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Certs.Installed = installed
})
}
diff --git a/internal/vault/cookies.go b/internal/vault/cookies.go
index afbe6a57..96ccda66 100644
--- a/internal/vault/cookies.go
+++ b/internal/vault/cookies.go
@@ -18,11 +18,11 @@
package vault
func (vault *Vault) GetCookies() ([]byte, error) {
- return vault.get().Cookies, nil
+ return vault.getSafe().Cookies, nil
}
func (vault *Vault) SetCookies(cookies []byte) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Cookies = cookies
})
}
diff --git a/internal/vault/password_archive.go b/internal/vault/password_archive.go
new file mode 100644
index 00000000..cf471adb
--- /dev/null
+++ b/internal/vault/password_archive.go
@@ -0,0 +1,46 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Proton Mail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Proton Mail Bridge. If not, see .
+
+package vault
+
+import (
+ "crypto/sha256"
+ "fmt"
+)
+
+// set archives the password for an email address, overwriting any existing archived value.
+func (p *PasswordArchive) set(emailAddress string, password []byte) {
+ if p.Archive == nil {
+ p.Archive = make(map[string][]byte)
+ }
+
+ p.Archive[emailHashString(emailAddress)] = password
+}
+
+// get retrieves the archived password for an email address, or nil if not found.
+func (p *PasswordArchive) get(emailAddress string) []byte {
+ if p.Archive == nil {
+ return nil
+ }
+
+ return p.Archive[emailHashString(emailAddress)]
+}
+
+// emailHashString returns a hash string for an email address as a hexadecimal string.
+func emailHashString(emailAddress string) string {
+ return fmt.Sprintf("%x", sha256.Sum256([]byte(emailAddress)))
+}
diff --git a/internal/vault/settings.go b/internal/vault/settings.go
index f625825e..9a7815b3 100644
--- a/internal/vault/settings.go
+++ b/internal/vault/settings.go
@@ -33,72 +33,72 @@ const (
// GetIMAPPort sets the port that the IMAP server should listen on.
func (vault *Vault) GetIMAPPort() int {
- return vault.get().Settings.IMAPPort
+ return vault.getSafe().Settings.IMAPPort
}
// SetIMAPPort sets the port that the IMAP server should listen on.
func (vault *Vault) SetIMAPPort(port int) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.IMAPPort = port
})
}
// GetSMTPPort sets the port that the SMTP server should listen on.
func (vault *Vault) GetSMTPPort() int {
- return vault.get().Settings.SMTPPort
+ return vault.getSafe().Settings.SMTPPort
}
// SetSMTPPort sets the port that the SMTP server should listen on.
func (vault *Vault) SetSMTPPort(port int) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.SMTPPort = port
})
}
// GetIMAPSSL sets whether the IMAP server should use SSL.
func (vault *Vault) GetIMAPSSL() bool {
- return vault.get().Settings.IMAPSSL
+ return vault.getSafe().Settings.IMAPSSL
}
// SetIMAPSSL sets whether the IMAP server should use SSL.
func (vault *Vault) SetIMAPSSL(ssl bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.IMAPSSL = ssl
})
}
// GetSMTPSSL sets whether the SMTP server should use SSL.
func (vault *Vault) GetSMTPSSL() bool {
- return vault.get().Settings.SMTPSSL
+ return vault.getSafe().Settings.SMTPSSL
}
// SetSMTPSSL sets whether the SMTP server should use SSL.
func (vault *Vault) SetSMTPSSL(ssl bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.SMTPSSL = ssl
})
}
// GetGluonCacheDir sets the directory where the gluon should store its data.
func (vault *Vault) GetGluonCacheDir() string {
- return vault.get().Settings.GluonDir
+ return vault.getSafe().Settings.GluonDir
}
// SetGluonDir sets the directory where the gluon should store its data.
func (vault *Vault) SetGluonDir(dir string) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.GluonDir = dir
})
}
// GetUpdateChannel sets the update channel.
func (vault *Vault) GetUpdateChannel() updater.Channel {
- return vault.get().Settings.UpdateChannel
+ return vault.getSafe().Settings.UpdateChannel
}
// SetUpdateChannel sets the update channel.
func (vault *Vault) SetUpdateChannel(channel updater.Channel) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.UpdateChannel = channel
})
}
@@ -106,7 +106,7 @@ func (vault *Vault) SetUpdateChannel(channel updater.Channel) error {
// GetUpdateRollout sets the update rollout.
func (vault *Vault) GetUpdateRollout() float64 {
// The rollout value 0.6046602879796196 is forbidden. The RNG was not seeded when it was picked (GODT-2319).
- rollout := vault.get().Settings.UpdateRollout
+ rollout := vault.getSafe().Settings.UpdateRollout
if math.Abs(rollout-ForbiddenRollout) >= 0.00000001 {
return rollout
}
@@ -120,110 +120,110 @@ func (vault *Vault) GetUpdateRollout() float64 {
// SetUpdateRollout sets the update rollout.
func (vault *Vault) SetUpdateRollout(rollout float64) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.UpdateRollout = rollout
})
}
// GetColorScheme sets the color scheme to be used by the bridge GUI.
func (vault *Vault) GetColorScheme() string {
- return vault.get().Settings.ColorScheme
+ return vault.getSafe().Settings.ColorScheme
}
// SetColorScheme sets the color scheme to be used by the bridge GUI.
func (vault *Vault) SetColorScheme(colorScheme string) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.ColorScheme = colorScheme
})
}
// GetProxyAllowed sets whether the bridge is allowed to use alternative routing.
func (vault *Vault) GetProxyAllowed() bool {
- return vault.get().Settings.ProxyAllowed
+ return vault.getSafe().Settings.ProxyAllowed
}
// SetProxyAllowed sets whether the bridge is allowed to use alternative routing.
func (vault *Vault) SetProxyAllowed(allowed bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.ProxyAllowed = allowed
})
}
// GetShowAllMail sets whether the bridge should show the All Mail folder.
func (vault *Vault) GetShowAllMail() bool {
- return vault.get().Settings.ShowAllMail
+ return vault.getSafe().Settings.ShowAllMail
}
// SetShowAllMail sets whether the bridge should show the All Mail folder.
func (vault *Vault) SetShowAllMail(showAllMail bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.ShowAllMail = showAllMail
})
}
// GetAutostart sets whether the bridge should autostart.
func (vault *Vault) GetAutostart() bool {
- return vault.get().Settings.Autostart
+ return vault.getSafe().Settings.Autostart
}
// SetAutostart sets whether the bridge should autostart.
func (vault *Vault) SetAutostart(autostart bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.Autostart = autostart
})
}
// GetAutoUpdate sets whether the bridge should automatically update.
func (vault *Vault) GetAutoUpdate() bool {
- return vault.get().Settings.AutoUpdate
+ return vault.getSafe().Settings.AutoUpdate
}
// SetAutoUpdate sets whether the bridge should automatically update.
func (vault *Vault) SetAutoUpdate(autoUpdate bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.AutoUpdate = autoUpdate
})
}
// GetTelemetryDisabled checks whether telemetry is disabled.
func (vault *Vault) GetTelemetryDisabled() bool {
- return vault.get().Settings.TelemetryDisabled
+ return vault.getSafe().Settings.TelemetryDisabled
}
// SetTelemetryDisabled sets whether telemetry is disabled.
func (vault *Vault) SetTelemetryDisabled(telemetryDisabled bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.TelemetryDisabled = telemetryDisabled
})
}
// GetLastVersion returns the last version of the bridge that was run.
func (vault *Vault) GetLastVersion() *semver.Version {
- return semver.MustParse(vault.get().Settings.LastVersion)
+ return semver.MustParse(vault.getSafe().Settings.LastVersion)
}
// SetLastVersion sets the last version of the bridge that was run.
func (vault *Vault) SetLastVersion(version *semver.Version) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.LastVersion = version.String()
})
}
// GetFirstStart returns whether this is the first time the bridge has been started.
func (vault *Vault) GetFirstStart() bool {
- return vault.get().Settings.FirstStart
+ return vault.getSafe().Settings.FirstStart
}
// SetFirstStart sets whether this is the first time the bridge has been started.
func (vault *Vault) SetFirstStart(firstStart bool) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.FirstStart = firstStart
})
}
// GetMaxSyncMemory returns the maximum amount of memory the sync process should use.
func (vault *Vault) GetMaxSyncMemory() uint64 {
- v := vault.get().Settings.MaxSyncMemory
+ v := vault.getSafe().Settings.MaxSyncMemory
// can be zero if never written to vault before.
if v == 0 {
return DefaultMaxSyncMemory
@@ -234,14 +234,14 @@ func (vault *Vault) GetMaxSyncMemory() uint64 {
// SetMaxSyncMemory sets the maximum amount of memory the sync process should use.
func (vault *Vault) SetMaxSyncMemory(maxMemory uint64) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.MaxSyncMemory = maxMemory
})
}
// GetLastUserAgent returns the last user agent recorded by bridge.
func (vault *Vault) GetLastUserAgent() string {
- v := vault.get().Settings.LastUserAgent
+ v := vault.getSafe().Settings.LastUserAgent
// Handle case where there may be no value.
if len(v) == 0 {
@@ -253,19 +253,19 @@ func (vault *Vault) GetLastUserAgent() string {
// SetLastUserAgent store the last user agent recorded by bridge.
func (vault *Vault) SetLastUserAgent(userAgent string) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.LastUserAgent = userAgent
})
}
// GetLastHeartbeatSent returns the last time heartbeat was sent.
func (vault *Vault) GetLastHeartbeatSent() time.Time {
- return vault.get().Settings.LastHeartbeatSent
+ return vault.getSafe().Settings.LastHeartbeatSent
}
// SetLastHeartbeatSent store the last time heartbeat was sent.
func (vault *Vault) SetLastHeartbeatSent(timestamp time.Time) error {
- return vault.mod(func(data *Data) {
+ return vault.modSafe(func(data *Data) {
data.Settings.LastHeartbeatSent = timestamp
})
}
diff --git a/internal/vault/settings_test.go b/internal/vault/settings_test.go
index 3996b9ae..37efda0f 100644
--- a/internal/vault/settings_test.go
+++ b/internal/vault/settings_test.go
@@ -238,3 +238,30 @@ func TestVault_Settings_LastUserAgent(t *testing.T) {
// Check the default first start value.
require.Equal(t, vault.DefaultUserAgent, s.GetLastUserAgent())
}
+
+func Test_Settings_PasswordArchive(t *testing.T) {
+ // Create a new test vault.
+ s := newVault(t)
+
+ // The store should have no users.
+ require.Empty(t, s.GetUserIDs())
+
+ // Create a new user.
+ user, err := s.AddUser("userID1", "username1", "username1@pm.me", "authUID1", "authRef1", []byte("keyPass1"))
+ require.NoError(t, err)
+ bridgePass := user.BridgePass()
+
+ // Remove the user.
+ require.NoError(t, user.Close())
+ require.NoError(t, s.DeleteUser("userID1"))
+
+ // Add a different user. Another password is generated.
+ user, err = s.AddUser("userID2", "username2", "username2@pm.me", "authUID2", "authRef2", []byte("keyPass2"))
+ require.NoError(t, err)
+ require.NotEqual(t, user.BridgePass(), bridgePass)
+
+ // Add the first user again. The password is restored.
+ user, err = s.AddUser("userID1", "username1", "username1@pm.me", "authUID1", "authRef1", []byte("keyPass1"))
+ require.NoError(t, err)
+ require.Equal(t, user.BridgePass(), bridgePass)
+}
diff --git a/internal/vault/types_file.go b/internal/vault/types_file.go
index 6781114d..99da52e8 100644
--- a/internal/vault/types_file.go
+++ b/internal/vault/types_file.go
@@ -48,11 +48,7 @@ func unmarshalFile[T any](gcm cipher.AEAD, b []byte, data *T) error {
}
}
- if err := msgpack.Unmarshal(dec, data); err != nil {
- return err
- }
-
- return nil
+ return msgpack.Unmarshal(dec, data)
}
func marshalFile[T any](gcm cipher.AEAD, t T) ([]byte, error) {
diff --git a/internal/vault/types_password_archive.go b/internal/vault/types_password_archive.go
new file mode 100644
index 00000000..715727f4
--- /dev/null
+++ b/internal/vault/types_password_archive.go
@@ -0,0 +1,25 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Proton Mail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Proton Mail Bridge. If not, see .
+
+package vault
+
+// PasswordArchive maps a list email address hashes to passwords.
+// The type is not defined as a map alias to prevent having to handle nil default values when vault was created by an older version of the application.
+type PasswordArchive struct {
+ // we store the SHA-256 sum as string for readability and JSON marshalling of map[[32]byte][]byte will not be allowed, thus breaking vault-editor.
+ Archive map[string][]byte
+}
diff --git a/internal/vault/types_settings.go b/internal/vault/types_settings.go
index d6961673..158791ea 100644
--- a/internal/vault/types_settings.go
+++ b/internal/vault/types_settings.go
@@ -53,6 +53,8 @@ type Settings struct {
LastHeartbeatSent time.Time
+ PasswordArchive PasswordArchive
+
// **WARNING**: These entry can't be removed until they vault has proper migration support.
SyncWorkers int
SyncAttPool int
@@ -105,5 +107,7 @@ func newDefaultSettings(gluonDir string) Settings {
LastUserAgent: DefaultUserAgent,
LastHeartbeatSent: time.Time{},
+
+ PasswordArchive: PasswordArchive{},
}
}
diff --git a/internal/vault/types_user.go b/internal/vault/types_user.go
index a41085fb..9da47835 100644
--- a/internal/vault/types_user.go
+++ b/internal/vault/types_user.go
@@ -73,7 +73,7 @@ func (status SyncStatus) IsComplete() bool {
return status.HasLabels && status.HasMessages
}
-func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) UserData {
+func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, keyPass, bridgePass []byte) UserData {
return UserData{
UserID: userID,
Username: username,
@@ -82,7 +82,7 @@ func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, key
GluonKey: newRandomToken(32),
GluonIDs: make(map[string]string),
UIDValidity: make(map[string]imap.UID),
- BridgePass: newRandomToken(16),
+ BridgePass: bridgePass,
AddressMode: CombinedMode,
AuthUID: authUID,
diff --git a/internal/vault/user.go b/internal/vault/user.go
index f97358dc..1f997984 100644
--- a/internal/vault/user.go
+++ b/internal/vault/user.go
@@ -122,6 +122,14 @@ func (user *User) SetAuth(authUID, authRef string) error {
})
}
+func (user *User) setAuthAndKeyPassUnsafe(authUID, authRef string, keyPass []byte) error {
+ return user.vault.modUserUnsafe(user.userID, func(userData *UserData) {
+ userData.AuthRef = authRef
+ userData.AuthUID = authUID
+ userData.KeyPass = keyPass
+ })
+}
+
// KeyPass returns the user's (salted) key password.
func (user *User) KeyPass() []byte {
return user.vault.getUser(user.userID).KeyPass
diff --git a/internal/vault/vault.go b/internal/vault/vault.go
index 0b1c7002..e8cbc3b1 100644
--- a/internal/vault/vault.go
+++ b/internal/vault/vault.go
@@ -40,11 +40,11 @@ type Vault struct {
path string
gcm cipher.AEAD
- enc []byte
- encLock sync.RWMutex
+ enc []byte
- ref map[string]int
- refLock sync.Mutex
+ ref map[string]int
+
+ lock sync.RWMutex
panicHandler async.PanicHandler
}
@@ -79,14 +79,46 @@ func New(vaultDir, gluonCacheDir string, key []byte, panicHandler async.PanicHan
// GetUserIDs returns the user IDs and usernames of all users in the vault.
func (vault *Vault) GetUserIDs() []string {
- return xslices.Map(vault.get().Users, func(user UserData) string {
+ vault.lock.RLock()
+ defer vault.lock.RUnlock()
+
+ return xslices.Map(vault.getUnsafe().Users, func(user UserData) string {
return user.UserID
})
}
+func (vault *Vault) getUsers() ([]*User, error) {
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
+
+ users := vault.getUnsafe().Users
+
+ result := make([]*User, 0, len(users))
+
+ for _, user := range users {
+ u, err := vault.newUserUnsafe(user.UserID)
+ if err != nil {
+ for _, v := range result {
+ if err := v.Close(); err != nil {
+ logrus.WithError(err).Error("Fait to close user after failed get")
+ }
+ }
+
+ return nil, err
+ }
+
+ result = append(result, u)
+ }
+
+ return result, nil
+}
+
// HasUser returns true if the vault contains a user with the given ID.
func (vault *Vault) HasUser(userID string) bool {
- return xslices.IndexFunc(vault.get().Users, func(user UserData) bool {
+ vault.lock.RLock()
+ defer vault.lock.RUnlock()
+
+ return xslices.IndexFunc(vault.getUnsafe().Users, func(user UserData) bool {
return user.UserID == userID
}) >= 0
}
@@ -106,46 +138,72 @@ func (vault *Vault) GetUser(userID string, fn func(*User)) error {
// NewUser returns a new vault user. It must be closed before it can be deleted.
func (vault *Vault) NewUser(userID string) (*User, error) {
- if idx := xslices.IndexFunc(vault.get().Users, func(user UserData) bool {
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
+
+ return vault.newUserUnsafe(userID)
+}
+
+func (vault *Vault) newUserUnsafe(userID string) (*User, error) {
+ if idx := xslices.IndexFunc(vault.getUnsafe().Users, func(user UserData) bool {
return user.UserID == userID
}); idx < 0 {
return nil, errors.New("no such user")
}
- return vault.attachUser(userID), nil
+ return vault.attachUserUnsafe(userID), nil
}
// ForUser executes a callback for each user in the vault.
func (vault *Vault) ForUser(parallelism int, fn func(*User) error) error {
- userIDs := vault.GetUserIDs()
+ users, err := vault.getUsers()
+ if err != nil {
+ return err
+ }
- return parallel.DoContext(context.Background(), parallelism, len(userIDs), func(_ context.Context, idx int) error {
+ r := parallel.DoContext(context.Background(), parallelism, len(users), func(_ context.Context, idx int) error {
defer async.HandlePanic(vault.panicHandler)
- user, err := vault.NewUser(userIDs[idx])
- if err != nil {
- return err
- }
- defer func() { _ = user.Close() }()
-
+ user := users[idx]
return fn(user)
})
+
+ for _, u := range users {
+ if err := u.Close(); err != nil {
+ logrus.WithError(err).Error("Failed to close user after ForUser")
+ }
+ }
+
+ return r
}
// AddUser creates a new user in the vault with the given ID, username and password.
-// A bridge password and gluon key are generated using the package's token generator.
+// A gluon key is generated using the package's token generator. If a password is found in the password archive for this user,
+// it is restored, otherwise a new bridge password is generated using the package's token generator.
func (vault *Vault) AddUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) (*User, error) {
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
+
+ return vault.addUserUnsafe(userID, username, primaryEmail, authUID, authRef, keyPass)
+}
+
+func (vault *Vault) addUserUnsafe(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) (*User, error) {
logrus.WithField("userID", userID).Info("Adding vault user")
var exists bool
- if err := vault.mod(func(data *Data) {
+ if err := vault.modUnsafe(func(data *Data) {
if idx := xslices.IndexFunc(data.Users, func(user UserData) bool {
return user.UserID == userID
}); idx >= 0 {
exists = true
} else {
- data.Users = append(data.Users, newDefaultUser(userID, username, primaryEmail, authUID, authRef, keyPass))
+ bridgePass := data.Settings.PasswordArchive.get(primaryEmail)
+ if len(bridgePass) == 0 {
+ bridgePass = newRandomToken(16)
+ }
+
+ data.Users = append(data.Users, newDefaultUser(userID, username, primaryEmail, authUID, authRef, keyPass, bridgePass))
}
}); err != nil {
return nil, err
@@ -155,13 +213,42 @@ func (vault *Vault) AddUser(userID, username, primaryEmail, authUID, authRef str
return nil, errors.New("user already exists")
}
- return vault.NewUser(userID)
+ return vault.attachUserUnsafe(userID), nil
+}
+
+// GetOrAddUser retrieves an existing user and updates the authRef and keyPass or creates a new user. Returns
+// the user and whether the user did not exist before.
+func (vault *Vault) GetOrAddUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) (*User, bool, error) {
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
+
+ {
+ users := vault.getUnsafe().Users
+
+ idx := xslices.IndexFunc(users, func(user UserData) bool {
+ return user.UserID == userID
+ })
+
+ if idx >= 0 {
+ user := vault.attachUserUnsafe(userID)
+
+ if err := user.setAuthAndKeyPassUnsafe(authUID, authRef, keyPass); err != nil {
+ return nil, false, err
+ }
+
+ return user, false, nil
+ }
+ }
+
+ u, err := vault.addUserUnsafe(userID, username, primaryEmail, authUID, authRef, keyPass)
+
+ return u, true, err
}
// DeleteUser removes the given user from the vault.
func (vault *Vault) DeleteUser(userID string) error {
- vault.refLock.Lock()
- defer vault.refLock.Unlock()
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
logrus.WithField("userID", userID).Info("Deleting vault user")
@@ -169,7 +256,7 @@ func (vault *Vault) DeleteUser(userID string) error {
return fmt.Errorf("user %s is currently in use", userID)
}
- return vault.mod(func(data *Data) {
+ return vault.modUnsafe(func(data *Data) {
idx := xslices.IndexFunc(data.Users, func(user UserData) bool {
return user.UserID == userID
})
@@ -177,23 +264,32 @@ func (vault *Vault) DeleteUser(userID string) error {
if idx < 0 {
return
}
-
+ data.Settings.PasswordArchive.set(data.Users[idx].PrimaryEmail, data.Users[idx].BridgePass)
data.Users = append(data.Users[:idx], data.Users[idx+1:]...)
})
}
func (vault *Vault) Migrated() bool {
- return vault.get().Migrated
+ vault.lock.RLock()
+ defer vault.lock.RUnlock()
+
+ return vault.getUnsafe().Migrated
}
func (vault *Vault) SetMigrated() error {
- return vault.mod(func(data *Data) {
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
+
+ return vault.modUnsafe(func(data *Data) {
data.Migrated = true
})
}
func (vault *Vault) Reset(gluonDir string) error {
- return vault.mod(func(data *Data) {
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
+
+ return vault.modUnsafe(func(data *Data) {
*data = newDefaultData(gluonDir)
})
}
@@ -203,8 +299,8 @@ func (vault *Vault) Path() string {
}
func (vault *Vault) Close() error {
- vault.refLock.Lock()
- defer vault.refLock.Unlock()
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
if len(vault.ref) > 0 {
return errors.New("vault is still in use")
@@ -215,10 +311,7 @@ func (vault *Vault) Close() error {
return nil
}
-func (vault *Vault) attachUser(userID string) *User {
- vault.refLock.Lock()
- defer vault.refLock.Unlock()
-
+func (vault *Vault) attachUserUnsafe(userID string) *User {
logrus.WithField("userID", userID).Trace("Attaching vault user")
vault.ref[userID]++
@@ -230,8 +323,8 @@ func (vault *Vault) attachUser(userID string) *User {
}
func (vault *Vault) detachUser(userID string) error {
- vault.refLock.Lock()
- defer vault.refLock.Unlock()
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
logrus.WithField("userID", userID).Trace("Detaching vault user")
@@ -283,10 +376,14 @@ func newVault(path, gluonDir string, gcm cipher.AEAD) (*Vault, bool, error) {
}, corrupt, nil
}
-func (vault *Vault) get() Data {
- vault.encLock.RLock()
- defer vault.encLock.RUnlock()
+func (vault *Vault) getSafe() Data {
+ vault.lock.RLock()
+ defer vault.lock.RUnlock()
+ return vault.getUnsafe()
+}
+
+func (vault *Vault) getUnsafe() Data {
var data Data
if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil {
@@ -296,10 +393,14 @@ func (vault *Vault) get() Data {
return data
}
-func (vault *Vault) mod(fn func(data *Data)) error {
- vault.encLock.Lock()
- defer vault.encLock.Unlock()
+func (vault *Vault) modSafe(fn func(data *Data)) error {
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
+ return vault.modUnsafe(fn)
+}
+
+func (vault *Vault) modUnsafe(fn func(data *Data)) error {
var data Data
if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil {
@@ -319,13 +420,31 @@ func (vault *Vault) mod(fn func(data *Data)) error {
}
func (vault *Vault) getUser(userID string) UserData {
- return vault.get().Users[xslices.IndexFunc(vault.get().Users, func(user UserData) bool {
+ vault.lock.RLock()
+ defer vault.lock.RUnlock()
+
+ users := vault.getUnsafe().Users
+
+ idx := xslices.IndexFunc(users, func(user UserData) bool {
return user.UserID == userID
- })]
+ })
+
+ if idx < 0 {
+ panic("Unknown user")
+ }
+
+ return users[idx]
}
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
- return vault.mod(func(data *Data) {
+ vault.lock.Lock()
+ defer vault.lock.Unlock()
+
+ return vault.modUserUnsafe(userID, fn)
+}
+
+func (vault *Vault) modUserUnsafe(userID string, fn func(userData *UserData)) error {
+ return vault.modUnsafe(func(data *Data) {
idx := xslices.IndexFunc(data.Users, func(user UserData) bool {
return user.UserID == userID
})
diff --git a/internal/vault/vault_debug.go b/internal/vault/vault_debug.go
index 17ece7f8..aef5f5cf 100644
--- a/internal/vault/vault_debug.go
+++ b/internal/vault/vault_debug.go
@@ -24,7 +24,7 @@ import (
)
func (vault *Vault) ImportJSON(dec []byte) {
- vault.mod(func(data *Data) {
+ vault.modSafe(func(data *Data) {
if err := json.Unmarshal(dec, data); err != nil {
panic(err)
}
@@ -32,7 +32,7 @@ func (vault *Vault) ImportJSON(dec []byte) {
}
func (vault *Vault) ExportJSON() []byte {
- enc, err := json.MarshalIndent(vault.get(), "", " ")
+ enc, err := json.MarshalIndent(vault.getSafe(), "", " ")
if err != nil {
panic(err)
}
diff --git a/internal/versioner/remove_darwin.go b/internal/versioner/remove_darwin.go
index 3ffe26c0..6bcc20e6 100644
--- a/internal/versioner/remove_darwin.go
+++ b/internal/versioner/remove_darwin.go
@@ -29,7 +29,7 @@ func (v *Versioner) RemoveOldVersions() error {
}
// RemoveOtherVersions removes all but the specific provided app version.
-func (v *Versioner) RemoveOtherVersions(versionToKeep *semver.Version) error {
+func (v *Versioner) RemoveOtherVersions(_ *semver.Version) error {
// darwin does not use the versioner; removal is a noop.
return nil
}
diff --git a/pkg/cpc/cpc.go b/pkg/cpc/cpc.go
new file mode 100644
index 00000000..0892c28b
--- /dev/null
+++ b/pkg/cpc/cpc.go
@@ -0,0 +1,129 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Proton Mail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Proton Mail Bridge. If not, see .
+
+package cpc
+
+import (
+ "context"
+ "errors"
+)
+
+var ErrInvalidReplyType = errors.New("reply type does not match")
+
+// Utilities to implement Chanel Procedure Calls. Similar in concept to RPC, but with between go-routines.
+
+// Request contains the data for a request as well as the means to reply to a request.
+type Request struct {
+ value any
+ reply chan reply
+}
+
+// Value returns the request value.
+func (r *Request) Value() any {
+ return r.value
+}
+
+// Reply should be used to send a reply to a given request.
+func (r *Request) Reply(ctx context.Context, value any, err error) {
+ defer close(r.reply)
+
+ select {
+ case <-ctx.Done():
+ case r.reply <- reply{
+ value: value,
+ error: err,
+ }:
+ }
+}
+
+// CPC Channel Procedure Call. A play on RPC, but with channels. Use this type to send requests and wait for replies
+// from a goroutine.
+type CPC struct {
+ request chan *Request
+}
+
+func NewCPC() *CPC {
+ return &CPC{
+ request: make(chan *Request),
+ }
+}
+
+// Receive invokes the function on all the request that arrive.
+func (c *CPC) Receive(ctx context.Context, f func(context.Context, *Request)) {
+ for request := range c.request {
+ f(ctx, request)
+ }
+}
+
+// ReceiveCh returns the channel on which all requests are sent.
+func (c *CPC) ReceiveCh() <-chan *Request {
+ return c.request
+}
+
+// Close closes the CPC channel and no further requests should be made.
+func (c *CPC) Close() {
+ close(c.request)
+}
+
+// Send sends a request which expects a reply.
+func (c *CPC) Send(ctx context.Context, value any) (any, error) {
+ return c.execute(ctx, newRequest(value))
+}
+
+// SendTyped is similar to CPC.Send, but ensure that reply is of the given Type T.
+func SendTyped[T any](ctx context.Context, c *CPC, value any) (T, error) {
+ val, err := c.execute(ctx, newRequest(value))
+ if err != nil {
+ var t T
+ return t, err
+ }
+
+ switch vt := val.(type) {
+ case T:
+ return vt, nil
+ default:
+ var t T
+ return t, ErrInvalidReplyType
+ }
+}
+
+type reply struct {
+ value any
+ error error
+}
+
+func (c *CPC) execute(ctx context.Context, request *Request) (any, error) {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case c.request <- request:
+ }
+
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case r := <-request.reply:
+ return r.value, r.error
+ }
+}
+
+func newRequest(value any) *Request {
+ return &Request{
+ value: value,
+ reply: make(chan reply),
+ }
+}
diff --git a/pkg/cpc/cpc_test.go b/pkg/cpc/cpc_test.go
new file mode 100644
index 00000000..102970e5
--- /dev/null
+++ b/pkg/cpc/cpc_test.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail Bridge is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Proton Mail Bridge is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Proton Mail Bridge. If not, see .
+
+package cpc
+
+import (
+ "context"
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type sendIntRequest struct{}
+
+type quitRequest struct{}
+
+func TestCPC_Receive(t *testing.T) {
+ const replyValue = 20
+
+ cpc := NewCPC()
+
+ wg := sync.WaitGroup{}
+
+ go func() {
+ defer wg.Done()
+
+ wg.Add(1)
+
+ cpc.Receive(context.Background(), func(ctx context.Context, request *Request) {
+ switch request.Value().(type) {
+ case sendIntRequest:
+ request.Reply(ctx, replyValue, nil)
+ case quitRequest:
+ request.Reply(ctx, nil, nil)
+ default:
+ panic("unknown request")
+ }
+ })
+ }()
+
+ r, err := cpc.Send(context.Background(), sendIntRequest{})
+ require.NoError(t, err)
+ require.Equal(t, r, replyValue)
+
+ _, err = cpc.Send(context.Background(), quitRequest{})
+ require.NoError(t, err)
+
+ cpc.Close()
+ wg.Wait()
+}
diff --git a/pkg/message/build.go b/pkg/message/build.go
index 5e7e44bb..baf916ee 100644
--- a/pkg/message/build.go
+++ b/pkg/message/build.go
@@ -92,11 +92,7 @@ func buildSimpleRFC822(kr *crypto.KeyRing, msg proton.Message, opts JobOptions,
return err
}
- if err := w.Close(); err != nil {
- return err
- }
-
- return nil
+ return w.Close()
}
func buildMultipartRFC822(
@@ -148,11 +144,7 @@ func buildMultipartRFC822(
}
}
- if err := w.Close(); err != nil {
- return err
- }
-
- return nil
+ return w.Close()
}
func writeTextPart(
@@ -319,11 +311,7 @@ func buildPGPMIMEFallbackRFC822(msg proton.Message, opts JobOptions, buf *bytes.
return err
}
- if err := w.Close(); err != nil {
- return err
- }
-
- return nil
+ return w.Close()
}
func writeMultipartSignedRFC822(header message.Header, body []byte, sig proton.Signature, buf *bytes.Buffer) error {
@@ -379,11 +367,7 @@ func writeMultipartSignedRFC822(header message.Header, body []byte, sig proton.S
return err
}
- if err := mw.Close(); err != nil {
- return err
- }
-
- return nil
+ return mw.Close()
}
func writeMultipartEncryptedRFC822(header message.Header, body []byte, buf *bytes.Buffer) error {
diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go
index 5bbce163..981c06a9 100644
--- a/pkg/message/parser_test.go
+++ b/pkg/message/parser_test.go
@@ -673,6 +673,40 @@ func TestParsePanic(t *testing.T) {
require.Error(t, err)
}
+func TestParseTextPlainWithPdfAttachmentCyrillic(t *testing.T) {
+ f := getFileReader("text_plain_pdf_attachment_cyrillic.eml")
+
+ m, err := Parse(f)
+ require.NoError(t, err)
+
+ assert.Equal(t, `"Sender" `, m.Sender.String())
+ assert.Equal(t, `"Receiver" `, m.ToList[0].String())
+
+ assert.Equal(t, "Shake that body", string(m.RichBody))
+ assert.Equal(t, "Shake that body", string(m.PlainBody))
+
+ require.Len(t, m.Attachments, 1)
+ require.Equal(t, "application/pdf", m.Attachments[0].MIMEType)
+ assert.Equal(t, "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.pdf", m.Attachments[0].Name)
+}
+
+func TestParseTextPlainWithDocxAttachmentCyrillic(t *testing.T) {
+ f := getFileReader("text_plain_docx_attachment_cyrillic.eml")
+
+ m, err := Parse(f)
+ require.NoError(t, err)
+
+ assert.Equal(t, `"Sender" `, m.Sender.String())
+ assert.Equal(t, `"Receiver" `, m.ToList[0].String())
+
+ assert.Equal(t, "Shake that body", string(m.RichBody))
+ assert.Equal(t, "Shake that body", string(m.PlainBody))
+
+ require.Len(t, m.Attachments, 1)
+ require.Equal(t, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", m.Attachments[0].MIMEType)
+ assert.Equal(t, "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx", m.Attachments[0].Name)
+}
+
func getFileReader(filename string) io.Reader {
f, err := os.Open(filepath.Join("testdata", filename))
if err != nil {
@@ -684,6 +718,6 @@ func getFileReader(filename string) io.Reader {
type panicReader struct{}
-func (panicReader) Read(p []byte) (int, error) {
+func (panicReader) Read(_ []byte) (int, error) {
panic("lol")
}
diff --git a/pkg/message/testdata/text_plain_docx_attachment_cyrillic.eml b/pkg/message/testdata/text_plain_docx_attachment_cyrillic.eml
new file mode 100644
index 00000000..b81cfd1e
--- /dev/null
+++ b/pkg/message/testdata/text_plain_docx_attachment_cyrillic.eml
@@ -0,0 +1,24 @@
+Content-Type: multipart/mixed; boundary="------------nq8WTMHkJcymWO6pWfby0uY3"
+To: "Receiver"
+From: "Sender"
+Subject: Test with cyrillic attachment
+
+--------------nq8WTMHkJcymWO6pWfby0uY3
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Shake that body
+--------------nq8WTMHkJcymWO6pWfby0uY3
+Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document;
+ name="=?UTF-8?B?0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg?=
+ =?UTF-8?B?0KHQotCM0KPQpNCl0KfQj9CX0KguZG9jeA==?="
+Content-Disposition: attachment;
+ filename*0*=UTF-8''%D0%90%D0%91%D0%92%D0%93%D0%94%D0%83%D0%95%D0%96%D0%97;
+ filename*1*=%D0%85%D0%98%D0%88%D0%9A%D0%9B%D0%89%D0%9C%D0%9D%D0%8A%D0%9E;
+ filename*2*=%D0%9F%D0%A0%D0%A1%D0%A2%D0%8C%D0%A3%D0%A4%D0%A5%D0%A7%D0%8F;
+ filename*3*=%D0%97%D0%A8%2E%64%6F%63%78
+Content-Transfer-Encoding: base64
+
+0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg0KHQotCM0KPQpNCl0KfQj9CX0Kg=
+
+--------------nq8WTMHkJcymWO6pWfby0uY3--
diff --git a/pkg/message/testdata/text_plain_pdf_attachment_cyrillic.eml b/pkg/message/testdata/text_plain_pdf_attachment_cyrillic.eml
new file mode 100644
index 00000000..e83c92b6
--- /dev/null
+++ b/pkg/message/testdata/text_plain_pdf_attachment_cyrillic.eml
@@ -0,0 +1,25 @@
+Content-Type: multipart/mixed; boundary="------------bYzsV6z0EdKTbltmCDZgIM15"
+To: "Receiver"
+From: "Sender"
+Subject: Test with cyrillic attachment
+
+--------------bYzsV6z0EdKTbltmCDZgIM15
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+
+Shake that body
+--------------bYzsV6z0EdKTbltmCDZgIM15
+Content-Type: application/pdf;
+ name="=?UTF-8?B?0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg?=
+ =?UTF-8?B?0KHQotCM0KPQpNCl0KfQj9CX0KgucGRm?="
+Content-Disposition: attachment;
+ filename*0*=UTF-8''%D0%90%D0%91%D0%92%D0%93%D0%94%D0%83%D0%95%D0%96%D0%97;
+ filename*1*=%D0%85%D0%98%D0%88%D0%9A%D0%9B%D0%89%D0%9C%D0%9D%D0%8A%D0%9E;
+ filename*2*=%D0%9F%D0%A0%D0%A1%D0%A2%D0%8C%D0%A3%D0%A4%D0%A5%D0%A7%D0%8F;
+ filename*3*=%D0%97%D0%A8%2E%70%64%66
+Content-Transfer-Encoding: base64
+
+0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg0KHQotCM0KPQpNCl0KfQj9CX0Kg=
+
+
+--------------bYzsV6z0EdKTbltmCDZgIM15--
diff --git a/tests/bdd_test.go b/tests/bdd_test.go
index 92a73bed..def2ac15 100644
--- a/tests/bdd_test.go
+++ b/tests/bdd_test.go
@@ -106,6 +106,7 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^the user agent is "([^"]*)"$`, s.theUserAgentIs)
ctx.Step(`^the header in the "([^"]*)" request to "([^"]*)" has "([^"]*)" set to "([^"]*)"$`, s.theHeaderInTheRequestToHasSetTo)
ctx.Step(`^the body in the "([^"]*)" request to "([^"]*)" is:$`, s.theBodyInTheRequestToIs)
+ ctx.Step(`^the body in the "([^"]*)" response to "([^"]*)" is:$`, s.theBodyInTheResponseToIs)
ctx.Step(`^the API requires bridge version at least "([^"]*)"$`, s.theAPIRequiresBridgeVersion)
ctx.Step(`^the network port (\d+) is busy$`, s.networkPortIsBusy)
ctx.Step(`^the network port range (\d+)-(\d+) is busy$`, s.networkPortRangeIsBusy)
@@ -179,6 +180,8 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^user "([^"]*)" is not listed$`, s.userIsNotListed)
ctx.Step(`^user "([^"]*)" finishes syncing$`, s.userFinishesSyncing)
ctx.Step(`^user "([^"]*)" has telemetry set to (\d+)$`, s.userHasTelemetrySetTo)
+ ctx.Step(`^the bridge password of user "([^"]*)" is changed to "([^"]*)"`, s.bridgePasswordOfUserIsChangedTo)
+ ctx.Step(`^the bridge password of user "([^"]*)" is equal to "([^"]*)"`, s.bridgePasswordOfUserIsEqualTo)
// ==== IMAP ====
ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient)
diff --git a/tests/bridge_test.go b/tests/bridge_test.go
index 7d3edd14..fd5f2324 100644
--- a/tests/bridge_test.go
+++ b/tests/bridge_test.go
@@ -60,11 +60,11 @@ func (s *scenario) theAPIRequiresBridgeVersion(version string) error {
}
func (s *scenario) theUserChangesTheIMAPPortTo(port int) error {
- return s.t.bridge.SetIMAPPort(port)
+ return s.t.bridge.SetIMAPPort(context.Background(), port)
}
func (s *scenario) theUserChangesTheSMTPPortTo(port int) error {
- return s.t.bridge.SetSMTPPort(port)
+ return s.t.bridge.SetSMTPPort(context.Background(), port)
}
func (s *scenario) theUserSetsTheAddressModeOfUserTo(user, mode string) error {
@@ -144,11 +144,11 @@ func (s *scenario) theUserHasEnabledAlternativeRouting() error {
}
func (s *scenario) theUserSetIMAPModeToSSL() error {
- return s.t.bridge.SetIMAPSSL(true)
+ return s.t.bridge.SetIMAPSSL(context.Background(), true)
}
func (s *scenario) theUserSetSMTPModeToSSL() error {
- return s.t.bridge.SetSMTPSSL(true)
+ return s.t.bridge.SetSMTPSSL(context.Background(), true)
}
func (s *scenario) theUserReportsABug() error {
diff --git a/tests/ctx_bridge_test.go b/tests/ctx_bridge_test.go
index 0afe93d7..b153ac39 100644
--- a/tests/ctx_bridge_test.go
+++ b/tests/ctx_bridge_test.go
@@ -114,6 +114,7 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) {
} else if corrupt {
return nil, fmt.Errorf("vault is corrupt")
}
+ t.vault = vault
// Create the underlying cookie jar.
jar, err := cookiejar.New(nil)
@@ -351,8 +352,8 @@ func (t *testCtx) expectProxyCtlAllowProxy() {
type mockRestarter struct{}
-func (m *mockRestarter) Set(restart, crash bool) {}
+func (m *mockRestarter) Set(_, _ bool) {}
-func (m *mockRestarter) AddFlags(flags ...string) {}
+func (m *mockRestarter) AddFlags(_ ...string) {}
-func (m *mockRestarter) Override(exe string) {}
+func (m *mockRestarter) Override(_ string) {}
diff --git a/tests/ctx_imap_test.go b/tests/ctx_imap_test.go
index 7f8b3d6a..13b590ad 100644
--- a/tests/ctx_imap_test.go
+++ b/tests/ctx_imap_test.go
@@ -19,6 +19,7 @@ package tests
import (
"fmt"
+ "time"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/emersion/go-imap/client"
@@ -29,14 +30,14 @@ func (t *testCtx) newIMAPClient(userID, clientID string) error {
}
func (t *testCtx) newIMAPClientOnPort(userID, clientID string, imapPort int) error {
- client, err := client.Dial(fmt.Sprintf("%v:%d", constants.Host, imapPort))
+ cli, err := eventuallyDial(fmt.Sprintf("%v:%d", constants.Host, imapPort))
if err != nil {
return err
}
t.imapClients[clientID] = &imapClient{
userID: userID,
- client: client,
+ client: cli,
}
return nil
@@ -45,3 +46,16 @@ func (t *testCtx) newIMAPClientOnPort(userID, clientID string, imapPort int) err
func (t *testCtx) getIMAPClient(clientID string) (string, *client.Client) {
return t.imapClients[clientID].userID, t.imapClients[clientID].client
}
+
+func eventuallyDial(addr string) (cli *client.Client, err error) {
+ var sleep = 1 * time.Second
+ for i := 0; i < 5; i++ {
+ cli, err := client.Dial(addr)
+ if err == nil {
+ return cli, nil
+ }
+ time.Sleep(sleep)
+ sleep *= 2
+ }
+ return nil, fmt.Errorf("after 5 attempts, last error: %s", err)
+}
diff --git a/tests/ctx_test.go b/tests/ctx_test.go
index 369e865f..f667c5f1 100644
--- a/tests/ctx_test.go
+++ b/tests/ctx_test.go
@@ -36,6 +36,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
frontend "github.com/ProtonMail/proton-bridge/v3/internal/frontend/grpc"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
+ "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/bradenaw/juniper/xslices"
"github.com/cucumber/godog"
"github.com/emersion/go-imap/client"
@@ -135,6 +136,7 @@ type testCtx struct {
// bridge holds the bridge app under test.
bridge *bridge.Bridge
+ vault *vault.Vault
// service holds the gRPC frontend service under test.
service *frontend.Service
@@ -165,6 +167,9 @@ type testCtx struct {
// This slice contains the dummy listeners that are intended to block network ports.
dummyListeners []net.Listener
+
+ imapServerStarted bool
+ smtpServerStarted bool
}
type imapClient struct {
diff --git a/tests/environment_test.go b/tests/environment_test.go
index da78767d..d80a7861 100644
--- a/tests/environment_test.go
+++ b/tests/environment_test.go
@@ -110,3 +110,27 @@ func (s *scenario) theBodyInTheRequestToIs(method, path string, value *godog.Doc
return nil
}
+
+func (s *scenario) theBodyInTheResponseToIs(method, path string, value *godog.DocString) error {
+ // We have to exclude HTTP-Overrides to avoid race condition with the creating and sending of the draft message.
+ call, err := s.t.getLastCallExcludingHTTPOverride(method, path)
+ if err != nil {
+ return err
+ }
+
+ var body, want map[string]any
+
+ if err := json.Unmarshal(call.ResponseBody, &body); err != nil {
+ return err
+ }
+
+ if err := json.Unmarshal([]byte(value.Content), &want); err != nil {
+ return err
+ }
+
+ if !IsSub(body, want) {
+ return fmt.Errorf("have body %v, want %v", body, want)
+ }
+
+ return nil
+}
diff --git a/tests/fast.go b/tests/fast.go
index 84befbeb..e08283a6 100644
--- a/tests/fast.go
+++ b/tests/fast.go
@@ -28,7 +28,7 @@ var (
preCompKeyPEM []byte
)
-func FastGenerateCert(template *x509.Certificate) ([]byte, []byte, error) {
+func FastGenerateCert(_ *x509.Certificate) ([]byte, []byte, error) {
return preCompCertPEM, preCompKeyPEM, nil
}
diff --git a/tests/features/bridge/heartbeat.feature b/tests/features/bridge/heartbeat.feature
index 9584f800..1288b7c5 100644
--- a/tests/features/bridge/heartbeat.feature
+++ b/tests/features/bridge/heartbeat.feature
@@ -1,7 +1,9 @@
Feature: Send Telemetry Heartbeat
Background:
Given there exists an account with username "[user:user1]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
+ Then it succeeds
Scenario: Send at first start - one user default settings
diff --git a/tests/features/imap/auth.feature b/tests/features/imap/auth.feature
index d839a3bc..c51539f4 100644
--- a/tests/features/imap/auth.feature
+++ b/tests/features/imap/auth.feature
@@ -4,9 +4,11 @@ Feature: A user can authenticate an IMAP client
And there exists an account with username "[user:user2]" and password "password2"
And the account "[user:user]" has additional address "[alias:alias]@[domain]"
And the account "[user:user2]" has additional disabled address "[alias:alias2]@[domain]"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And the user logs in with username "[user:user2]" and password "password2"
+ Then it succeeds
Scenario: IMAP client can authenticate successfully
When user "[user:user]" connects IMAP client "1"
diff --git a/tests/features/imap/id.feature b/tests/features/imap/id.feature
index a8653b9e..95d1ab83 100644
--- a/tests/features/imap/id.feature
+++ b/tests/features/imap/id.feature
@@ -1,8 +1,10 @@
Feature: The IMAP ID is propagated to bridge
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
+ Then it succeeds
Scenario: Initial user agent before an IMAP client announces its ID
When user "[user:user]" connects IMAP client "1"
diff --git a/tests/features/imap/mailbox/create.feature b/tests/features/imap/mailbox/create.feature
index e89ffc7c..c7617b43 100644
--- a/tests/features/imap/mailbox/create.feature
+++ b/tests/features/imap/mailbox/create.feature
@@ -7,10 +7,12 @@ Feature: IMAP create mailbox
| f2 | folder |
| l1 | label |
| l2 | label |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Create folder
When IMAP client "1" creates "Folders/mbox"
diff --git a/tests/features/imap/mailbox/delete.feature b/tests/features/imap/mailbox/delete.feature
index 01c664ae..0f269dda 100644
--- a/tests/features/imap/mailbox/delete.feature
+++ b/tests/features/imap/mailbox/delete.feature
@@ -6,10 +6,12 @@ Feature: IMAP delete mailbox
| one | folder |
| two | folder |
| three | label |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Delete folder
When IMAP client "1" deletes "Folders/one"
diff --git a/tests/features/imap/mailbox/hide_all_mail.feature b/tests/features/imap/mailbox/hide_all_mail.feature
index 9a450e8f..4009a1d9 100644
--- a/tests/features/imap/mailbox/hide_all_mail.feature
+++ b/tests/features/imap/mailbox/hide_all_mail.feature
@@ -1,10 +1,12 @@
Feature: IMAP Hide All Mail
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Hide All Mail Mailbox
Given IMAP client "1" eventually sees the following mailbox info:
diff --git a/tests/features/imap/mailbox/info.feature b/tests/features/imap/mailbox/info.feature
index 670f0936..bcf938a2 100644
--- a/tests/features/imap/mailbox/info.feature
+++ b/tests/features/imap/mailbox/info.feature
@@ -8,9 +8,11 @@ Feature: IMAP get mailbox info
| from | to | subject | unread |
| a@example.com | a@example.com | one | true |
| b@example.com | b@example.com | two | false |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
+ Then it succeeds
Scenario: Mailbox status reports correct name, total and unread
When user "[user:user]" connects and authenticates IMAP client "1"
diff --git a/tests/features/imap/mailbox/list.feature b/tests/features/imap/mailbox/list.feature
index ac939363..d800019b 100644
--- a/tests/features/imap/mailbox/list.feature
+++ b/tests/features/imap/mailbox/list.feature
@@ -5,11 +5,13 @@ Feature: IMAP list mailboxes
| name | type |
| mbox1 | folder |
| mbox2 | label |
+ Then it succeeds
When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
- Then IMAP client "1" eventually sees the following mailbox info:
+ Then it succeeds
+ And IMAP client "1" eventually sees the following mailbox info:
| name |
| INBOX |
| Drafts |
diff --git a/tests/features/imap/mailbox/rename.feature b/tests/features/imap/mailbox/rename.feature
index 5e88af5c..25f2bb04 100644
--- a/tests/features/imap/mailbox/rename.feature
+++ b/tests/features/imap/mailbox/rename.feature
@@ -5,10 +5,12 @@ Feature: IMAP get mailbox info
| name | type |
| f1 | folder |
| l1 | label |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Rename folder
When IMAP client "1" renames "Folders/f1" to "Folders/f2"
diff --git a/tests/features/imap/mailbox/rename_hiearchy.feature b/tests/features/imap/mailbox/rename_hiearchy.feature
index 7bb0de8d..4ca53036 100644
--- a/tests/features/imap/mailbox/rename_hiearchy.feature
+++ b/tests/features/imap/mailbox/rename_hiearchy.feature
@@ -5,10 +5,12 @@ Feature: IMAP get mailbox info
| name | type |
| f1 | folder |
| f1/f2| folder |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Rename folder with subfolders
When IMAP client "1" renames "Folders/f1" to "Folders/f3"
diff --git a/tests/features/imap/mailbox/select.feature b/tests/features/imap/mailbox/select.feature
index abfa1d9c..af3dfb3d 100644
--- a/tests/features/imap/mailbox/select.feature
+++ b/tests/features/imap/mailbox/select.feature
@@ -5,10 +5,12 @@ Feature: IMAP select mailbox
| name | type |
| mbox | folder |
| label | label |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Select inbox
When IMAP client "1" selects "INBOX"
diff --git a/tests/features/imap/message/copy.feature b/tests/features/imap/message/copy.feature
index 016fa323..4dd0b6c6 100644
--- a/tests/features/imap/message/copy.feature
+++ b/tests/features/imap/message/copy.feature
@@ -9,10 +9,12 @@ Feature: IMAP copy messages
| from | to | subject | unread |
| john.doe@mail.com | [user:user]@[domain] | foo | false |
| jane.doe@mail.com | name@[domain] | bar | true |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Copy message to label
When IMAP client "1" copies the message with subject "foo" from "INBOX" to "Labels/label"
diff --git a/tests/features/imap/message/create.feature b/tests/features/imap/message/create.feature
index 367197da..9b7305c4 100644
--- a/tests/features/imap/message/create.feature
+++ b/tests/features/imap/message/create.feature
@@ -2,10 +2,12 @@ Feature: IMAP create messages
Background:
Given there exists an account with username "[user:user]" and password "password"
And the account "[user:user]" has additional address "[alias:alias]@[domain]"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Creates message to user's primary address
When IMAP client "1" appends the following messages to "INBOX":
diff --git a/tests/features/imap/message/delete.feature b/tests/features/imap/message/delete.feature
index 72fcb288..b9247f19 100644
--- a/tests/features/imap/message/delete.feature
+++ b/tests/features/imap/message/delete.feature
@@ -7,10 +7,12 @@ Feature: IMAP remove messages from mailbox
| label | label |
And the address "[user:user]@[domain]" of account "[user:user]" has 10 messages in "Folders/mbox"
And the address "[user:user]@[domain]" of account "[user:user]" has 1 messages in "Scheduled"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Mark message as deleted and EXPUNGE
When IMAP client "1" selects "Folders/mbox"
diff --git a/tests/features/imap/message/delete_from_trash.feature b/tests/features/imap/message/delete_from_trash.feature
index fdc5105e..15446eed 100644
--- a/tests/features/imap/message/delete_from_trash.feature
+++ b/tests/features/imap/message/delete_from_trash.feature
@@ -5,6 +5,7 @@ Feature: IMAP remove messages from Trash
| name | type |
| mbox | folder |
| label | label |
+ Then it succeeds
Scenario Outline: Message in Trash and some other label is not permanently deleted
Given the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Trash":
diff --git a/tests/features/imap/message/drafts.feature b/tests/features/imap/message/drafts.feature
index 2c8e6ae7..0d5f9729 100644
--- a/tests/features/imap/message/drafts.feature
+++ b/tests/features/imap/message/drafts.feature
@@ -1,7 +1,8 @@
Feature: IMAP Draft messages
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
@@ -11,12 +12,13 @@ Feature: IMAP Draft messages
This is a dra
"""
- And it succeeds
- Then IMAP client "1" eventually sees the following messages in "Drafts":
+ Then it succeeds
+ And IMAP client "1" eventually sees the following messages in "Drafts":
| body |
| This is a dra |
And IMAP client "1" eventually sees 1 messages in "Drafts"
+
Scenario: Draft edited locally
When IMAP client "1" marks message 1 as deleted
And IMAP client "1" expunges
diff --git a/tests/features/imap/message/fetch.feature b/tests/features/imap/message/fetch.feature
index f72461b4..f1375c91 100644
--- a/tests/features/imap/message/fetch.feature
+++ b/tests/features/imap/message/fetch.feature
@@ -7,10 +7,12 @@ Feature: IMAP Fetch
And the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Inbox":
| from | to | subject | date |
| john.doe@mail.com | [user:user]@[domain] | foo | 13 Jul 69 00:00 +0000 |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Fetch very old message
Given IMAP client "1" eventually sees the following messages in "INBOX":
diff --git a/tests/features/imap/message/import.feature b/tests/features/imap/message/import.feature
index b70ad4c7..3608d133 100644
--- a/tests/features/imap/message/import.feature
+++ b/tests/features/imap/message/import.feature
@@ -1,10 +1,12 @@
Feature: IMAP import messages
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Basic message import
When IMAP client "1" appends the following message to "INBOX":
diff --git a/tests/features/imap/message/move.feature b/tests/features/imap/message/move.feature
index 33235de5..1434930f 100644
--- a/tests/features/imap/message/move.feature
+++ b/tests/features/imap/message/move.feature
@@ -19,10 +19,12 @@ Feature: IMAP move messages
And the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Scheduled":
| from | to | subject | unread |
| john.doe@mail.com | [user:user]@[domain] | sch | false |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Move message from folder to label (keeps in folder)
When IMAP client "1" moves the message with subject "foo" from "INBOX" to "Labels/label"
diff --git a/tests/features/imap/message/move_without_support.feature b/tests/features/imap/message/move_without_support.feature
index ca81095e..0ef82762 100644
--- a/tests/features/imap/message/move_without_support.feature
+++ b/tests/features/imap/message/move_without_support.feature
@@ -4,11 +4,13 @@ Feature: IMAP move messages by append and delete (without MOVE support, e.g., Ou
And the account "[user:user]" has the following custom mailboxes:
| name | type |
| mbox | folder |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "source"
And user "[user:user]" connects and authenticates IMAP client "target"
+ Then it succeeds
Scenario Outline: Move message from to by
When IMAP client "source" appends the following message to "":
diff --git a/tests/features/imap/migration.feature b/tests/features/imap/migration.feature
index 4d6a11a9..485e966f 100644
--- a/tests/features/imap/migration.feature
+++ b/tests/features/imap/migration.feature
@@ -7,10 +7,11 @@ Feature: Bridge can fully sync an account
| jane.doe@mail.com | name@[domain] | bar | true |
And the account "[user:user]" has 20 custom folders
And the account "[user:user]" has 60 custom labels
+ Then it succeeds
When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
- When user "[user:user]" connects and authenticates IMAP client "1"
+ And user "[user:user]" connects and authenticates IMAP client "1"
Then IMAP client "1" counts 20 mailboxes under "Folders"
And IMAP client "1" counts 60 mailboxes under "Labels"
diff --git a/tests/features/imap/ports.feature b/tests/features/imap/ports.feature
index 11ff9e12..8a221cdc 100644
--- a/tests/features/imap/ports.feature
+++ b/tests/features/imap/ports.feature
@@ -1,9 +1,11 @@
Feature: A user can connect an IMAP client to custom ports
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And the user changes the IMAP port to 1144
+ Then it succeeds
Scenario: Authenticates successfully on custom port
When user "[user:user]" connects IMAP client "1" on port 1144
diff --git a/tests/features/smtp/auth.feature b/tests/features/smtp/auth.feature
index 540b291f..067b9321 100644
--- a/tests/features/smtp/auth.feature
+++ b/tests/features/smtp/auth.feature
@@ -6,10 +6,12 @@ Feature: A user can authenticate an SMTP client
And the account "[user:user]" has additional address "[alias:alias]@[domain]"
And the account "[user:user2]" has additional disabled address "[alias:alias2]@[domain]"
And the account "[user:user3]" has additional address "[alias:alias3]@[domain]"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And the user logs in with username "[user:user2]" and password "password2"
And the user logs in with username "[user:user3]" and password "password3"
+ Then it succeeds
Scenario: SMTP client can authenticate successfully
When user "[user:user]" connects SMTP client "1"
diff --git a/tests/features/smtp/init.feature b/tests/features/smtp/init.feature
index 20fb9258..4d5ba114 100644
--- a/tests/features/smtp/init.feature
+++ b/tests/features/smtp/init.feature
@@ -1,9 +1,11 @@
Feature: SMTP initiation
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
- When user "[user:user]" connects and authenticates SMTP client "1"
+ And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: Send without first announcing FROM and TO
When SMTP client "1" sends DATA:
diff --git a/tests/features/smtp/ports.feature b/tests/features/smtp/ports.feature
index 82fd17ec..d117d968 100644
--- a/tests/features/smtp/ports.feature
+++ b/tests/features/smtp/ports.feature
@@ -1,10 +1,12 @@
Feature: A user can connect an SMTP client to custom ports
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
- And the user changes the SMTP port to 1144
+ Then it succeeds
Scenario: Authenticates successfully on custom port
+ When the user changes the SMTP port to 1144
When user "[user:user]" connects SMTP client "1" on port 1144
Then SMTP client "1" can authenticate
\ No newline at end of file
diff --git a/tests/features/smtp/send/attachment.feature b/tests/features/smtp/send/attachment.feature
new file mode 100644
index 00000000..d8151a3c
--- /dev/null
+++ b/tests/features/smtp/send/attachment.feature
@@ -0,0 +1,143 @@
+Feature: SMTP sending with attachment
+ Background:
+ Given there exists an account with username "[user:user1]" and password "password"
+ And there exists an account with username "[user:user2]" and password "password"
+ Then it succeeds
+ When bridge starts
+ And the user logs in with username "[user:user1]" and password "password"
+ And user "[user:user1]" finishes syncing
+ Then it succeeds
+ When user "[user:user1]" connects and authenticates SMTP client "1"
+ And user "[user:user1]" connects and authenticates IMAP client "1"
+ Then it succeeds
+
+ @long-black
+ Scenario: Sending with cyrillic PDF attachment
+ When SMTP client "1" sends the following message from "[user:user1]@[domain]" to "[user:user2]@[domain]":
+ """
+ Content-Type: multipart/mixed; boundary="------------bYzsV6z0EdKTbltmCDZgIM15"
+ From: Bridge Test <[user:user1]@[domain]>
+ To: Internal Bridge <[user:user2]@[domain]>
+ Subject: Test with cyrillic attachment
+
+ --------------bYzsV6z0EdKTbltmCDZgIM15
+ Content-Transfer-Encoding: quoted-printable
+ Content-Type: text/plain; charset=utf-8
+
+ Shake that body
+ --------------bYzsV6z0EdKTbltmCDZgIM15
+ Content-Type: application/pdf;
+ name="=?UTF-8?B?0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg?=
+ =?UTF-8?B?0KHQotCM0KPQpNCl0KfQj9CX0KgucGRm?="
+ Content-Disposition: attachment;
+ filename*0*=UTF-8''%D0%90%D0%91%D0%92%D0%93%D0%94%D0%83%D0%95%D0%96%D0%97;
+ filename*1*=%D0%85%D0%98%D0%88%D0%9A%D0%9B%D0%89%D0%9C%D0%9D%D0%8A%D0%9E;
+ filename*2*=%D0%9F%D0%A0%D0%A1%D0%A2%D0%8C%D0%A3%D0%A4%D0%A5%D0%A7%D0%8F;
+ filename*3*=%D0%97%D0%A8%2E%70%64%66
+ Content-Transfer-Encoding: base64
+
+ 0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg0KHQotCM0KPQpNCl0KfQj9CX0Kg=
+
+ --------------bYzsV6z0EdKTbltmCDZgIM15--
+
+ """
+ Then it succeeds
+ Then IMAP client "1" eventually sees the following messages in "Sent":
+ | from | to | subject |
+ | [user:user1]@[domain] | [user:user2]@[domain] | Test with cyrillic attachment |
+ And the body in the "POST" request to "/mail/v4/messages" is:
+ """
+ {
+ "Message": {
+ "Subject": "Test with cyrillic attachment",
+ "Sender": {
+ "Name": "Bridge Test"
+ },
+ "ToList": [
+ {
+ "Address": "[user:user2]@[domain]",
+ "Name": "Internal Bridge"
+ }
+ ],
+ "CCList": [],
+ "BCCList": [],
+ "MIMEType": "text/plain"
+ }
+ }
+ """
+ And the body in the "POST" response to "/mail/v4/attachments" is:
+ """
+ {
+ "Attachment":{
+ "Name": "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.pdf",
+ "MIMEType": "application/pdf",
+ "Disposition": "attachment"
+ }
+ }
+ """
+
+
+ @long-black
+ Scenario: Sending with cyrillic docx attachment
+ When SMTP client "1" sends the following message from "[user:user1]@[domain]" to "[user:user2]@[domain]":
+ """
+ Content-Type: multipart/mixed; boundary="------------9xfXriG1c1v5iJlMiIMCaIWP"
+ From: Bridge Test <[user:user1]@[domain]>
+ To: Internal Bridge <[user:user2]@[domain]>
+ Subject: Test with cyrillic attachment
+
+ --------------9xfXriG1c1v5iJlMiIMCaIWP
+ Content-Type: text/plain; charset=UTF-8; format=flowed
+ Content-Transfer-Encoding: 7bit
+
+ Shake that body
+ --------------9xfXriG1c1v5iJlMiIMCaIWP
+ Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document;
+ name="=?UTF-8?B?0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg?=
+ =?UTF-8?B?0KHQotCM0KPQpNCl0KfQj9CX0KguZG9jeA==?="
+ Content-Disposition: attachment;
+ filename*0*=UTF-8''%D0%90%D0%91%D0%92%D0%93%D0%94%D0%83%D0%95%D0%96%D0%97;
+ filename*1*=%D0%85%D0%98%D0%88%D0%9A%D0%9B%D0%89%D0%9C%D0%9D%D0%8A%D0%9E;
+ filename*2*=%D0%9F%D0%A0%D0%A1%D0%A2%D0%8C%D0%A3%D0%A4%D0%A5%D0%A7%D0%8F;
+ filename*3*=%D0%97%D0%A8%2E%64%6F%63%78
+ Content-Transfer-Encoding: base64
+
+ 0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg0KHQotCM0KPQpNCl0KfQj9CX0Kg=
+
+ --------------9xfXriG1c1v5iJlMiIMCaIWP--
+
+ """
+ Then it succeeds
+ Then IMAP client "1" eventually sees the following messages in "Sent":
+ | from | to | subject |
+ | [user:user1]@[domain] | [user:user2]@[domain] | Test with cyrillic attachment |
+ And the body in the "POST" request to "/mail/v4/messages" is:
+ """
+ {
+ "Message": {
+ "Subject": "Test with cyrillic attachment",
+ "Sender": {
+ "Name": "Bridge Test"
+ },
+ "ToList": [
+ {
+ "Address": "[user:user2]@[domain]",
+ "Name": "Internal Bridge"
+ }
+ ],
+ "CCList": [],
+ "BCCList": [],
+ "MIMEType": "text/plain"
+ }
+ }
+ """
+ And the body in the "POST" response to "/mail/v4/attachments" is:
+ """
+ {
+ "Attachment":{
+ "Name": "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx",
+ "MIMEType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "Disposition": "attachment"
+ }
+ }
+ """
\ No newline at end of file
diff --git a/tests/features/smtp/send/bcc.feature b/tests/features/smtp/send/bcc.feature
index a41f60da..8ae2a770 100644
--- a/tests/features/smtp/send/bcc.feature
+++ b/tests/features/smtp/send/bcc.feature
@@ -1,12 +1,14 @@
Feature: SMTP with bcc
Background:
Given there exists an account with username "[user:user]" and password "password"
- Given there exists an account with username "[user:to]" and password "password"
- Given there exists an account with username "[user:bcc]" and password "password"
- And bridge starts
+ And there exists an account with username "[user:to]" and password "password"
+ And there exists an account with username "[user:bcc]" and password "password"
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And the user logs in with username "[user:bcc]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: Send message to address in to and bcc
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain], [user:bcc]@[domain]":
diff --git a/tests/features/smtp/send/embedded_message.feature b/tests/features/smtp/send/embedded_message.feature
index 3ab685c9..a7c94db3 100644
--- a/tests/features/smtp/send/embedded_message.feature
+++ b/tests/features/smtp/send/embedded_message.feature
@@ -2,10 +2,12 @@ Feature: SMTP sending embedded message
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And the user logs in with username "[user:to]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
@long-black
Scenario: Send it
diff --git a/tests/features/smtp/send/failures.feature b/tests/features/smtp/send/failures.feature
index cd93f6a6..19c53538 100644
--- a/tests/features/smtp/send/failures.feature
+++ b/tests/features/smtp/send/failures.feature
@@ -2,9 +2,11 @@ Feature: SMTP wrong messages
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: Message with attachment and wrong boundaries
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
diff --git a/tests/features/smtp/send/html.feature b/tests/features/smtp/send/html.feature
index 0b120d93..5f2d28ea 100644
--- a/tests/features/smtp/send/html.feature
+++ b/tests/features/smtp/send/html.feature
@@ -2,9 +2,11 @@ Feature: SMTP sending of plain messages
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: HTML message to external account
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "pm.bridge.qa@gmail.com":
diff --git a/tests/features/smtp/send/html_att.feature b/tests/features/smtp/send/html_att.feature
index effb7989..5d0a547f 100644
--- a/tests/features/smtp/send/html_att.feature
+++ b/tests/features/smtp/send/html_att.feature
@@ -2,9 +2,11 @@ Feature: SMTP sending of plain messages
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: HTML message with attachment to internal account
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
diff --git a/tests/features/smtp/send/inline.feature b/tests/features/smtp/send/inline.feature
index 29d0fd17..97aab9e7 100644
--- a/tests/features/smtp/send/inline.feature
+++ b/tests/features/smtp/send/inline.feature
@@ -2,9 +2,11 @@ Feature: SMTP messages containing inlines
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: A message with inline attachment to internal account
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
diff --git a/tests/features/smtp/send/mixed_case.feature b/tests/features/smtp/send/mixed_case.feature
index dd6b918b..248b14df 100644
--- a/tests/features/smtp/send/mixed_case.feature
+++ b/tests/features/smtp/send/mixed_case.feature
@@ -2,9 +2,11 @@ Feature: SMTP sending with mixed case address
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: Mixed sender case in sender address
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
diff --git a/tests/features/smtp/send/one_account_to_another.feature b/tests/features/smtp/send/one_account_to_another.feature
index 62e20587..82b29568 100644
--- a/tests/features/smtp/send/one_account_to_another.feature
+++ b/tests/features/smtp/send/one_account_to_another.feature
@@ -2,9 +2,11 @@ Feature: SMTP sending two messages
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:recp]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And the user logs in with username "[user:recp]" and password "password"
+ Then it succeeds
@long-black
diff --git a/tests/features/smtp/send/plain.feature b/tests/features/smtp/send/plain.feature
index 1dffe325..a8427333 100644
--- a/tests/features/smtp/send/plain.feature
+++ b/tests/features/smtp/send/plain.feature
@@ -3,9 +3,11 @@ Feature: SMTP sending of plain messages
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
And there exists an account with username "[user:cc]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: Only from and to headers to internal account
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
diff --git a/tests/features/smtp/send/plain_att.feature b/tests/features/smtp/send/plain_att.feature
index 41a9112f..009e6d8c 100644
--- a/tests/features/smtp/send/plain_att.feature
+++ b/tests/features/smtp/send/plain_att.feature
@@ -2,9 +2,11 @@ Feature: SMTP sending of plain messages
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
+ Then it succeeds
Scenario: Basic message with attachment to internal account
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
diff --git a/tests/features/smtp/send/same_message.feature b/tests/features/smtp/send/same_message.feature
index 394b28d3..f877f659 100644
--- a/tests/features/smtp/send/same_message.feature
+++ b/tests/features/smtp/send/same_message.feature
@@ -2,7 +2,8 @@ Feature: SMTP sending the same message twice
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And the user logs in with username "[user:to]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
diff --git a/tests/features/smtp/send/send_append.feature b/tests/features/smtp/send/send_append.feature
index 1d475360..57e5b443 100644
--- a/tests/features/smtp/send/send_append.feature
+++ b/tests/features/smtp/send/send_append.feature
@@ -2,10 +2,12 @@ Feature: SMTP sending with APPENDing to Sent
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" connects and authenticates SMTP client "1"
And user "[user:user]" connects and authenticates IMAP client "1"
+ Then it succeeds
Scenario: Send message and append to Sent
# First do sending.
diff --git a/tests/features/smtp/send/send_reply.feature b/tests/features/smtp/send/send_reply.feature
index 095b5749..4727e1bc 100644
--- a/tests/features/smtp/send/send_reply.feature
+++ b/tests/features/smtp/send/send_reply.feature
@@ -3,11 +3,13 @@ Feature: SMTP send reply
Background:
Given there exists an account with username "[user:user1]" and password "password"
And there exists an account with username "[user:user2]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user1]" and password "password"
And user "[user:user1]" finishes syncing
And user "[user:user1]" connects and authenticates SMTP client "1"
And user "[user:user1]" connects and authenticates IMAP client "1"
+ Then it succeeds
@long-black
Scenario: Reply with In-Reply-To but no References
diff --git a/tests/features/smtp/send/two_messages.feature b/tests/features/smtp/send/two_messages.feature
index 509c848c..e484a566 100644
--- a/tests/features/smtp/send/two_messages.feature
+++ b/tests/features/smtp/send/two_messages.feature
@@ -4,10 +4,12 @@ Feature: SMTP sending two messages
And there exists an account with username "[user:multi]" and password "password"
And the account "[user:multi]" has additional address "[user:multi-alias]@[domain]"
And there exists an account with username "[user:to]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And the user logs in with username "[user:multi]" and password "password"
And the user sets the address mode of user "[user:multi]" to "split"
+ Then it succeeds
Scenario: Send two messages in one connection
When user "[user:user]" connects and authenticates SMTP client "1"
diff --git a/tests/features/user/addressmode.feature b/tests/features/user/addressmode.feature
index 714e2e3c..026c3205 100644
--- a/tests/features/user/addressmode.feature
+++ b/tests/features/user/addressmode.feature
@@ -14,9 +14,11 @@ Feature: Address mode
| from | to | subject | unread |
| c@[domain] | c@[domain] | three | true |
| d@[domain] | d@[domain] | four | false |
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
+ Then it succeeds
Scenario: The user is in combined mode
When user "[user:user]" connects and authenticates IMAP client "1" with address "[user:user]@[domain]"
diff --git a/tests/features/user/delete.feature b/tests/features/user/delete.feature
index e4027df4..2d586a79 100644
--- a/tests/features/user/delete.feature
+++ b/tests/features/user/delete.feature
@@ -1,8 +1,10 @@
Feature: A user can be deleted
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
+ Then it succeeds
Scenario: Delete a connected user
When user "[user:user]" is deleted
diff --git a/tests/features/user/login.feature b/tests/features/user/login.feature
index 1e512431..e38d74fc 100644
--- a/tests/features/user/login.feature
+++ b/tests/features/user/login.feature
@@ -1,9 +1,11 @@
Feature: A user can login
Background:
Given there exists an account with username "[user:user]" and password "password"
- Given there exists an account with username "[user:MixedCaps]" and password "password"
- Given there exists a disabled account with username "[user:disabled]" and password "password"
+ And there exists an account with username "[user:MixedCaps]" and password "password"
+ And there exists a disabled account with username "[user:disabled]" and password "password"
+ Then it succeeds
And bridge starts
+ Then it succeeds
Scenario: Login to account
When the user logs in with username "[user:user]" and password "password"
diff --git a/tests/features/user/relogin.feature b/tests/features/user/relogin.feature
index 021a6696..cf1b5a9e 100644
--- a/tests/features/user/relogin.feature
+++ b/tests/features/user/relogin.feature
@@ -1,8 +1,10 @@
Feature: A logged out user can login again
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
+ Then it succeeds
Scenario: Login to disconnected account
When user "[user:user]" logs out
@@ -12,4 +14,13 @@ Feature: A logged out user can login again
Scenario: Cannot login to removed account
When user "[user:user]" is deleted
- Then user "[user:user]" is not listed
\ No newline at end of file
+ Then user "[user:user]" is not listed
+
+ Scenario: Bridge password persists after logout/login
+ Given there exists an account with username "testUser" and password "password"
+ And the user logs in with username "testUser" and password "password"
+ And the bridge password of user "testUser" is changed to "YnJpZGdlcGFzc3dvcmQK"
+ And user "testUser" is deleted
+ And the user logs in with username "testUser" and password "password"
+ Then user "testUser" is eventually listed and connected
+ And the bridge password of user "testUser" is equal to "YnJpZGdlcGFzc3dvcmQK"
diff --git a/tests/features/user/revoke.feature b/tests/features/user/revoke.feature
index f0d59055..1eb984ac 100644
--- a/tests/features/user/revoke.feature
+++ b/tests/features/user/revoke.feature
@@ -1,8 +1,10 @@
Feature: A logged in user is logged out when its auth is revoked.
Background:
Given there exists an account with username "[user:user]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
And the user logs in with username "[user:user]" and password "password"
+ Then it succeeds
Scenario: The auth is revoked while bridge is running
When the auth of user "[user:user]" is revoked
diff --git a/tests/features/user/sync.feature b/tests/features/user/sync.feature
index 260eea3d..a939c1b8 100644
--- a/tests/features/user/sync.feature
+++ b/tests/features/user/sync.feature
@@ -14,7 +14,9 @@ Feature: Bridge can fully sync an account
| from | to | subject | unread |
| a@[domain] | a@[domain] | one | true |
| b@[domain] | b@[domain] | two | false |
- And bridge starts
+ Then it succeeds
+ When bridge starts
+ Then it succeeds
Scenario: The account is synced when the user logs in and persists across bridge restarts
When the user logs in with username "[user:user]" and password "password"
diff --git a/tests/features/user/telemetry.feature b/tests/features/user/telemetry.feature
index ff98029b..d1120acc 100644
--- a/tests/features/user/telemetry.feature
+++ b/tests/features/user/telemetry.feature
@@ -2,7 +2,9 @@ Feature: Bridge send usage metrics
Background:
Given there exists an account with username "[user:user1]" and password "password"
And there exists an account with username "[user:user2]" and password "password"
- And bridge starts
+ Then it succeeds
+ When bridge starts
+ Then it succeeds
Scenario: Telemetry availability - No user
diff --git a/tests/imap_test.go b/tests/imap_test.go
index 155b6e37..ab17cddc 100644
--- a/tests/imap_test.go
+++ b/tests/imap_test.go
@@ -470,7 +470,7 @@ func (s *scenario) imapClientAppendsToMailbox(clientID string, file, mailbox str
return nil
}
-func (s *scenario) imapClientsMoveMessageWithSubjectUserFromToByOrderedOperations(sourceIMAPClient, targetIMAPClient, messageSubject, bddUserID, targetMailboxName, op1, op2, op3 string) error {
+func (s *scenario) imapClientsMoveMessageWithSubjectUserFromToByOrderedOperations(sourceIMAPClient, targetIMAPClient, messageSubject, _, targetMailboxName, op1, op2, op3 string) error {
// call NOOP to prevent unilateral updates in following FETCH
_, sourceClient := s.t.getIMAPClient(sourceIMAPClient)
_, targetClient := s.t.getIMAPClient(targetIMAPClient)
diff --git a/tests/user_test.go b/tests/user_test.go
index 17542987..a6b31636 100644
--- a/tests/user_test.go
+++ b/tests/user_test.go
@@ -28,6 +28,9 @@ import (
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
+ "github.com/ProtonMail/proton-bridge/v3/internal/events"
+ "github.com/ProtonMail/proton-bridge/v3/internal/vault"
+ "github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/bradenaw/juniper/iterator"
"github.com/bradenaw/juniper/xslices"
"github.com/cucumber/godog"
@@ -329,10 +332,26 @@ func (s *scenario) drafAtIndexWasMovedToTrashForAddressOfAccount(draftIndex int,
}
func (s *scenario) userLogsInWithUsernameAndPassword(username, password string) error {
+ smtpEvtCh, cancelSMTP := s.t.bridge.GetEvents(events.SMTPServerReady{})
+ defer cancelSMTP()
+ imapEvtCh, cancelIMAP := s.t.bridge.GetEvents(events.IMAPServerReady{})
+ defer cancelIMAP()
+
userID, err := s.t.bridge.LoginFull(context.Background(), username, []byte(password), nil, nil)
if err != nil {
s.t.pushError(err)
} else {
+ // We need to wait for server to be up or we won't be able to connect. It should only happen once to avoid
+ // blocking on multiple Logins.
+ if !s.t.imapServerStarted {
+ <-imapEvtCh
+ s.t.imapServerStarted = true
+ }
+ if !s.t.smtpServerStarted {
+ <-smtpEvtCh
+ s.t.smtpServerStarted = true
+ }
+
if userID != s.t.getUserByName(username).getUserID() {
return errors.New("user ID mismatch")
}
@@ -426,6 +445,37 @@ func (s *scenario) userHasTelemetrySetTo(username string, telemetry int) error {
})
}
+func (s *scenario) bridgePasswordOfUserIsChangedTo(username, bridgePassword string) error {
+ b, err := algo.B64RawDecode([]byte(bridgePassword))
+ if err != nil {
+ return errors.New("the password is not base64 encoded")
+ }
+
+ var setErr error
+ if err := s.t.vault.GetUser(
+ s.t.getUserByName(username).getUserID(),
+ func(user *vault.User) { setErr = user.SetBridgePass(b) },
+ ); err != nil {
+ return err
+ }
+
+ return setErr
+}
+
+func (s *scenario) bridgePasswordOfUserIsEqualTo(username, bridgePassword string) error {
+ userInfo, err := s.t.bridge.QueryUserInfo(username)
+ if err != nil {
+ return err
+ }
+
+ readPassword := string(userInfo.BridgePass)
+ if readPassword != bridgePassword {
+ return fmt.Errorf("bridge password mismatch, expected '%v', got '%v'", bridgePassword, readPassword)
+ }
+
+ return nil
+}
+
func (s *scenario) addAdditionalAddressToAccount(username, address string, disabled bool) error {
userID := s.t.getUserByName(username).getUserID()