1
0

chore: merge release/Rialto into devel

This commit is contained in:
Jakub
2023-05-23 15:53:29 +02:00
144 changed files with 2947 additions and 1145 deletions

View File

@ -16,7 +16,7 @@
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. # along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
--- ---
image: harbor.protontech.ch/docker.io/library/golang:1.18 image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
variables: variables:
GOPRIVATE: gitlab.protontech.ch GOPRIVATE: gitlab.protontech.ch
@ -99,7 +99,7 @@ test-linux:
- .rules-branch-manual-MR-and-devel-always - .rules-branch-manual-MR-and-devel-always
- .after-script-code-coverage - .after-script-code-coverage
tags: tags:
- medium - large
test-linux-race: test-linux-race:
extends: extends:
@ -126,10 +126,10 @@ test-integration-race:
.windows-base: .windows-base:
before_script: before_script:
- export GOROOT=/c/Go1.18 - export GOROOT=/c/Go1.20
- export PATH=$GOROOT/bin:$PATH - export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64 - export GOARCH=amd64
- export GOPATH=~/go18 - export GOPATH=~/go1.20
- export GO111MODULE=on - export GO111MODULE=on
- export PATH=$GOPATH/bin:$PATH - export PATH=$GOPATH/bin:$PATH
- export MSYSTEM= - export MSYSTEM=
@ -172,7 +172,7 @@ test-windows:
.linux-build-setup: .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: variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache: cache:
@ -209,7 +209,7 @@ build-linux-qa:
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH - export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/go@1.13/bin:$PATH - export PATH=/usr/local/opt/go@1.13/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH - export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOPATH=~/go - export GOPATH=~/go1.20
- export PATH=$GOPATH/bin:$PATH - export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header' - 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" - $(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: .windows-build-setup:
before_script: before_script:
- export GOROOT=/c/Go1.18/ - export GOROOT=/c/Go1.20/
- export PATH=$GOROOT/bin:$PATH - export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64 - export GOARCH=amd64
- export GOPATH=~/go18 - export GOPATH=~/go1.20
- export GO111MODULE=on - export GO111MODULE=on
- export PATH="${GOPATH}/bin:${PATH}" - export PATH="${GOPATH}/bin:${PATH}"
- export MSYSTEM= - export MSYSTEM=

View File

@ -48,16 +48,13 @@ linters:
disable-all: true disable-all: true
enable: 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] - 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] - 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] - 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] - 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] - 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] - 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] - 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] - 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] - 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] - 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] # - 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] # - 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] # - 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.

View File

@ -3,7 +3,7 @@
## Prerequisites ## Prerequisites
* 64-bit OS: * 64-bit OS:
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes - 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, ... * 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/) - For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (Linux), msvc (Windows) or Xcode (macOS) * GCC (Linux), msvc (Windows) or Xcode (macOS)

View File

@ -183,7 +183,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
## Dev dependencies ## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks .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" LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated 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 ./utils/missing_license.sh change-year
test: gofiles 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 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 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 test-integration-debug: gofiles
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1 dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1

2
extern/vcpkg vendored

34
go.mod
View File

@ -1,18 +1,18 @@
module github.com/ProtonMail/proton-bridge/v3 module github.com/ProtonMail/proton-bridge/v3
go 1.18 go 1.20
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0 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-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/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 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/godog v0.12.5
github.com/cucumber/messages-go/v16 v16.0.1 github.com/cucumber/messages-go/v16 v16.0.1
github.com/docker/docker-credential-helpers v0.6.3 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/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.7.0 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/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.24.4 github.com/urfave/cli/v2 v2.24.4
github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1 go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.8.0 golang.org/x/net v0.10.0
golang.org/x/sys v0.6.0 golang.org/x/sys v0.8.0
golang.org/x/text v0.8.0 golang.org/x/text v0.9.0
google.golang.org/grpc v1.53.0 google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1 google.golang.org/protobuf v1.28.1
howett.net/plist v1.0.0 howett.net/plist v1.0.0
@ -55,17 +55,17 @@ require (
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
entgo.io/ent v0.11.8 // indirect entgo.io/ent v0.11.8 // indirect
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // 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-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/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/agext/levenshtein v1.2.3 // 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/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/bytedance/sonic v1.8.1 // indirect github.com/bytedance/sonic v1.8.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chzyer/test v1.0.0 // 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/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect github.com/cronokirby/saferith v0.33.0 // indirect
github.com/cucumber/gherkin-go/v19 v19.0.3 // 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/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/go-windows v1.0.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-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/felixge/fgprof v0.9.3 // indirect
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gin-contrib/sse v0.1.0 // 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/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zclconf/go-cty v1.12.1 // indirect github.com/zclconf/go-cty v1.12.1 // indirect
golang.org/x/arch v0.2.0 // 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/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 golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
@ -127,5 +127,5 @@ require (
replace ( replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 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/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
) )

63
go.sum
View File

@ -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/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 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= 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.20230523090642-633e61ce9bc2 h1:EFmaapQ2BM5OZ16+/c03108+wAt5nq1m/eCzHMl2Vg4=
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/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 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-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-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800 h1:o8/VQLSiuRkkSAfVOpFCG1GnTsWxFIOPLvJ2O7hJcFg=
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/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 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= 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 h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= 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.20230523092337-ea8de5f674b7 h1:LL+cERFLR5m3AKr6G58AVpsSuQQXulYf9WWWJ+2HUkY=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28= 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.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs= 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 h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= 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/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 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY= 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.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 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 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= 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/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 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/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.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
github.com/bradenaw/juniper v0.10.2/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI= 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/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.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4= 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/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw= 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/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/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 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.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 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= 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-20230517073537-fc1740a83768 h1:Jrcoxtrk4qpuzKIYPlEkjIK0M+bABs0oW2QzrOuwlzk=
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/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.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= 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-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 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= 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-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
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/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 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 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 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/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/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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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 h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 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-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-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-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.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.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-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-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-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-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-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-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 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-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/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= 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.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.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.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.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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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.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.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-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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-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.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.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.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-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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 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.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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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.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.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.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.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-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/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= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -19,14 +19,12 @@ package app
import ( import (
"fmt" "fmt"
"math/rand"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/async"
@ -160,9 +158,6 @@ func New() *cli.App {
} }
func run(c *cli.Context) error { 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. // Get the current bridge version.
version, err := semver.NewVersion(constants.Version) version, err := semver.NewVersion(constants.Version)
if err != nil { if err != nil {

View File

@ -29,7 +29,6 @@ import (
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/async"
imapEvents "github.com/ProtonMail/gluon/events" imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/imap" "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/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
"github.com/emersion/go-smtp"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -67,13 +65,7 @@ type Bridge struct {
tlsConfig *tls.Config tlsConfig *tls.Config
// imapServer is the bridge's IMAP server. // imapServer is the bridge's IMAP server.
imapServer *gluon.Server imapEventCh chan imapEvents.Event
imapListener net.Listener
imapEventCh chan imapEvents.Event
// smtpServer is the bridge's SMTP server.
smtpServer *smtp.Server
smtpListener net.Listener
// updater is the bridge's updater. // updater is the bridge's updater.
updater Updater updater Updater
@ -134,6 +126,8 @@ type Bridge struct {
goHeartbeat func() goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator uidValidityGenerator imap.UIDValidityGenerator
serverManager *ServerManager
} }
// New creates a new bridge. // New creates a new bridge.
@ -224,16 +218,6 @@ func newBridge(
return nil, fmt.Errorf("failed to load TLS config: %w", err) 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() firstStart := vault.GetFirstStart()
if err := vault.SetFirstStart(false); err != nil { if err := vault.SetFirstStart(false); err != nil {
return nil, fmt.Errorf("failed to save first start indicator: %w", err) return nil, fmt.Errorf("failed to save first start indicator: %w", err)
@ -246,23 +230,6 @@ func newBridge(
identifier.SetClientString(vault.GetLastUserAgent()) 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) focusService, err := focus.NewService(locator, curVersion, panicHandler)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create focus service: %w", err) return nil, fmt.Errorf("failed to create focus service: %w", err)
@ -279,7 +246,6 @@ func newBridge(
identifier: identifier, identifier: identifier,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
imapServer: imapServer,
imapEventCh: imapEventCh, imapEventCh: imapEventCh,
updater: updater, updater: updater,
@ -306,9 +272,13 @@ func newBridge(
tasks: tasks, tasks: tasks,
uidValidityGenerator: uidValidityGenerator, uidValidityGenerator: uidValidityGenerator,
serverManager: newServerManager(),
} }
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP) if err := bridge.serverManager.Init(bridge); err != nil {
return nil, err
}
return bridge, nil 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. // Attempt to load users from the vault when triggered.
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) { bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
if err := bridge.loadUsers(ctx); err != nil { if err := bridge.loadUsers(ctx); err != nil {
@ -396,17 +362,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
} }
bridge.publish(events.AllUsersLoaded{}) 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() defer bridge.goLoad()
@ -452,18 +407,13 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) { func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge") logrus.Info("Closing bridge")
// Close the IMAP server. // Close the servers
if err := bridge.closeIMAP(ctx); err != nil { if err := bridge.serverManager.CloseServers(ctx); err != nil {
logrus.WithError(err).Error("Failed to close IMAP server") logrus.WithError(err).Error("Failed to close servers")
}
// Close the SMTP server.
if err := bridge.closeSMTP(); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
} }
// Close all users. // Close all users.
safe.RLock(func() { safe.Lock(func() {
for _, user := range bridge.users { for _, user := range bridge.users {
user.Close() user.Close()
} }

View File

@ -50,7 +50,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/tests" "github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id" imapid "github.com/emersion/go-imap-id"
"github.com/emersion/go-imap/client"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -173,11 +172,27 @@ func TestBridge_UserAgent(t *testing.T) {
func TestBridge_UserAgent_Persistence(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) { 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) { 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() currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, vault.DefaultUserAgent) 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) require.NoError(t, err)
defer func() { _ = imapClient.Logout() }() defer func() { _ = imapClient.Logout() }()
@ -220,8 +235,24 @@ func TestBridge_UserAgentFromIMAPID(t *testing.T) {
calls = append(calls, call) 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) { 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) require.NoError(t, err)
defer func() { _ = imapClient.Logout() }() defer func() { _ = imapClient.Logout() }()
@ -592,10 +623,22 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
func TestBridge_LoginFailed(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) { 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) { 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{})) failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
defer done() 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.NoError(t, err)
require.Error(t, imapClient.Login("badUser", "badPass")) require.Error(t, imapClient.Login("badUser", "badPass"))
@ -622,6 +665,12 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
configDir, err := b.GetGluonDataDir() configDir, err := b.GetGluonDataDir()
require.NoError(t, err) require.NoError(t, err)
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
// Login the user. // Login the user.
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -655,7 +704,10 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()
@ -695,7 +747,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()
@ -716,7 +768,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()
@ -778,6 +830,7 @@ func withBridgeNoMocks(
locator bridge.Locator, locator bridge.Locator,
vaultKey []byte, vaultKey []byte,
tests func(*bridge.Bridge), tests func(*bridge.Bridge),
waitOnServers bool,
) { ) {
// Bridge will disable the proxy by default at startup. // Bridge will disable the proxy by default at startup.
mocks.ProxyCtl.EXPECT().DisallowProxy() mocks.ProxyCtl.EXPECT().DisallowProxy()
@ -828,14 +881,17 @@ func withBridgeNoMocks(
// Wait for bridge to finish loading users. // Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{}) 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. // Set random IMAP and SMTP ports for the tests.
require.NoError(t, bridge.SetIMAPPort(0)) require.NoError(t, bridge.SetIMAPPort(ctx, 0))
require.NoError(t, bridge.SetSMTPPort(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. // Close the bridge when done.
defer bridge.Close(ctx) defer bridge.Close(ctx)
@ -857,7 +913,24 @@ func withBridge(
withMocks(t, func(mocks *bridge.Mocks) { withMocks(t, func(mocks *bridge.Mocks) {
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) { withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
tests(bridge, mocks) 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 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,
}
}

View File

@ -18,6 +18,7 @@
package bridge package bridge
import ( import (
"context"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig" "github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
@ -31,7 +32,7 @@ import (
// ConfigureAppleMail configures apple mail for the given userID and address. // ConfigureAppleMail configures apple mail for the given userID and address.
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL. // 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{ logrus.WithFields(logrus.Fields{
"userID": userID, "userID": userID,
"address": logging.Sensitive(address), "address": logging.Sensitive(address),
@ -56,7 +57,7 @@ func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
} }
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() { if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
if err := bridge.SetSMTPSSL(true); err != nil { if err := bridge.SetSMTPSSL(ctx, true); err != nil {
return err return err
} }
} }

View File

@ -58,11 +58,7 @@ func moveFile(from, to string) error {
return err return err
} }
if err := os.Rename(from, to); err != nil { return os.Rename(from, to)
return err
}
return nil
} }
func copyDir(from, to string) error { func copyDir(from, to string) error {

View File

@ -20,7 +20,6 @@ package bridge
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@ -37,203 +36,21 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func (bridge *Bridge) serveIMAP() error { func (bridge *Bridge) restartIMAP(ctx context.Context) error {
port, err := func() (int, error) { return bridge.serverManager.RestartIMAP(ctx)
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
} }
// addIMAPUser connects the given user to gluon. // addIMAPUser connects the given user to gluon.
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error { func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
if bridge.imapServer == nil { return bridge.serverManager.AddIMAPUser(ctx, user)
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
} }
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files. // 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 { func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
if bridge.imapServer == nil { return bridge.serverManager.RemoveIMAPUser(ctx, user, withData)
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
} }
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) { func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
@ -262,19 +79,12 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"sessionID": event.SessionID, "sessionID": event.SessionID,
"username": event.Username, "username": event.Username,
}).Info("Received IMAP login failure notification") "pkg": "imap",
}).Error("Incorrect login credentials.")
bridge.publish(events.IMAPLoginFailed{Username: event.Username}) 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 { func ApplyGluonCachePathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "store") return filepath.Join(basePath, "backend", "store")
} }

View File

@ -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() testUpdater.lock.RLock()
defer testUpdater.lock.RUnlock() defer testUpdater.lock.RUnlock()
return testUpdater.latest, nil 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 return nil
} }

View File

@ -28,7 +28,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/bradenaw/juniper/iterator" "github.com/bradenaw/juniper/iterator"
"github.com/emersion/go-imap/client"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -66,7 +65,7 @@ func TestBridge_Refresh(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()
@ -99,7 +98,7 @@ func TestBridge_Refresh(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()

View File

@ -34,7 +34,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -46,12 +45,17 @@ func TestBridge_Send(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { 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) senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil) recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
smtpWaiter.Wait()
senderInfo, err := bridge.GetUserInfo(senderUserID) senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err) require.NoError(t, err)
@ -91,13 +95,13 @@ func TestBridge_Send(t *testing.T) {
} }
// Connect the sender IMAP client. // 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, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass))) require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client. // 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, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass))) require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck defer recipientIMAPClient.Logout() //nolint:errcheck
@ -135,13 +139,13 @@ func TestBridge_SendDraftFlags(t *testing.T) {
}) })
// Start the bridge. // 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. // Get the sender user info.
userInfo, err := bridge.QueryUserInfo(username) userInfo, err := bridge.QueryUserInfo(username)
require.NoError(t, err) require.NoError(t, err)
// Connect the sender IMAP client. // 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, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass))) require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck defer imapClient.Logout() //nolint:errcheck
@ -245,13 +249,13 @@ func TestBridge_SendInvite(t *testing.T) {
}) })
// Start the bridge. // 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. // Get the sender user info.
userInfo, err := bridge.QueryUserInfo(username) userInfo, err := bridge.QueryUserInfo(username)
require.NoError(t, err) require.NoError(t, err)
// Connect the sender IMAP client. // 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, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass))) require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck defer imapClient.Logout() //nolint:errcheck
@ -401,6 +405,9 @@ SGVsbG8gd29ybGQK
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { 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) senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -420,6 +427,8 @@ SGVsbG8gd29ybGQK
messageMultipartWithoutTextWithTextAttachment, messageMultipartWithoutTextWithTextAttachment,
} }
smtpWaiter.Wait()
for _, m := range messages { for _, m := range messages {
// Dial the server. // Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort()))) client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
@ -444,13 +453,13 @@ SGVsbG8gd29ybGQK
} }
// Connect the sender IMAP client. // 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, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass))) require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client. // 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, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass))) require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck defer recipientIMAPClient.Logout() //nolint:errcheck

View File

@ -36,6 +36,9 @@ import (
func TestBridge_Report(t *testing.T) { func TestBridge_Report(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { 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) { 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{})) syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -51,6 +54,8 @@ func TestBridge_Report(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) require.True(t, info.State == bridge.Connected)
imapWaiter.Wait()
// Dial the IMAP port. // Dial the IMAP port.
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err) require.NoError(t, err)

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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()
})
})
}

View File

@ -22,7 +22,6 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"path/filepath"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
@ -55,7 +54,7 @@ func (bridge *Bridge) GetIMAPPort() int {
return bridge.vault.GetIMAPPort() 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() { if newPort == bridge.vault.GetIMAPPort() {
return nil return nil
} }
@ -66,14 +65,14 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error {
bridge.heartbeat.SetIMAPPort(newPort) bridge.heartbeat.SetIMAPPort(newPort)
return bridge.restartIMAP() return bridge.restartIMAP(ctx)
} }
func (bridge *Bridge) GetIMAPSSL() bool { func (bridge *Bridge) GetIMAPSSL() bool {
return bridge.vault.GetIMAPSSL() 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() { if newSSL == bridge.vault.GetIMAPSSL() {
return nil return nil
} }
@ -84,14 +83,14 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
bridge.heartbeat.SetIMAPConnectionMode(newSSL) bridge.heartbeat.SetIMAPConnectionMode(newSSL)
return bridge.restartIMAP() return bridge.restartIMAP(ctx)
} }
func (bridge *Bridge) GetSMTPPort() int { func (bridge *Bridge) GetSMTPPort() int {
return bridge.vault.GetSMTPPort() 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() { if newPort == bridge.vault.GetSMTPPort() {
return nil return nil
} }
@ -102,14 +101,14 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error {
bridge.heartbeat.SetSMTPPort(newPort) bridge.heartbeat.SetSMTPPort(newPort)
return bridge.restartSMTP() return bridge.restartSMTP(ctx)
} }
func (bridge *Bridge) GetSMTPSSL() bool { func (bridge *Bridge) GetSMTPSSL() bool {
return bridge.vault.GetSMTPSSL() 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() { if newSSL == bridge.vault.GetSMTPSSL() {
return nil return nil
} }
@ -120,7 +119,7 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
bridge.heartbeat.SetSMTPConnectionMode(newSSL) bridge.heartbeat.SetSMTPConnectionMode(newSSL)
return bridge.restartSMTP() return bridge.restartSMTP(ctx)
} }
func (bridge *Bridge) GetGluonCacheDir() string { 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 { func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
return safe.RLockRet(func() error { return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
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)
} }
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error { func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {

View File

@ -57,7 +57,7 @@ func TestBridge_Settings_IMAPPort(t *testing.T) {
curPort := bridge.GetIMAPPort() curPort := bridge.GetIMAPPort()
// Set the port to 1144. // Set the port to 1144.
require.NoError(t, bridge.SetIMAPPort(1144)) require.NoError(t, bridge.SetIMAPPort(ctx, 1144))
// Get the new setting. // Get the new setting.
require.Equal(t, 1144, bridge.GetIMAPPort()) require.Equal(t, 1144, bridge.GetIMAPPort())
@ -75,7 +75,7 @@ func TestBridge_Settings_IMAPSSL(t *testing.T) {
require.False(t, bridge.GetIMAPSSL()) require.False(t, bridge.GetIMAPSSL())
// Enable IMAP SSL. // Enable IMAP SSL.
require.NoError(t, bridge.SetIMAPSSL(true)) require.NoError(t, bridge.SetIMAPSSL(ctx, true))
// Get the new setting. // Get the new setting.
require.True(t, bridge.GetIMAPSSL()) require.True(t, bridge.GetIMAPSSL())
@ -89,7 +89,7 @@ func TestBridge_Settings_SMTPPort(t *testing.T) {
curPort := bridge.GetSMTPPort() curPort := bridge.GetSMTPPort()
// Set the port to 1024. // Set the port to 1024.
require.NoError(t, bridge.SetSMTPPort(1024)) require.NoError(t, bridge.SetSMTPPort(ctx, 1024))
// Get the new setting. // Get the new setting.
require.Equal(t, 1024, bridge.GetSMTPPort()) require.Equal(t, 1024, bridge.GetSMTPPort())
@ -107,7 +107,7 @@ func TestBridge_Settings_SMTPSSL(t *testing.T) {
require.False(t, bridge.GetSMTPSSL()) require.False(t, bridge.GetSMTPSSL())
// Enable SMTP SSL. // Enable SMTP SSL.
require.NoError(t, bridge.SetSMTPSSL(true)) require.NoError(t, bridge.SetSMTPSSL(ctx, true))
// Get the new setting. // Get the new setting.
require.True(t, bridge.GetSMTPSSL()) require.True(t, bridge.GetSMTPSSL())

View File

@ -20,93 +20,16 @@ package bridge
import ( import (
"context" "context"
"crypto/tls" "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/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func (bridge *Bridge) serveSMTP() error { func (bridge *Bridge) restartSMTP(ctx context.Context) error {
port, err := func() (int, error) { return bridge.serverManager.RestartSMTP(ctx)
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 newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server { func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {

View File

@ -58,6 +58,11 @@ func (s *smtpSession) AuthPlain(username, password string) error {
return nil return nil
} }
logrus.WithFields(logrus.Fields{
"username": username,
"pkg": "smtp",
}).Error("Incorrect login credentials.")
return fmt.Errorf("invalid username or password") return fmt.Errorf("invalid username or password")
}, s.usersLock) }, s.usersLock)
} }
@ -72,7 +77,7 @@ func (s *smtpSession) Logout() error {
return nil return nil
} }
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error { func (s *smtpSession) Mail(from string, _ *smtp.MailOptions) error {
s.from = from s.from = from
return nil return nil
} }

View File

@ -80,7 +80,7 @@ func TestBridge_Sync(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()
@ -112,15 +112,6 @@ func TestBridge_Sync(t *testing.T) {
info, err := b.GetUserInfo(userID) info, err := b.GetUserInfo(userID)
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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. // Remove the network limit, allowing the sync to finish.
@ -136,7 +127,7 @@ func TestBridge_Sync(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()
@ -187,7 +178,7 @@ func _TestBridge_Sync_BadMessage(t *testing.T) { //nolint:unused,deadcode
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()
@ -273,15 +264,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
info, err := b.GetUserInfo(userID) info, err := b.GetUserInfo(userID)
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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 // 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.NoError(t, err)
require.True(t, info.State == bridge.Connected) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()

View File

@ -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 <https://www.gnu.org/licenses/>.
//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))
}

View File

@ -584,29 +584,7 @@ func (bridge *Bridge) newVaultUser(
authUID, authRef string, authUID, authRef string,
saltedKeyPass []byte, saltedKeyPass []byte,
) (*vault.User, bool, error) { ) (*vault.User, bool, error) {
if !bridge.vault.HasUser(apiUser.ID) { return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
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
} }
// logout logs out the given user, optionally logging them out from the API too. // logout logs out the given user, optionally logging them out from the API too.

View File

@ -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) { 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) userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string var messageIDs []string
@ -176,6 +179,8 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
userFeedback(t, ctx, bridge, badUserID) userFeedback(t, ctx, bridge, badUserID)
smtpWaiter.Wait()
userContinueEventProcess(ctx, t, s, bridge) 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) { 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) userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string var messageIDs []string
@ -217,6 +225,7 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
require.NoError(t, c.DeleteMessage(ctx, messageIDs...)) require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
}) })
smtpWaiter.Wait()
userContinueEventProcess(ctx, t, s, bridge) 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) { 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) userLoginAndSync(ctx, t, bridge, "user", password)
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes() 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) 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") info, err := bridge.QueryUserInfo("user")
require.NoError(t, err) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = cli.Logout() }()
// The IMAP client will eventually see 20 messages. // The IMAP client will eventually see 20 messages.
require.Eventually(t, func() bool { 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 return err == nil && status.Messages == 20
}, 10*time.Second, 100*time.Millisecond) }, 10*time.Second, 100*time.Millisecond)
}) })
@ -638,12 +645,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
info, err := bridge.QueryUserInfo("user") info, err := bridge.QueryUserInfo("user")
require.NoError(t, err) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = cli.Logout() }()
messages, err := clientFetch(client, "Drafts") messages, err := clientFetch(cli, "Drafts")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, messages, 1) require.Len(t, messages, 1)
require.Contains(t, messages[0].Flags, imap.DraftFlag) require.Contains(t, messages[0].Flags, imap.DraftFlag)
@ -677,12 +684,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
info, err := bridge.QueryUserInfo("user") info, err := bridge.QueryUserInfo("user")
require.NoError(t, err) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = cli.Logout() }()
messages, err := clientFetch(client, "Sent") messages, err := clientFetch(cli, "Sent")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, messages, 1) require.Len(t, messages, 1)
require.NotContains(t, messages[0].Flags, imap.DraftFlag) 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) { func TestBridge_User_HandleParentLabelRename(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { 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) { 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))) require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
info, err := bridge.QueryUserInfo(username) info, err := bridge.QueryUserInfo(username)
require.NoError(t, err) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = cli.Logout() }()
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) { withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
parentName := uuid.NewString() parentName := uuid.NewString()
@ -795,7 +811,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be created. // Wait for the parent folder to be created.
require.Eventually(t, func() bool { 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) return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
}) >= 0 }) >= 0
}, 100*user.EventPeriod, user.EventPeriod) }, 100*user.EventPeriod, user.EventPeriod)
@ -812,7 +828,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be created. // Wait for the parent folder to be created.
require.Eventually(t, func() bool { 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) return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
}) >= 0 }) >= 0
}, 100*user.EventPeriod, user.EventPeriod) }, 100*user.EventPeriod, user.EventPeriod)
@ -827,14 +843,14 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be renamed. // Wait for the parent folder to be renamed.
require.Eventually(t, func() bool { 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) return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName)
}) >= 0 }) >= 0
}, 100*user.EventPeriod, user.EventPeriod) }, 100*user.EventPeriod, user.EventPeriod)
// Wait for the child folder to be renamed. // Wait for the child folder to be renamed.
require.Eventually(t, func() bool { 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) return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
}) >= 0 }) >= 0
}, 100*user.EventPeriod, user.EventPeriod) }, 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. // userLoginAndSync logs in user and waits until user is fully synced.
func userLoginAndSync( func userLoginAndSync(
ctx context.Context, ctx context.Context,
@ -928,10 +902,10 @@ func userContinueEventProcess(
info, err := bridge.QueryUserInfo("user") info, err := bridge.QueryUserInfo("user")
require.NoError(t, err) 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, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = cli.Logout() }()
randomLabel := uuid.NewString() randomLabel := uuid.NewString()
@ -946,8 +920,21 @@ func userContinueEventProcess(
// Wait for the label to be created. // Wait for the label to be created.
require.Eventually(t, func() bool { 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 return mailbox.Name == "Labels/"+randomLabel
}) >= 0 }) >= 0
}, 100*user.EventPeriod, user.EventPeriod) }, 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)
}

View File

@ -75,11 +75,7 @@ func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.U
return nil return nil
} }
if bridge.imapServer == nil { gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
return fmt.Errorf("no imap server instance running")
}
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil { if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err) 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 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 { if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err) 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) 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) 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 return nil
} }
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
gluonID, ok := user.GetGluonID(event.AddressID) gluonID, ok := user.GetGluonID(event.AddressID)
if !ok { if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID) 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) return fmt.Errorf("failed to remove user from IMAP server: %w", err)
} }

View File

@ -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. // 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 return err
} }

View File

@ -50,7 +50,7 @@ int installTrustedCert(char const *bytes, unsigned long long length) {
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot], (id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
(id)kSecTrustSettingsPolicy: (__bridge id) policy, (id)kSecTrustSettingsPolicy: (__bridge id) policy,
}; };
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainAdmin, (__bridge CFTypeRef)(trustSettings)); status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy); CFRelease(policy);
CFRelease(cert); CFRelease(cert);
@ -72,7 +72,7 @@ int removeTrustedCert(char const *bytes, unsigned long long length) {
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified], (id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified],
(id)kSecTrustSettingsPolicy: (__bridge id) policy, (id)kSecTrustSettingsPolicy: (__bridge id) policy,
}; };
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainAdmin, (__bridge CFTypeRef)(trustSettings)); OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy); CFRelease(policy);
if (errSecSuccess != status) { if (errSecSuccess != status) {
CFRelease(cert); 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. // 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) { func certPEMToDER(certPEM []byte) ([]byte, error) {
block, left := pem.Decode(certPEM) block, left := pem.Decode(certPEM)
if block == nil { if block == nil {
return []byte{}, errors.New("invalid PEM certificate") return []byte{}, errors.New("invalid PEM certificate")
@ -127,7 +126,7 @@ func installCert(certPEM []byte) error {
} }
p := C.CBytes(certDER) 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))) errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER)))
switch errCode { switch errCode {
@ -147,7 +146,7 @@ func uninstallCert(certPEM []byte) error {
} }
p := C.CBytes(certDER) 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 { 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) return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)

View File

@ -26,7 +26,7 @@ import (
) )
// This test implies human interactions to enter password and is disabled by default. // 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() template, err := NewTLSTemplate()
require.NoError(t, err) require.NoError(t, err)

View File

@ -75,7 +75,7 @@ if(NOT UNIX)
set(CMAKE_INSTALL_BINDIR ".") set(CMAKE_INSTALL_BINDIR ".")
endif(NOT UNIX) 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() qt_standard_project_setup()
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
message(STATUS "Using Qt ${Qt6_VERSION}") message(STATUS "Using Qt ${Qt6_VERSION}")
@ -147,6 +147,7 @@ target_link_libraries(bridge-gui
Qt6::Quick Qt6::Quick
Qt6::Qml Qt6::Qml
Qt6::QuickControls2 Qt6::QuickControls2
Qt6::Svg
sentry::sentry sentry::sentry
bridgepp bridgepp
) )

View File

@ -25,6 +25,7 @@
#include <QtQml> #include <QtQml>
#include <QtWidgets> #include <QtWidgets>
#include <QtQuickControls2> #include <QtQuickControls2>
#include <QtSvg>
#include <AppController.h> #include <AppController.h>

View File

@ -994,15 +994,44 @@ void QMLBackend::onUserBadEvent(QString const &userID, QString const &) {
void QMLBackend::onIMAPLoginFailed(QString const &username) { void QMLBackend::onIMAPLoginFailed(QString const &username) {
HANDLE_EXCEPTION( HANDLE_EXCEPTION(
SPUser const user = users_->getUserWithUsernameOrEmail(username); 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; 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, 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" 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))); " 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(); emit showMainWindow();
) )
} }

View File

@ -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 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 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. 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 setNormalTrayIcon(); ///< Set the tray icon to normal.
void setErrorTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'error' state. 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. 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 hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu). void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
void showSettings(); ///< 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 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. void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.

View File

@ -49,7 +49,7 @@ QString sentryAttachmentFilePath() {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
QByteArray getProtectedHostname() { QByteArray getProtectedHostname() {
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256); QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
return hostname.toHex(); return hostname.toBase64();
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -22,6 +22,7 @@
#include <sentry.h> #include <sentry.h>
void initSentry(); void initSentry();
QByteArray getProtectedHostname();
void setSentryReportScope(); void setSentryReportScope();
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir); sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message); sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);

View File

@ -43,12 +43,75 @@ qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds duri
QIcon loadIconFromImage(QString const &path) { QIcon loadIconFromImage(QString const &path) {
QPixmap const pixmap(path); QPixmap const pixmap(path);
if (pixmap.isNull()) { 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); 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. /// \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 } // anonymous namespace
@ -103,17 +178,17 @@ QString stateText(TrayIcon::State state) {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
TrayIcon::TrayIcon() TrayIcon::TrayIcon()
: QSystemTrayIcon() : QSystemTrayIcon()
, menu_(new QMenu) { , menu_(new QMenu)
, notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) {
this->generateDotIcons(); this->generateDotIcons();
this->setContextMenu(menu_.get()); this->setContextMenu(menu_.get());
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow); connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser); connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser);
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated); 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->show();
this->setState(State::Normal, QString(), QString());
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler. // TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
for (QScreen *screen: QGuiApplication::screens()) { for (QScreen *screen: QGuiApplication::screens()) {
@ -151,7 +226,7 @@ void TrayIcon::onUserClicked() {
throw Exception("Could not retrieve context menu's selected user."); throw Exception("Could not retrieve context menu's selected user.");
} }
emit selectUser(userID); emit selectUser(userID, true);
} catch (Exception const &e) { } catch (Exception const &e) {
app().log().error(e.qwhat()); app().log().error(e.qwhat());
} }
@ -212,18 +287,17 @@ void TrayIcon::onIconRefreshTimer() {
// //
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void TrayIcon::generateDotIcons() { void TrayIcon::generateDotIcons() {
QPixmap dotSVG(":/qml/icons/ic-dot.svg"); QSvgRenderer dotSVG(QString(":/qml/icons/ic-dot.svg"));
struct IconColor { struct IconColor {
QIcon &icon; QIcon &icon;
QColor color; QColor color;
}; };
for (auto pair: QList<IconColor> {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) { for (auto pair: QList<IconColor> {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) {
QPixmap p = dotSVG; pair.icon = loadIconFromSVGRenderer(dotSVG, pair.color);
QPainter painter(&p); if (pair.icon.isNull()) {
painter.setCompositionMode(QPainter::CompositionMode_SourceIn); throw Exception("Could not generate dot icon from vector file.");
painter.fillRect(p.rect(), pair.color); }
painter.end();
pair.icon = QIcon(p);
} }
} }
@ -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] svgPath The path of the SVG file for the icon.
/// \param[in] color The color to apply to the icon. /// \param[in] color The color to apply to the icon.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) { void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) {
// We use the SVG path as pixmap mask and fill it with the appropriate color // We use the SVG path as pixmap mask and fill it with the appropriate color
QString resourcePath = svgPath; statusIcon_ = loadIconFromSVG(qmlResourcePathToQt(svgPath), color);
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);
} }
//********************************************************************************************************************** //****************************************************************************************************************************************************
// //
//********************************************************************************************************************** //****************************************************************************************************************************************************
void TrayIcon::refreshContextMenu() { void TrayIcon::refreshContextMenu() {
if (!menu_) { if (!menu_) {
app().log().error("Native tray icon context menu is null."); app().log().error("Native tray icon context menu is null.");
@ -297,3 +373,5 @@ void TrayIcon::refreshContextMenu() {
menu_->addSeparator(); menu_->addSeparator();
menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit); menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit);
} }

View File

@ -41,10 +41,10 @@ public: // data members
TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator. TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator.
TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move 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 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: 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: private slots:
void onMenuAboutToShow(); ///< Slot called before the context menu is shown. void onMenuAboutToShow(); ///< Slot called before the context menu is shown.
@ -67,6 +67,7 @@ private: // data members
QIcon greenDot_; ///< The green dot icon. QIcon greenDot_; ///< The green dot icon.
QIcon greyDot_; ///< The grey dot icon. QIcon greyDot_; ///< The grey dot icon.
QIcon orangeDot_; ///< The orange 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. QTimer iconRefreshTimer_; ///< The timer used to periodically refresh the icon when DPI changes.
QDateTime iconRefreshDeadline_; ///< The deadline for refreshing the icon QDateTime iconRefreshDeadline_; ///< The deadline for refreshing the icon

View File

@ -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 // 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. // these outputs and output them on the command-line.
log.setLevel(cliOptions.logLevel); log.setLevel(cliOptions.logLevel);
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
QString bridgeexec; QString bridgeexec;
if (!cliOptions.attach) { if (!cliOptions.attach) {
if (isBridgeRunning()) { if (isBridgeRunning()) {

View File

@ -95,9 +95,11 @@ ApplicationWindow {
root.showAndRise() root.showAndRise()
} }
function onSelectUser(userID) { function onSelectUser(userID, forceShowWindow) {
contentWrapper.selectUser(userID) contentWrapper.selectUser(userID)
root.showAndRise() if (forceShowWindow) {
root.showAndRise()
}
} }
} }

View File

@ -535,11 +535,12 @@ QtObject {
} }
property Notification onlyPaidUsers: Notification { 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") brief: qsTr("Upgrade your account")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
property var pricingLink: "https://proton.me/mail/pricing"
Connections { Connections {
target: Backend target: Backend
@ -550,8 +551,9 @@ QtObject {
action: [ action: [
Action { Action {
text: qsTr("OK") text: qsTr("Upgrade")
onTriggered: { onTriggered: {
Qt.openUrlExternally(root.onlyPaidUsers.pricingLink)
root.onlyPaidUsers.active = false root.onlyPaidUsers.active = false
} }
} }

View File

@ -344,7 +344,12 @@ FocusScope {
if (str.length === 0) { if (str.length === 0) {
return qsTr("Enter the 6-digit code") return qsTr("Enter the 6-digit code")
} }
return }
onTextChanged: {
if (text.length >= 6) {
twoFAButton.onClicked()
}
} }
onAccepted: { onAccepted: {

View File

@ -6,19 +6,19 @@
#include "focus.grpc.pb.h" #include "focus.grpc.pb.h"
#include <functional> #include <functional>
#include <grpcpp/impl/codegen/async_stream.h> #include <grpcpp/support/async_stream.h>
#include <grpcpp/impl/codegen/async_unary_call.h> #include <grpcpp/support/async_unary_call.h>
#include <grpcpp/impl/codegen/channel_interface.h> #include <grpcpp/impl/channel_interface.h>
#include <grpcpp/impl/codegen/client_unary_call.h> #include <grpcpp/impl/client_unary_call.h>
#include <grpcpp/impl/codegen/client_callback.h> #include <grpcpp/support/client_callback.h>
#include <grpcpp/impl/codegen/message_allocator.h> #include <grpcpp/support/message_allocator.h>
#include <grpcpp/impl/codegen/method_handler.h> #include <grpcpp/support/method_handler.h>
#include <grpcpp/impl/codegen/rpc_service_method.h> #include <grpcpp/impl/rpc_service_method.h>
#include <grpcpp/impl/codegen/server_callback.h> #include <grpcpp/support/server_callback.h>
#include <grpcpp/impl/codegen/server_callback_handlers.h> #include <grpcpp/impl/codegen/server_callback_handlers.h>
#include <grpcpp/impl/codegen/server_context.h> #include <grpcpp/server_context.h>
#include <grpcpp/impl/codegen/service_type.h> #include <grpcpp/impl/service_type.h>
#include <grpcpp/impl/codegen/sync_stream.h> #include <grpcpp/support/sync_stream.h>
namespace focus { namespace focus {
static const char* Focus_method_names[] = { static const char* Focus_method_names[] = {

View File

@ -25,23 +25,23 @@
#include "focus.pb.h" #include "focus.pb.h"
#include <functional> #include <functional>
#include <grpcpp/impl/codegen/async_generic_service.h> #include <grpcpp/generic/async_generic_service.h>
#include <grpcpp/impl/codegen/async_stream.h> #include <grpcpp/support/async_stream.h>
#include <grpcpp/impl/codegen/async_unary_call.h> #include <grpcpp/support/async_unary_call.h>
#include <grpcpp/impl/codegen/client_callback.h> #include <grpcpp/support/client_callback.h>
#include <grpcpp/impl/codegen/client_context.h> #include <grpcpp/client_context.h>
#include <grpcpp/impl/codegen/completion_queue.h> #include <grpcpp/completion_queue.h>
#include <grpcpp/impl/codegen/message_allocator.h> #include <grpcpp/support/message_allocator.h>
#include <grpcpp/impl/codegen/method_handler.h> #include <grpcpp/support/method_handler.h>
#include <grpcpp/impl/codegen/proto_utils.h> #include <grpcpp/impl/codegen/proto_utils.h>
#include <grpcpp/impl/codegen/rpc_method.h> #include <grpcpp/impl/rpc_method.h>
#include <grpcpp/impl/codegen/server_callback.h> #include <grpcpp/support/server_callback.h>
#include <grpcpp/impl/codegen/server_callback_handlers.h> #include <grpcpp/impl/codegen/server_callback_handlers.h>
#include <grpcpp/impl/codegen/server_context.h> #include <grpcpp/server_context.h>
#include <grpcpp/impl/codegen/service_type.h> #include <grpcpp/impl/service_type.h>
#include <grpcpp/impl/codegen/status.h> #include <grpcpp/impl/codegen/status.h>
#include <grpcpp/impl/codegen/stub_options.h> #include <grpcpp/support/stub_options.h>
#include <grpcpp/impl/codegen/sync_stream.h> #include <grpcpp/support/sync_stream.h>
namespace focus { namespace focus {

View File

@ -13,7 +13,7 @@
#error incompatible with your Protocol Buffer headers. Please update #error incompatible with your Protocol Buffer headers. Please update
#error your headers. #error your headers.
#endif #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 This file was generated by an older version of protoc which is
#error incompatible with your Protocol Buffer headers. Please #error incompatible with your Protocol Buffer headers. Please
#error regenerate this file with a newer version of protoc. #error regenerate this file with a newer version of protoc.

View File

@ -6,19 +6,19 @@
#include "bridge.grpc.pb.h" #include "bridge.grpc.pb.h"
#include <functional> #include <functional>
#include <grpcpp/impl/codegen/async_stream.h> #include <grpcpp/support/async_stream.h>
#include <grpcpp/impl/codegen/async_unary_call.h> #include <grpcpp/support/async_unary_call.h>
#include <grpcpp/impl/codegen/channel_interface.h> #include <grpcpp/impl/channel_interface.h>
#include <grpcpp/impl/codegen/client_unary_call.h> #include <grpcpp/impl/client_unary_call.h>
#include <grpcpp/impl/codegen/client_callback.h> #include <grpcpp/support/client_callback.h>
#include <grpcpp/impl/codegen/message_allocator.h> #include <grpcpp/support/message_allocator.h>
#include <grpcpp/impl/codegen/method_handler.h> #include <grpcpp/support/method_handler.h>
#include <grpcpp/impl/codegen/rpc_service_method.h> #include <grpcpp/impl/rpc_service_method.h>
#include <grpcpp/impl/codegen/server_callback.h> #include <grpcpp/support/server_callback.h>
#include <grpcpp/impl/codegen/server_callback_handlers.h> #include <grpcpp/impl/codegen/server_callback_handlers.h>
#include <grpcpp/impl/codegen/server_context.h> #include <grpcpp/server_context.h>
#include <grpcpp/impl/codegen/service_type.h> #include <grpcpp/impl/service_type.h>
#include <grpcpp/impl/codegen/sync_stream.h> #include <grpcpp/support/sync_stream.h>
namespace grpc { namespace grpc {
static const char* Bridge_method_names[] = { static const char* Bridge_method_names[] = {

View File

@ -25,23 +25,23 @@
#include "bridge.pb.h" #include "bridge.pb.h"
#include <functional> #include <functional>
#include <grpcpp/impl/codegen/async_generic_service.h> #include <grpcpp/generic/async_generic_service.h>
#include <grpcpp/impl/codegen/async_stream.h> #include <grpcpp/support/async_stream.h>
#include <grpcpp/impl/codegen/async_unary_call.h> #include <grpcpp/support/async_unary_call.h>
#include <grpcpp/impl/codegen/client_callback.h> #include <grpcpp/support/client_callback.h>
#include <grpcpp/impl/codegen/client_context.h> #include <grpcpp/client_context.h>
#include <grpcpp/impl/codegen/completion_queue.h> #include <grpcpp/completion_queue.h>
#include <grpcpp/impl/codegen/message_allocator.h> #include <grpcpp/support/message_allocator.h>
#include <grpcpp/impl/codegen/method_handler.h> #include <grpcpp/support/method_handler.h>
#include <grpcpp/impl/codegen/proto_utils.h> #include <grpcpp/impl/codegen/proto_utils.h>
#include <grpcpp/impl/codegen/rpc_method.h> #include <grpcpp/impl/rpc_method.h>
#include <grpcpp/impl/codegen/server_callback.h> #include <grpcpp/support/server_callback.h>
#include <grpcpp/impl/codegen/server_callback_handlers.h> #include <grpcpp/impl/codegen/server_callback_handlers.h>
#include <grpcpp/impl/codegen/server_context.h> #include <grpcpp/server_context.h>
#include <grpcpp/impl/codegen/service_type.h> #include <grpcpp/impl/service_type.h>
#include <grpcpp/impl/codegen/status.h> #include <grpcpp/impl/codegen/status.h>
#include <grpcpp/impl/codegen/stub_options.h> #include <grpcpp/support/stub_options.h>
#include <grpcpp/impl/codegen/sync_stream.h> #include <grpcpp/support/sync_stream.h>
namespace grpc { namespace grpc {

View File

@ -13,7 +13,7 @@
#error incompatible with your Protocol Buffer headers. Please update #error incompatible with your Protocol Buffer headers. Please update
#error your headers. #error your headers.
#endif #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 This file was generated by an older version of protoc which is
#error incompatible with your Protocol Buffer headers. Please #error incompatible with your Protocol Buffer headers. Please
#error regenerate this file with a newer version of protoc. #error regenerate this file with a newer version of protoc.

View File

@ -34,9 +34,7 @@ SPUser User::newUser(QObject *parent) {
/// \param[in] parent The parent object. /// \param[in] parent The parent object.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
User::User(QObject *parent) User::User(QObject *parent)
: QObject(parent) : QObject(parent) {
, imapFailureCooldownEndTime_(QDateTime::currentDateTime()) {
} }
@ -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 /// \param[in] durationMSecs The duration of the period in milliseconds.
/// 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.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void User::startImapLoginFailureCooldown(qint64 durationMSecs) { void User::startNotificationCooldownPeriod(User::ENotification notification, qint64 durationMSecs) {
imapFailureCooldownEndTime_ = QDateTime::currentDateTime().addMSecs(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 { bool User::isNotificationInCooldown(User::ENotification notification) const {
return QDateTime::currentDateTime() < imapFailureCooldownEndTime_; return notificationCooldownList_.contains(notification) && (QDateTime::currentDateTime() < notificationCooldownList_[notification]);
} }

View File

@ -62,6 +62,13 @@ typedef std::shared_ptr<class User> SPUser; ///< Type definition for shared poin
class User : public QObject { class User : public QObject {
Q_OBJECT 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 public: // static member function
static SPUser newUser(QObject *parent); ///< Create a new user static SPUser newUser(QObject *parent); ///< Create a new user
static QString stateToString(UserState state); ///< Return a string describing a user state. 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. User &operator=(User &&) = delete; ///< Disabled move assignment operator.
void update(User const &user); ///< Update the user. void update(User const &user); ///< Update the user.
Q_INVOKABLE QString primaryEmailOrUsername() const; ///< Return the user primary email, or, if unknown its username. 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. void startNotificationCooldownPeriod(ENotification notification, qint64 durationMSecs); ///< Start the user cooldown period for a notification.
bool isInIMAPLoginFailureCooldown() const; ///< Check if the user in a IMAP login failure notification. bool isNotificationInCooldown(ENotification notification) const; ///< Return true iff the notification is in a cooldown period.
public slots: public slots:
// slots for QML generated calls // slots for QML generated calls
@ -147,7 +154,7 @@ private: // member functions.
User(QObject *parent); ///< Default constructor. User(QObject *parent); ///< Default constructor.
private: // data members. private: // data members.
QDateTime imapFailureCooldownEndTime_; ///< The end date/time for the IMAP login failure notification cooldown period. QMap<ENotification, QDateTime> notificationCooldownList_; ///< A list of cooldown period end time for notifications.
QString id_; ///< The userID. QString id_; ///< The userID.
QString username_; ///< The username QString username_; ///< The username
QString password_; ///< The IMAP password of the user. QString password_; ///< The IMAP password of the user.

View File

@ -297,7 +297,7 @@ func (f *frontendCLI) configureAppleMail(c *ishell.Context) {
return 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) f.printAndLogError(err)
return 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]) 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) f.badEventFeedback(true)
} }
func (f *frontendCLI) badEventLogout(c *ishell.Context) { func (f *frontendCLI) badEventLogout(_ *ishell.Context) {
f.badEventFeedback(false) f.badEventFeedback(false)
} }

View File

@ -20,6 +20,7 @@ package cli
import ( import (
"errors" "errors"
"os"
"github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge" "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
@ -60,6 +61,11 @@ func New(
panicHandler: panicHandler, 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. // Clear commands.
clearCmd := &ishell.Cmd{ clearCmd := &ishell.Cmd{
Name: "clear", Name: "clear",

View File

@ -31,7 +31,7 @@ import (
"github.com/abiosoft/ishell" "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 { if path, err := f.bridge.GetLogsPath(); err != nil {
f.Println("Failed to determine location of log files") f.Println("Failed to determine location of log files")
} else { } 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") 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, ";") { for _, pkg := range strings.Split(bridge.Credits, ";") {
f.Println(pkg) f.Println(pkg)
} }
} }
func (f *frontendCLI) changeIMAPSecurity(c *ishell.Context) { func (f *frontendCLI) changeIMAPSecurity(_ *ishell.Context) {
f.ShowPrompt(false) f.ShowPrompt(false)
defer f.ShowPrompt(true) 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) msg := fmt.Sprintf("Are you sure you want to change IMAP setting to %q", newSecurity)
if f.yesNoQuestion(msg) { 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) f.printAndLogError(err)
return return
} }
} }
} }
func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) { func (f *frontendCLI) changeSMTPSecurity(_ *ishell.Context) {
f.ShowPrompt(false) f.ShowPrompt(false)
defer f.ShowPrompt(true) 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) msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q", newSecurity)
if f.yesNoQuestion(msg) { 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) f.printAndLogError(err)
return return
} }
@ -103,7 +103,7 @@ func (f *frontendCLI) changeIMAPPort(c *ishell.Context) {
return return
} }
if err := f.bridge.SetIMAPPort(newIMAPPortInt); err != nil { if err := f.bridge.SetIMAPPort(context.Background(), newIMAPPortInt); err != nil {
f.printAndLogError(err) f.printAndLogError(err)
return return
} }
@ -125,13 +125,13 @@ func (f *frontendCLI) changeSMTPPort(c *ishell.Context) {
return return
} }
if err := f.bridge.SetSMTPPort(newSMTPPortInt); err != nil { if err := f.bridge.SetSMTPPort(context.Background(), newSMTPPortInt); err != nil {
f.printAndLogError(err) f.printAndLogError(err)
return return
} }
} }
func (f *frontendCLI) allowProxy(c *ishell.Context) { func (f *frontendCLI) allowProxy(_ *ishell.Context) {
if f.bridge.GetProxyAllowed() { if f.bridge.GetProxyAllowed() {
f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.") f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.")
return 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() { if !f.bridge.GetProxyAllowed() {
f.Println("Bridge is already set to NOT use alternative routing to connect to Proton if it is being blocked.") f.Println("Bridge is already set to NOT use alternative routing to connect to Proton if it is being blocked.")
return 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() { if !f.bridge.GetShowAllMail() {
f.Println("All Mail folder is not listed in your local client.") f.Println("All Mail folder is not listed in your local client.")
return 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() { if f.bridge.GetShowAllMail() {
f.Println("All Mail folder is listed in your local client.") f.Println("All Mail folder is listed in your local client.")
return return

View File

@ -23,7 +23,7 @@ import (
"github.com/abiosoft/ishell" "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{}) updateCh, done := f.bridge.GetEvents(events.UpdateAvailable{}, events.UpdateNotAvailable{})
defer done() 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() { if f.bridge.GetAutoUpdate() {
f.Println("Bridge is already set to automatically install updates.") f.Println("Bridge is already set to automatically install updates.")
return 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() { if !f.bridge.GetAutoUpdate() {
f.Println("Bridge is already set to NOT automatically install updates.") f.Println("Bridge is already set to NOT automatically install updates.")
return 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 { if f.bridge.GetUpdateChannel() == updater.EarlyChannel {
f.Println("Bridge is already on the early-access update channel.") f.Println("Bridge is already on the early-access update channel.")
return 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 { if f.bridge.GetUpdateChannel() == updater.StableChannel {
f.Println("Bridge is already on the stable update channel.") f.Println("Bridge is already on the stable update channel.")
return return

View File

@ -47,7 +47,7 @@ import (
) )
// CheckTokens implements the CheckToken gRPC service call. // 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") s.log.Debug("CheckTokens")
path := clientConfigPath.Value path := clientConfigPath.Value
@ -65,7 +65,7 @@ func (s *Service) CheckTokens(ctx context.Context, clientConfigPath *wrapperspb.
return &wrapperspb.StringValue{Value: clientConfig.Token}, nil 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 entry := s.log
if len(request.Package) > 0 { 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. // 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.log.Debug("GuiReady")
s.initializationDone.Do(s.initializing.Done) 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. // 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") s.log.Debug("Quit")
return &emptypb.Empty{}, s.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) 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") s.log.Debug("ShowOnStartup")
return wrapperspb.Bool(s.showOnStartup), nil 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") s.log.WithField("show", isOn.Value).Debug("SetIsAutostartOn")
defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }() defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }()
@ -169,13 +169,13 @@ func (s *Service) SetIsAutostartOn(ctx context.Context, isOn *wrapperspb.BoolVal
return &emptypb.Empty{}, nil 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") s.log.Debug("IsAutostartOn")
return wrapperspb.Bool(s.bridge.GetAutostart()), nil 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") s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsBetaEnabled")
channel := updater.StableChannel channel := updater.StableChannel
@ -191,13 +191,13 @@ func (s *Service) SetIsBetaEnabled(ctx context.Context, isEnabled *wrapperspb.Bo
return &emptypb.Empty{}, nil 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") s.log.Debug("IsBetaEnabled")
return wrapperspb.Bool(s.bridge.GetUpdateChannel() == updater.EarlyChannel), nil 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") s.log.WithField("isVisible", isVisible.Value).Debug("SetIsAllMailVisible")
if err := s.bridge.SetShowAllMail(isVisible.Value); err != nil { 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 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") s.log.Debug("IsAllMailVisible")
return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil 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 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 s.log.Debug("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name
return wrapperspb.String(runtime.GOOS), nil 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") s.log.Debug("TriggerReset")
go func() { go func() {
@ -248,13 +248,13 @@ func (s *Service) TriggerReset(ctx context.Context, _ *emptypb.Empty) (*emptypb.
return &emptypb.Empty{}, nil 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") s.log.Debug("Version")
return wrapperspb.String(s.bridge.GetCurrentVersion().Original()), nil 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") s.log.Debug("LogsPath")
path, err := s.bridge.GetLogsPath() 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 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") s.log.Debug("LicensePath")
return wrapperspb.String(s.bridge.GetLicenseFilePath()), nil 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 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() s.latestLock.RLock()
defer s.latestLock.RUnlock() defer s.latestLock.RUnlock()
@ -289,7 +289,7 @@ func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapper
return wrapperspb.String(s.latest.LandingPage), nil 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") s.log.WithField("ColorSchemeName", name.Value).Debug("SetColorSchemeName")
if !theme.IsAvailable(theme.Theme(name.Value)) { 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 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") s.log.Debug("ColorSchemeName")
current := s.bridge.GetColorScheme() current := s.bridge.GetColorScheme()
@ -320,13 +320,13 @@ func (s *Service) ColorSchemeName(ctx context.Context, _ *emptypb.Empty) (*wrapp
return wrapperspb.String(current), nil 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") s.log.Debug("CurrentEmailClient")
return wrapperspb.String(s.bridge.GetCurrentUserAgent()), nil 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{ s.log.WithFields(logrus.Fields{
"osType": report.OsType, "osType": report.OsType,
"osVersion": report.OsVersion, "osVersion": report.OsVersion,
@ -382,7 +382,7 @@ func (s *Service) ExportTLSCertificates(_ context.Context, folderPath *wrappersp
return &emptypb.Empty{}, nil 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.log.WithField("launcher", launcher.Value).Debug("ForceLauncher")
s.restarter.Override(launcher.Value) s.restarter.Override(launcher.Value)
@ -390,7 +390,7 @@ func (s *Service) ForceLauncher(ctx context.Context, launcher *wrapperspb.String
return &emptypb.Empty{}, nil 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.log.WithField("executable", exe.Value).Debug("SetMainExecutable")
s.restarter.AddFlags("--wait", exe.Value) s.restarter.AddFlags("--wait", exe.Value)
@ -398,7 +398,7 @@ func (s *Service) SetMainExecutable(ctx context.Context, exe *wrapperspb.StringV
return &emptypb.Empty{}, nil 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") s.log.WithField("username", login.Username).Debug("Login")
go func() { go func() {
@ -454,7 +454,7 @@ func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empt
return &emptypb.Empty{}, nil 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") s.log.WithField("username", login.Username).Debug("Login2FA")
go func() { go func() {
@ -499,7 +499,7 @@ func (s *Service) Login2FA(ctx context.Context, login *LoginRequest) (*emptypb.E
return &emptypb.Empty{}, nil 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") s.log.WithField("username", login.Username).Debug("Login2Passwords")
go func() { go func() {
@ -521,7 +521,7 @@ func (s *Service) Login2Passwords(ctx context.Context, login *LoginRequest) (*em
return &emptypb.Empty{}, nil 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") s.log.WithField("username", loginAbort.Username).Debug("LoginAbort")
go func() { go func() {
@ -565,7 +565,7 @@ func (s *Service) CheckUpdate(context.Context, *emptypb.Empty) (*emptypb.Empty,
return &emptypb.Empty{}, nil 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") s.log.Debug("InstallUpdate")
go func() { go func() {
@ -579,7 +579,7 @@ func (s *Service) InstallUpdate(ctx context.Context, _ *emptypb.Empty) (*emptypb
return &emptypb.Empty{}, nil 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") s.log.WithField("isOn", isOn.Value).Debug("SetIsAutomaticUpdateOn")
if currentlyOn := s.bridge.GetAutoUpdate(); currentlyOn == isOn.Value { 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 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") s.log.Debug("IsAutomaticUpdateOn")
return wrapperspb.Bool(s.bridge.GetAutoUpdate()), nil 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") s.log.Debug("DiskCachePath")
return wrapperspb.String(s.bridge.GetGluonCacheDir()), nil 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") s.log.WithField("path", newPath.Value).Debug("setDiskCachePath")
go func() { go func() {
@ -637,7 +637,7 @@ func (s *Service) SetDiskCachePath(ctx context.Context, newPath *wrapperspb.Stri
return &emptypb.Empty{}, nil 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") s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsDohEnabled")
if err := s.bridge.SetProxyAllowed(isEnabled.Value); err != nil { 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 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") s.log.Debug("IsDohEnabled")
return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil
@ -668,7 +668,7 @@ func (s *Service) MailServerSettings(_ context.Context, _ *emptypb.Empty) (*Imap
}, nil }, 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. s.log.
WithField("ImapPort", settings.ImapPort). WithField("ImapPort", settings.ImapPort).
WithField("SmtpPort", settings.SmtpPort). WithField("SmtpPort", settings.SmtpPort).
@ -682,28 +682,28 @@ func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSet
defer func() { _ = s.SendEvent(NewChangeMailServerSettingsFinishedEvent()) }() defer func() { _ = s.SendEvent(NewChangeMailServerSettingsFinishedEvent()) }()
if s.bridge.GetIMAPSSL() != settings.UseSSLForImap { 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.log.WithError(err).Error("Failed to set IMAP SSL")
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_CONNECTION_MODE_CHANGE_ERROR)) _ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_CONNECTION_MODE_CHANGE_ERROR))
} }
} }
if s.bridge.GetSMTPSSL() != settings.UseSSLForSmtp { 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.log.WithError(err).Error("Failed to set SMTP SSL")
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_CONNECTION_MODE_CHANGE_ERROR)) _ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_CONNECTION_MODE_CHANGE_ERROR))
} }
} }
if s.bridge.GetIMAPPort() != int(settings.ImapPort) { 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.log.WithError(err).Error("Failed to set IMAP port")
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_PORT_CHANGE_ERROR)) _ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_PORT_CHANGE_ERROR))
} }
} }
if s.bridge.GetSMTPPort() != int(settings.SmtpPort) { 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.log.WithError(err).Error("Failed to set SMTP port")
_ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_PORT_CHANGE_ERROR)) _ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_PORT_CHANGE_ERROR))
} }
@ -715,19 +715,19 @@ func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSet
return &emptypb.Empty{}, nil 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") s.log.Debug("Hostname")
return wrapperspb.String(constants.Host), nil 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") s.log.Debug("IsPortFree")
return wrapperspb.Bool(ports.IsPortFree(int(port.Value))), nil 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") s.log.Debug("AvailableKeychains")
return &AvailableKeychainsResponse{Keychains: maps.Keys(keychain.Helpers)}, nil 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 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") s.log.Debug("CurrentKeychain")
helper, err := s.bridge.GetKeychainApp() helper, err := s.bridge.GetKeychainApp()

View File

@ -28,7 +28,7 @@ import (
"google.golang.org/protobuf/types/known/wrapperspb" "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") s.log.Debug("GetUserList")
userIDs := s.bridge.GetUserIDs() userIDs := s.bridge.GetUserIDs()
@ -51,7 +51,7 @@ func (s *Service) GetUserList(ctx context.Context, _ *emptypb.Empty) (*UserListR
return &UserListResponse{Users: userList}, nil 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") s.log.WithField("userID", userID).Debug("GetUser")
user, err := s.bridge.GetUserInfo(userID.Value) 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 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") s.log.WithField("UserID", splitMode.UserID).WithField("Active", splitMode.Active).Debug("SetUserSplitMode")
user, err := s.bridge.GetUserInfo(splitMode.UserID) user, err := s.bridge.GetUserInfo(splitMode.UserID)
@ -96,7 +96,7 @@ func (s *Service) SetUserSplitMode(ctx context.Context, splitMode *UserSplitMode
return &emptypb.Empty{}, nil 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 := s.log.WithField("UserID", feedback.UserID).WithField("doResync", feedback.DoResync)
l.Debug("SendBadEventUserFeedback") l.Debug("SendBadEventUserFeedback")
@ -114,7 +114,7 @@ func (s *Service) SendBadEventUserFeedback(ctx context.Context, feedback *UserBa
return &emptypb.Empty{}, nil 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") s.log.WithField("UserID", userID.Value).Debug("LogoutUser")
if _, err := s.bridge.GetUserInfo(userID.Value); err != nil { 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 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") s.log.WithField("UserID", userID.Value).Debug("RemoveUser")
go func() { go func() {
@ -152,7 +152,7 @@ func (s *Service) ConfigureUserAppleMail(ctx context.Context, request *Configure
sslWasEnabled := s.bridge.GetSMTPSSL() 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") s.log.WithField("userID", request.UserID).Error("Cannot configure AppleMail for user")
return nil, status.Error(codes.Internal, "Apple Mail config failed") return nil, status.Error(codes.Internal, "Apple Mail config failed")
} }

View File

@ -113,7 +113,7 @@ func Init(logsPath, level string) error {
// Debug or Trace. // Debug or Trace.
func setLevel(level string) error { func setLevel(level string) error {
if level == "" { if level == "" {
return nil level = "debug"
} }
logLevel, err := logrus.ParseLevel(level) logLevel, err := logrus.ParseLevel(level)

View File

@ -96,6 +96,7 @@ func GetTimeZone() string {
// NewReporter creates new sentry reporter with appName and appVersion to report. // NewReporter creates new sentry reporter with appName and appVersion to report.
func NewReporter(appName string, identifier Identifier) *Reporter { func NewReporter(appName string, identifier Identifier) *Reporter {
logrus.WithField("id", GetProtectedHostname()).Info("New sentry reporter")
return &Reporter{ return &Reporter{
appName: appName, appName: appName,
appVersion: constants.Revision, appVersion: constants.Revision,
@ -203,7 +204,7 @@ func SkipDuringUnwind() {
} }
// EnhanceSentryEvent swaps type with value and removes panic handlers from the stacktrace. // 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 { for idx, exception := range event.Exception {
exception.Type, exception.Value = exception.Value, exception.Type exception.Type, exception.Value = exception.Value, exception.Type
if exception.Stacktrace != nil { if exception.Stacktrace != nil {

View File

@ -62,6 +62,6 @@ func (i *InstallerDarwin) InstallUpdate(_ *semver.Version, r io.Reader) error {
return syncFolders(oldBundle, newBundle) return syncFolders(oldBundle, newBundle)
} }
func (i *InstallerDarwin) IsAlreadyInstalled(version *semver.Version) bool { func (i *InstallerDarwin) IsAlreadyInstalled(_ *semver.Version) bool {
return false return false
} }

View File

@ -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. // If the address is enabled, we need to hook it up to the update channels.
switch user.vault.AddressMode() { switch user.vault.AddressMode() {
case vault.CombinedMode: case vault.CombinedMode:
primAddr, err := getAddrIdx(user.apiAddrs, 0) primAddr, err := getPrimaryAddr(user.apiAddrs)
if err != nil { if err != nil {
return fmt.Errorf("failed to get primary address: %w", err) 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: case oldAddr.Status != proton.AddressStatusEnabled && event.Address.Status == proton.AddressStatusEnabled:
switch user.vault.AddressMode() { switch user.vault.AddressMode() {
case vault.CombinedMode: case vault.CombinedMode:
primAddr, err := getAddrIdx(user.apiAddrs, 0) primAddr, err := getPrimaryAddr(user.apiAddrs)
if err != nil { if err != nil {
return fmt.Errorf("failed to get primary address: %w", err) 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 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) { return safe.LockRetErr(func() ([]imap.Update, error) {
var updates []imap.Update var updates []imap.Update
@ -480,7 +480,7 @@ func (user *User) handleUpdateLabelEvent(ctx context.Context, event proton.Label
}, user.apiLabelsLock, user.updateChLock) }, 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) { return safe.LockRetErr(func() ([]imap.Update, error) {
var updates []imap.Update var updates []imap.Update
@ -628,7 +628,14 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, message proton.M
} }
update = imap.NewMessagesCreated(false, res.update) 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 return nil
}); err != 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) }, 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) { return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{ user.log.WithFields(logrus.Fields{
"messageID": message.ID, "messageID": message.ID,
@ -674,13 +681,20 @@ func (user *User) handleUpdateMessageEvent(ctx context.Context, message proton.M
flags, 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 return []imap.Update{update}, nil
}, user.apiLabelsLock, user.updateChLock) }, 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) { return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithField("messageID", event.ID).Info("Handling message deleted event") 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) }, 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) { return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{ user.log.WithFields(logrus.Fields{
"messageID": event.ID, "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. 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 return nil
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
if update == nil {
return nil, nil
}
return []imap.Update{update}, nil return []imap.Update{update}, nil
}, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock) }, 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
}

View File

@ -270,7 +270,7 @@ func (conn *imapConnector) CreateMessage(
mailboxID imap.MailboxID, mailboxID imap.MailboxID,
literal []byte, literal []byte,
flags imap.FlagSet, flags imap.FlagSet,
date time.Time, _ time.Time,
) (imap.Message, []byte, error) { ) (imap.Message, []byte, error) {
defer conn.goPollAPIEvents(false) defer conn.goPollAPIEvents(false)
@ -459,11 +459,11 @@ func (conn *imapConnector) MoveMessages(ctx context.Context, messageIDs []imap.M
var result bool var result bool
if v, ok := conn.apiLabels[string(labelFromID)]; ok && v.Type == proton.LabelTypeLabel { 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) { if v, ok := conn.apiLabels[string(labelToID)]; ok && (v.Type == proton.LabelTypeFolder || v.Type == proton.LabelTypeSystem) {
result = result || true result = true
} }
return result 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. // 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 return nil
} }
@ -544,7 +544,7 @@ func (conn *imapConnector) importMessage(
if err := safe.RLockRet(func() error { if err := safe.RLockRet(func() error {
return withAddrKR(conn.apiUser, conn.apiAddrs[conn.addrID], conn.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) 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) { if slices.Contains(labelIDs, proton.DraftsLabel) {
msg, err := conn.createDraft(ctx, literal, addrKR, conn.apiAddrs[conn.addrID]) msg, err := conn.createDraft(ctx, literal, addrKR, conn.apiAddrs[conn.addrID])

View File

@ -25,6 +25,7 @@ import (
"time" "time"
"github.com/ProtonMail/gluon/rfc822" "github.com/ProtonMail/gluon/rfc822"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -34,22 +35,30 @@ const sendEntryExpiry = 30 * time.Minute
type sendRecorder struct { type sendRecorder struct {
expiry time.Duration expiry time.Duration
entries map[string]*sendEntry entries map[string][]*sendEntry
entriesLock sync.Mutex entriesLock sync.Mutex
} }
func newSendRecorder(expiry time.Duration) *sendRecorder { func newSendRecorder(expiry time.Duration) *sendRecorder {
return &sendRecorder{ return &sendRecorder{
expiry: expiry, expiry: expiry,
entries: make(map[string]*sendEntry), entries: make(map[string][]*sendEntry),
} }
} }
type sendEntry struct { type sendEntry struct {
msgID string msgID string
toList []string toList []string
exp time.Time exp time.Time
waitCh chan struct{} 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. // 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) 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 { func (h *sendRecorder) tryInsert(hash string, toList []string) bool {
h.entriesLock.Lock() h.entriesLock.Lock()
defer h.entriesLock.Unlock() defer h.entriesLock.Unlock()
for hash, entry := range h.entries { h.removeExpiredUnsafe()
if entry.exp.Before(time.Now()) {
delete(h.entries, hash) 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) { h.entries[hash] = append(entries, &sendEntry{
return false
}
h.entries[hash] = &sendEntry{
exp: time.Now().Add(h.expiry), exp: time.Now().Add(h.expiry),
toList: toList, toList: toList,
waitCh: make(chan struct{}), waitCh: make(chan struct{}),
} })
return true return true
} }
@ -129,11 +153,7 @@ func (h *sendRecorder) hasEntry(hash string) bool {
h.entriesLock.Lock() h.entriesLock.Lock()
defer h.entriesLock.Unlock() defer h.entriesLock.Unlock()
for hash, entry := range h.entries { h.removeExpiredUnsafe()
if entry.exp.Before(time.Now()) {
delete(h.entries, hash)
}
}
if _, ok := h.entries[hash]; ok { if _, ok := h.entries[hash]; ok {
return true return true
@ -142,32 +162,46 @@ func (h *sendRecorder) hasEntry(hash string) bool {
return false 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() h.entriesLock.Lock()
defer h.entriesLock.Unlock() defer h.entriesLock.Unlock()
entry, ok := h.entries[hash] entries, ok := h.entries[hash]
if ok { if ok {
entry.msgID = msgID for _, entry := range entries {
} else { if matchToList(entry.toList, toList) {
logrus.Warn("Cannot add message ID to send hash entry, it may have expired") 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() h.entriesLock.Lock()
defer h.entriesLock.Unlock() defer h.entriesLock.Unlock()
entry, ok := h.entries[hash] entries, ok := h.entries[hash]
if !ok || entry.msgID != "" { if !ok {
return 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) { 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() defer h.entriesLock.Unlock()
if entry, ok := h.entries[hash]; ok { if entry, ok := h.entries[hash]; ok {
return entry.msgID, true, nil return entry[0].msgID, true, nil
} }
return "", false, nil return "", false, nil
@ -202,7 +236,7 @@ func (h *sendRecorder) getWaitCh(hash string) (<-chan struct{}, bool) {
defer h.entriesLock.Unlock() defer h.entriesLock.Unlock()
if entry, ok := h.entries[hash]; ok { if entry, ok := h.entries[hash]; ok {
return entry.waitCh, true return entry[0].waitCh, true
} }
return nil, false return nil, false

View File

@ -35,7 +35,7 @@ func TestSendHasher_Insert(t *testing.T) {
require.NotEmpty(t, hash1) require.NotEmpty(t, hash1)
// Simulate successfully sending the message. // Simulate successfully sending the message.
h.addMessageID(hash1, "abc") h.signalMessageSent(hash1, "abc", nil)
// Inserting a message with the same hash should return false. // Inserting a message with the same hash should return false.
_, ok, err = testTryInsert(h, literal1, time.Now().Add(time.Second)) _, 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) require.NotEmpty(t, hash1)
// Simulate successfully sending the message. // Simulate successfully sending the message.
h.addMessageID(hash1, "abc") h.signalMessageSent(hash1, "abc", nil)
// Wait for the entry to expire. // Wait for the entry to expire.
time.Sleep(time.Second) time.Sleep(time.Second)
@ -106,7 +106,7 @@ func TestSendHasher_Wait_SendSuccess(t *testing.T) {
// Simulate successfully sending the message after half a second. // Simulate successfully sending the message after half a second.
go func() { go func() {
time.Sleep(time.Millisecond * 500) time.Sleep(time.Millisecond * 500)
h.addMessageID(hash, "abc") h.signalMessageSent(hash, "abc", nil)
}() }()
// Inserting a message with the same hash should fail. // 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. // Simulate failing to send the message after half a second.
go func() { go func() {
time.Sleep(time.Millisecond * 500) 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. // 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) require.NotEmpty(t, hash)
// Simulate successfully sending the message. // 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. // The message was already sent; we should find it in the hasher.
messageID, ok, err := testHasEntry(h, literal1, time.Now().Add(time.Second)) 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. // Simulate successfully sending the message after half a second.
go func() { go func() {
time.Sleep(time.Millisecond * 500) 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. // 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) 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 <receiver@pm.me>")
require.NoError(t, err)
require.True(t, ok)
require.NotEmpty(t, hash)
hash2, ok, err := testTryInsert(h, literal1, time.Now().Add(time.Second), "Receiver <receiver@pm.me>", "Receiver2 <receiver2@pm.me>")
require.NoError(t, err)
require.True(t, ok)
require.NotEmpty(t, hash2)
require.Equal(t, hash, hash2)
}
func TestSendHasher_HasEntry_SendFail(t *testing.T) { func TestSendHasher_HasEntry_SendFail(t *testing.T) {
h := newSendRecorder(sendEntryExpiry) h := newSendRecorder(sendEntryExpiry)
@ -206,7 +247,7 @@ func TestSendHasher_HasEntry_SendFail(t *testing.T) {
// Simulate failing to send the message after half a second. // Simulate failing to send the message after half a second.
go func() { go func() {
time.Sleep(time.Millisecond * 500) 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. // 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) require.NotEmpty(t, hash)
// Simulate successfully sending the message. // Simulate successfully sending the message.
h.addMessageID(hash, "abc") h.signalMessageSent(hash, "abc", nil)
// Wait for the entry to expire. // Wait for the entry to expire.
time.Sleep(time.Second) time.Sleep(time.Second)
@ -264,7 +305,6 @@ Content-Disposition: attachment; filename="attname.txt"
attachment attachment
--longrandomstring-- --longrandomstring--
` `
const literal2 = `From: Sender <sender@pm.me> const literal2 = `From: Sender <sender@pm.me>
To: Receiver <receiver@pm.me> To: Receiver <receiver@pm.me>
Content-Type: multipart/mixed; boundary=longrandomstring Content-Type: multipart/mixed; boundary=longrandomstring

View File

@ -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. // 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. // Create a new message parser from the reader.
parser, err := parser.New(bytes.NewReader(b)) 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. // 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 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{ attachment, err := client.UploadAttachment(ctx, addrKR, proton.CreateAttachmentReq{
Filename: att.Name, Filename: att.Name,
MessageID: draftID, MessageID: draftID,

View File

@ -19,6 +19,6 @@
package user package user
func debugDumpToDisk(b []byte) error { func debugDumpToDisk(_ []byte) error {
return nil return nil
} }

View File

@ -513,7 +513,16 @@ func (user *User) syncMessages(
result, err := parallel.MapContext(ctx, maxMessagesInParallel, chunk, func(ctx context.Context, msg proton.FullMessage) (*buildRes, error) { result, err := parallel.MapContext(ctx, maxMessagesInParallel, chunk, func(ctx context.Context, msg proton.FullMessage) (*buildRes, error) {
defer async.HandlePanic(user.panicHandler) 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 { if err != nil {
return return
@ -572,10 +581,10 @@ func (user *User) syncMessages(
// We could sync a placeholder message here, but for now we skip it entirely. // We could sync a placeholder message here, but for now we skip it entirely.
continue 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] targetInfo := addressToIndex[res.addressID]

View File

@ -83,6 +83,18 @@ func getAddrIdx(apiAddrs map[string]proton.Address, idx int) (proton.Address, er
return sorted[idx], nil 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. // sortSlice returns the given slice sorted by the given comparator.
func sortSlice[Item any](items []Item, less func(Item, Item) bool) []Item { func sortSlice[Item any](items []Item, less func(Item, Item) bool) []Item {
sorted := make([]Item, len(items)) sorted := make([]Item, len(items))

View File

@ -282,7 +282,7 @@ func (user *User) Match(query string) bool {
func (user *User) Emails() []string { func (user *User) Emails() []string {
return safe.RLockRet(func() []string { return safe.RLockRet(func() []string {
addresses := xslices.Filter(maps.Values(user.apiAddrs), func(addr proton.Address) bool { 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 { 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)) { for _, updateCh := range xslices.Unique(maps.Values(user.updateCh)) {
updateCh.CloseAndDiscardQueued() updateCh.CloseAndDiscardQueued()
} }
user.updateCh = make(map[string]*async.QueuedChannel[imap.Update])
}, user.updateChLock) }, user.updateChLock)
// Close the user's notify channel. // Close the user's notify channel.
@ -690,87 +692,89 @@ func (user *User) doEventPoll(ctx context.Context) error {
user.eventLock.Lock() user.eventLock.Lock()
defer user.eventLock.Unlock() 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 { if err != nil {
return fmt.Errorf("failed to get event (caused by %T): %w", internal.ErrCause(err), err) 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 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") user.log.Debug("No new API events")
return nil return nil
} }
user.log.WithFields(logrus.Fields{ for _, event := range gpaEvents {
"old": user.vault.EventID(), user.log.WithFields(logrus.Fields{
"new": event, "old": user.vault.EventID(),
}).Info("Received new API event") "new": event,
}).Info("Received new API event")
// Handle the event. // Handle the event.
if err := user.handleAPIEvent(ctx, event); err != nil { if err := user.handleAPIEvent(ctx, event); err != nil {
// If the error is a context cancellation, return error to retry later. // If the error is a context cancellation, return error to retry later.
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return fmt.Errorf("failed to handle event due to context cancellation: %w", err) 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. user.log.WithField("event", event).Debug("Handled API event")
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. // Update the event ID in the vault. If this fails, notify bridge to handle it.
if netErr := new(net.OpError); errors.As(err, &netErr) { if err := user.vault.SetEventID(event.EventID); err != nil {
return fmt.Errorf("failed to handle event due to network issues (uncategorized): %w", err) user.eventCh.Enqueue(events.UserBadEvent{
}
// 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(), UserID: user.ID(),
Error: err, 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. user.log.WithField("eventID", event.EventID).Debug("Updated event ID in vault")
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("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 { if more {
user.goPollAPIEvents(false) user.goPollAPIEvents(false)
} }

View File

@ -30,7 +30,12 @@ import (
// If CertPEMPath is set, it will attempt to read the certificate from the file. // 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. // Otherwise, or on read/validation failure, it will return the certificate from the vault.
func (vault *Vault) GetBridgeTLSCert() ([]byte, []byte) { 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 { if certPEM, keyPEM, err := readPEMCert(certPath, keyPath); err == nil {
return certPEM, keyPEM return certPEM, keyPEM
} }
@ -38,7 +43,7 @@ func (vault *Vault) GetBridgeTLSCert() ([]byte, []byte) {
logrus.Error("Failed to read certificate from file, using default") 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. // 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 fmt.Errorf("invalid certificate: %w", err)
} }
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Certs.CustomCertPath = certPath data.Certs.CustomCertPath = certPath
data.Certs.CustomKeyPath = keyPath 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. // SetBridgeTLSCertKey sets the path to PEM-encoded certificates for the bridge.
func (vault *Vault) SetBridgeTLSCertKey(cert, key []byte) error { 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.Cert = cert
data.Certs.Bridge.Key = key data.Certs.Bridge.Key = key
}) })
} }
func (vault *Vault) GetCertsInstalled() bool { func (vault *Vault) GetCertsInstalled() bool {
return vault.get().Certs.Installed return vault.getSafe().Certs.Installed
} }
func (vault *Vault) SetCertsInstalled(installed bool) error { func (vault *Vault) SetCertsInstalled(installed bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Certs.Installed = installed data.Certs.Installed = installed
}) })
} }

View File

@ -18,11 +18,11 @@
package vault package vault
func (vault *Vault) GetCookies() ([]byte, error) { func (vault *Vault) GetCookies() ([]byte, error) {
return vault.get().Cookies, nil return vault.getSafe().Cookies, nil
} }
func (vault *Vault) SetCookies(cookies []byte) error { func (vault *Vault) SetCookies(cookies []byte) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Cookies = cookies data.Cookies = cookies
}) })
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
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)))
}

View File

@ -33,72 +33,72 @@ const (
// GetIMAPPort sets the port that the IMAP server should listen on. // GetIMAPPort sets the port that the IMAP server should listen on.
func (vault *Vault) GetIMAPPort() int { 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. // SetIMAPPort sets the port that the IMAP server should listen on.
func (vault *Vault) SetIMAPPort(port int) error { func (vault *Vault) SetIMAPPort(port int) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.IMAPPort = port data.Settings.IMAPPort = port
}) })
} }
// GetSMTPPort sets the port that the SMTP server should listen on. // GetSMTPPort sets the port that the SMTP server should listen on.
func (vault *Vault) GetSMTPPort() int { 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. // SetSMTPPort sets the port that the SMTP server should listen on.
func (vault *Vault) SetSMTPPort(port int) error { func (vault *Vault) SetSMTPPort(port int) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.SMTPPort = port data.Settings.SMTPPort = port
}) })
} }
// GetIMAPSSL sets whether the IMAP server should use SSL. // GetIMAPSSL sets whether the IMAP server should use SSL.
func (vault *Vault) GetIMAPSSL() bool { func (vault *Vault) GetIMAPSSL() bool {
return vault.get().Settings.IMAPSSL return vault.getSafe().Settings.IMAPSSL
} }
// SetIMAPSSL sets whether the IMAP server should use SSL. // SetIMAPSSL sets whether the IMAP server should use SSL.
func (vault *Vault) SetIMAPSSL(ssl bool) error { func (vault *Vault) SetIMAPSSL(ssl bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.IMAPSSL = ssl data.Settings.IMAPSSL = ssl
}) })
} }
// GetSMTPSSL sets whether the SMTP server should use SSL. // GetSMTPSSL sets whether the SMTP server should use SSL.
func (vault *Vault) GetSMTPSSL() bool { func (vault *Vault) GetSMTPSSL() bool {
return vault.get().Settings.SMTPSSL return vault.getSafe().Settings.SMTPSSL
} }
// SetSMTPSSL sets whether the SMTP server should use SSL. // SetSMTPSSL sets whether the SMTP server should use SSL.
func (vault *Vault) SetSMTPSSL(ssl bool) error { func (vault *Vault) SetSMTPSSL(ssl bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.SMTPSSL = ssl data.Settings.SMTPSSL = ssl
}) })
} }
// GetGluonCacheDir sets the directory where the gluon should store its data. // GetGluonCacheDir sets the directory where the gluon should store its data.
func (vault *Vault) GetGluonCacheDir() string { 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. // SetGluonDir sets the directory where the gluon should store its data.
func (vault *Vault) SetGluonDir(dir string) error { func (vault *Vault) SetGluonDir(dir string) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.GluonDir = dir data.Settings.GluonDir = dir
}) })
} }
// GetUpdateChannel sets the update channel. // GetUpdateChannel sets the update channel.
func (vault *Vault) GetUpdateChannel() updater.Channel { func (vault *Vault) GetUpdateChannel() updater.Channel {
return vault.get().Settings.UpdateChannel return vault.getSafe().Settings.UpdateChannel
} }
// SetUpdateChannel sets the update channel. // SetUpdateChannel sets the update channel.
func (vault *Vault) SetUpdateChannel(channel updater.Channel) error { func (vault *Vault) SetUpdateChannel(channel updater.Channel) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.UpdateChannel = channel data.Settings.UpdateChannel = channel
}) })
} }
@ -106,7 +106,7 @@ func (vault *Vault) SetUpdateChannel(channel updater.Channel) error {
// GetUpdateRollout sets the update rollout. // GetUpdateRollout sets the update rollout.
func (vault *Vault) GetUpdateRollout() float64 { func (vault *Vault) GetUpdateRollout() float64 {
// The rollout value 0.6046602879796196 is forbidden. The RNG was not seeded when it was picked (GODT-2319). // 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 { if math.Abs(rollout-ForbiddenRollout) >= 0.00000001 {
return rollout return rollout
} }
@ -120,110 +120,110 @@ func (vault *Vault) GetUpdateRollout() float64 {
// SetUpdateRollout sets the update rollout. // SetUpdateRollout sets the update rollout.
func (vault *Vault) SetUpdateRollout(rollout float64) error { func (vault *Vault) SetUpdateRollout(rollout float64) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.UpdateRollout = rollout data.Settings.UpdateRollout = rollout
}) })
} }
// GetColorScheme sets the color scheme to be used by the bridge GUI. // GetColorScheme sets the color scheme to be used by the bridge GUI.
func (vault *Vault) GetColorScheme() string { 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. // SetColorScheme sets the color scheme to be used by the bridge GUI.
func (vault *Vault) SetColorScheme(colorScheme string) error { func (vault *Vault) SetColorScheme(colorScheme string) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.ColorScheme = colorScheme data.Settings.ColorScheme = colorScheme
}) })
} }
// GetProxyAllowed sets whether the bridge is allowed to use alternative routing. // GetProxyAllowed sets whether the bridge is allowed to use alternative routing.
func (vault *Vault) GetProxyAllowed() bool { 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. // SetProxyAllowed sets whether the bridge is allowed to use alternative routing.
func (vault *Vault) SetProxyAllowed(allowed bool) error { func (vault *Vault) SetProxyAllowed(allowed bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.ProxyAllowed = allowed data.Settings.ProxyAllowed = allowed
}) })
} }
// GetShowAllMail sets whether the bridge should show the All Mail folder. // GetShowAllMail sets whether the bridge should show the All Mail folder.
func (vault *Vault) GetShowAllMail() bool { 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. // SetShowAllMail sets whether the bridge should show the All Mail folder.
func (vault *Vault) SetShowAllMail(showAllMail bool) error { func (vault *Vault) SetShowAllMail(showAllMail bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.ShowAllMail = showAllMail data.Settings.ShowAllMail = showAllMail
}) })
} }
// GetAutostart sets whether the bridge should autostart. // GetAutostart sets whether the bridge should autostart.
func (vault *Vault) GetAutostart() bool { func (vault *Vault) GetAutostart() bool {
return vault.get().Settings.Autostart return vault.getSafe().Settings.Autostart
} }
// SetAutostart sets whether the bridge should autostart. // SetAutostart sets whether the bridge should autostart.
func (vault *Vault) SetAutostart(autostart bool) error { func (vault *Vault) SetAutostart(autostart bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.Autostart = autostart data.Settings.Autostart = autostart
}) })
} }
// GetAutoUpdate sets whether the bridge should automatically update. // GetAutoUpdate sets whether the bridge should automatically update.
func (vault *Vault) GetAutoUpdate() bool { func (vault *Vault) GetAutoUpdate() bool {
return vault.get().Settings.AutoUpdate return vault.getSafe().Settings.AutoUpdate
} }
// SetAutoUpdate sets whether the bridge should automatically update. // SetAutoUpdate sets whether the bridge should automatically update.
func (vault *Vault) SetAutoUpdate(autoUpdate bool) error { func (vault *Vault) SetAutoUpdate(autoUpdate bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.AutoUpdate = autoUpdate data.Settings.AutoUpdate = autoUpdate
}) })
} }
// GetTelemetryDisabled checks whether telemetry is disabled. // GetTelemetryDisabled checks whether telemetry is disabled.
func (vault *Vault) GetTelemetryDisabled() bool { func (vault *Vault) GetTelemetryDisabled() bool {
return vault.get().Settings.TelemetryDisabled return vault.getSafe().Settings.TelemetryDisabled
} }
// SetTelemetryDisabled sets whether telemetry is disabled. // SetTelemetryDisabled sets whether telemetry is disabled.
func (vault *Vault) SetTelemetryDisabled(telemetryDisabled bool) error { func (vault *Vault) SetTelemetryDisabled(telemetryDisabled bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.TelemetryDisabled = telemetryDisabled data.Settings.TelemetryDisabled = telemetryDisabled
}) })
} }
// GetLastVersion returns the last version of the bridge that was run. // GetLastVersion returns the last version of the bridge that was run.
func (vault *Vault) GetLastVersion() *semver.Version { 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. // SetLastVersion sets the last version of the bridge that was run.
func (vault *Vault) SetLastVersion(version *semver.Version) error { 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() data.Settings.LastVersion = version.String()
}) })
} }
// GetFirstStart returns whether this is the first time the bridge has been started. // GetFirstStart returns whether this is the first time the bridge has been started.
func (vault *Vault) GetFirstStart() bool { 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. // SetFirstStart sets whether this is the first time the bridge has been started.
func (vault *Vault) SetFirstStart(firstStart bool) error { func (vault *Vault) SetFirstStart(firstStart bool) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.FirstStart = firstStart data.Settings.FirstStart = firstStart
}) })
} }
// GetMaxSyncMemory returns the maximum amount of memory the sync process should use. // GetMaxSyncMemory returns the maximum amount of memory the sync process should use.
func (vault *Vault) GetMaxSyncMemory() uint64 { func (vault *Vault) GetMaxSyncMemory() uint64 {
v := vault.get().Settings.MaxSyncMemory v := vault.getSafe().Settings.MaxSyncMemory
// can be zero if never written to vault before. // can be zero if never written to vault before.
if v == 0 { if v == 0 {
return DefaultMaxSyncMemory return DefaultMaxSyncMemory
@ -234,14 +234,14 @@ func (vault *Vault) GetMaxSyncMemory() uint64 {
// SetMaxSyncMemory sets the maximum amount of memory the sync process should use. // SetMaxSyncMemory sets the maximum amount of memory the sync process should use.
func (vault *Vault) SetMaxSyncMemory(maxMemory uint64) error { func (vault *Vault) SetMaxSyncMemory(maxMemory uint64) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.MaxSyncMemory = maxMemory data.Settings.MaxSyncMemory = maxMemory
}) })
} }
// GetLastUserAgent returns the last user agent recorded by bridge. // GetLastUserAgent returns the last user agent recorded by bridge.
func (vault *Vault) GetLastUserAgent() string { func (vault *Vault) GetLastUserAgent() string {
v := vault.get().Settings.LastUserAgent v := vault.getSafe().Settings.LastUserAgent
// Handle case where there may be no value. // Handle case where there may be no value.
if len(v) == 0 { if len(v) == 0 {
@ -253,19 +253,19 @@ func (vault *Vault) GetLastUserAgent() string {
// SetLastUserAgent store the last user agent recorded by bridge. // SetLastUserAgent store the last user agent recorded by bridge.
func (vault *Vault) SetLastUserAgent(userAgent string) error { func (vault *Vault) SetLastUserAgent(userAgent string) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.LastUserAgent = userAgent data.Settings.LastUserAgent = userAgent
}) })
} }
// GetLastHeartbeatSent returns the last time heartbeat was sent. // GetLastHeartbeatSent returns the last time heartbeat was sent.
func (vault *Vault) GetLastHeartbeatSent() time.Time { func (vault *Vault) GetLastHeartbeatSent() time.Time {
return vault.get().Settings.LastHeartbeatSent return vault.getSafe().Settings.LastHeartbeatSent
} }
// SetLastHeartbeatSent store the last time heartbeat was sent. // SetLastHeartbeatSent store the last time heartbeat was sent.
func (vault *Vault) SetLastHeartbeatSent(timestamp time.Time) error { func (vault *Vault) SetLastHeartbeatSent(timestamp time.Time) error {
return vault.mod(func(data *Data) { return vault.modSafe(func(data *Data) {
data.Settings.LastHeartbeatSent = timestamp data.Settings.LastHeartbeatSent = timestamp
}) })
} }

View File

@ -238,3 +238,30 @@ func TestVault_Settings_LastUserAgent(t *testing.T) {
// Check the default first start value. // Check the default first start value.
require.Equal(t, vault.DefaultUserAgent, s.GetLastUserAgent()) 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)
}

View File

@ -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 msgpack.Unmarshal(dec, data)
return err
}
return nil
} }
func marshalFile[T any](gcm cipher.AEAD, t T) ([]byte, error) { func marshalFile[T any](gcm cipher.AEAD, t T) ([]byte, error) {

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -53,6 +53,8 @@ type Settings struct {
LastHeartbeatSent time.Time LastHeartbeatSent time.Time
PasswordArchive PasswordArchive
// **WARNING**: These entry can't be removed until they vault has proper migration support. // **WARNING**: These entry can't be removed until they vault has proper migration support.
SyncWorkers int SyncWorkers int
SyncAttPool int SyncAttPool int
@ -105,5 +107,7 @@ func newDefaultSettings(gluonDir string) Settings {
LastUserAgent: DefaultUserAgent, LastUserAgent: DefaultUserAgent,
LastHeartbeatSent: time.Time{}, LastHeartbeatSent: time.Time{},
PasswordArchive: PasswordArchive{},
} }
} }

View File

@ -73,7 +73,7 @@ func (status SyncStatus) IsComplete() bool {
return status.HasLabels && status.HasMessages 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{ return UserData{
UserID: userID, UserID: userID,
Username: username, Username: username,
@ -82,7 +82,7 @@ func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, key
GluonKey: newRandomToken(32), GluonKey: newRandomToken(32),
GluonIDs: make(map[string]string), GluonIDs: make(map[string]string),
UIDValidity: make(map[string]imap.UID), UIDValidity: make(map[string]imap.UID),
BridgePass: newRandomToken(16), BridgePass: bridgePass,
AddressMode: CombinedMode, AddressMode: CombinedMode,
AuthUID: authUID, AuthUID: authUID,

View File

@ -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. // KeyPass returns the user's (salted) key password.
func (user *User) KeyPass() []byte { func (user *User) KeyPass() []byte {
return user.vault.getUser(user.userID).KeyPass return user.vault.getUser(user.userID).KeyPass

View File

@ -40,11 +40,11 @@ type Vault struct {
path string path string
gcm cipher.AEAD gcm cipher.AEAD
enc []byte enc []byte
encLock sync.RWMutex
ref map[string]int ref map[string]int
refLock sync.Mutex
lock sync.RWMutex
panicHandler async.PanicHandler 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. // GetUserIDs returns the user IDs and usernames of all users in the vault.
func (vault *Vault) GetUserIDs() []string { 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 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. // HasUser returns true if the vault contains a user with the given ID.
func (vault *Vault) HasUser(userID string) bool { 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 return user.UserID == userID
}) >= 0 }) >= 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. // NewUser returns a new vault user. It must be closed before it can be deleted.
func (vault *Vault) NewUser(userID string) (*User, error) { 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 return user.UserID == userID
}); idx < 0 { }); idx < 0 {
return nil, errors.New("no such user") 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. // ForUser executes a callback for each user in the vault.
func (vault *Vault) ForUser(parallelism int, fn func(*User) error) error { 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) defer async.HandlePanic(vault.panicHandler)
user, err := vault.NewUser(userIDs[idx]) user := users[idx]
if err != nil {
return err
}
defer func() { _ = user.Close() }()
return fn(user) 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. // 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) { 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") logrus.WithField("userID", userID).Info("Adding vault user")
var exists bool 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 { if idx := xslices.IndexFunc(data.Users, func(user UserData) bool {
return user.UserID == userID return user.UserID == userID
}); idx >= 0 { }); idx >= 0 {
exists = true exists = true
} else { } 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 { }); err != nil {
return nil, err 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 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. // DeleteUser removes the given user from the vault.
func (vault *Vault) DeleteUser(userID string) error { func (vault *Vault) DeleteUser(userID string) error {
vault.refLock.Lock() vault.lock.Lock()
defer vault.refLock.Unlock() defer vault.lock.Unlock()
logrus.WithField("userID", userID).Info("Deleting vault user") 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 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 { idx := xslices.IndexFunc(data.Users, func(user UserData) bool {
return user.UserID == userID return user.UserID == userID
}) })
@ -177,23 +264,32 @@ func (vault *Vault) DeleteUser(userID string) error {
if idx < 0 { if idx < 0 {
return return
} }
data.Settings.PasswordArchive.set(data.Users[idx].PrimaryEmail, data.Users[idx].BridgePass)
data.Users = append(data.Users[:idx], data.Users[idx+1:]...) data.Users = append(data.Users[:idx], data.Users[idx+1:]...)
}) })
} }
func (vault *Vault) Migrated() bool { 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 { 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 data.Migrated = true
}) })
} }
func (vault *Vault) Reset(gluonDir string) error { 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) *data = newDefaultData(gluonDir)
}) })
} }
@ -203,8 +299,8 @@ func (vault *Vault) Path() string {
} }
func (vault *Vault) Close() error { func (vault *Vault) Close() error {
vault.refLock.Lock() vault.lock.Lock()
defer vault.refLock.Unlock() defer vault.lock.Unlock()
if len(vault.ref) > 0 { if len(vault.ref) > 0 {
return errors.New("vault is still in use") return errors.New("vault is still in use")
@ -215,10 +311,7 @@ func (vault *Vault) Close() error {
return nil return nil
} }
func (vault *Vault) attachUser(userID string) *User { func (vault *Vault) attachUserUnsafe(userID string) *User {
vault.refLock.Lock()
defer vault.refLock.Unlock()
logrus.WithField("userID", userID).Trace("Attaching vault user") logrus.WithField("userID", userID).Trace("Attaching vault user")
vault.ref[userID]++ vault.ref[userID]++
@ -230,8 +323,8 @@ func (vault *Vault) attachUser(userID string) *User {
} }
func (vault *Vault) detachUser(userID string) error { func (vault *Vault) detachUser(userID string) error {
vault.refLock.Lock() vault.lock.Lock()
defer vault.refLock.Unlock() defer vault.lock.Unlock()
logrus.WithField("userID", userID).Trace("Detaching vault user") 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 }, corrupt, nil
} }
func (vault *Vault) get() Data { func (vault *Vault) getSafe() Data {
vault.encLock.RLock() vault.lock.RLock()
defer vault.encLock.RUnlock() defer vault.lock.RUnlock()
return vault.getUnsafe()
}
func (vault *Vault) getUnsafe() Data {
var data Data var data Data
if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil { if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil {
@ -296,10 +393,14 @@ func (vault *Vault) get() Data {
return data return data
} }
func (vault *Vault) mod(fn func(data *Data)) error { func (vault *Vault) modSafe(fn func(data *Data)) error {
vault.encLock.Lock() vault.lock.Lock()
defer vault.encLock.Unlock() defer vault.lock.Unlock()
return vault.modUnsafe(fn)
}
func (vault *Vault) modUnsafe(fn func(data *Data)) error {
var data Data var data Data
if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil { 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 { 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 return user.UserID == userID
})] })
if idx < 0 {
panic("Unknown user")
}
return users[idx]
} }
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error { 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 { idx := xslices.IndexFunc(data.Users, func(user UserData) bool {
return user.UserID == userID return user.UserID == userID
}) })

View File

@ -24,7 +24,7 @@ import (
) )
func (vault *Vault) ImportJSON(dec []byte) { func (vault *Vault) ImportJSON(dec []byte) {
vault.mod(func(data *Data) { vault.modSafe(func(data *Data) {
if err := json.Unmarshal(dec, data); err != nil { if err := json.Unmarshal(dec, data); err != nil {
panic(err) panic(err)
} }
@ -32,7 +32,7 @@ func (vault *Vault) ImportJSON(dec []byte) {
} }
func (vault *Vault) ExportJSON() []byte { func (vault *Vault) ExportJSON() []byte {
enc, err := json.MarshalIndent(vault.get(), "", " ") enc, err := json.MarshalIndent(vault.getSafe(), "", " ")
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -29,7 +29,7 @@ func (v *Versioner) RemoveOldVersions() error {
} }
// RemoveOtherVersions removes all but the specific provided app version. // 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. // darwin does not use the versioner; removal is a noop.
return nil return nil
} }

129
pkg/cpc/cpc.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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),
}
}

65
pkg/cpc/cpc_test.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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()
}

View File

@ -92,11 +92,7 @@ func buildSimpleRFC822(kr *crypto.KeyRing, msg proton.Message, opts JobOptions,
return err return err
} }
if err := w.Close(); err != nil { return w.Close()
return err
}
return nil
} }
func buildMultipartRFC822( func buildMultipartRFC822(
@ -148,11 +144,7 @@ func buildMultipartRFC822(
} }
} }
if err := w.Close(); err != nil { return w.Close()
return err
}
return nil
} }
func writeTextPart( func writeTextPart(
@ -319,11 +311,7 @@ func buildPGPMIMEFallbackRFC822(msg proton.Message, opts JobOptions, buf *bytes.
return err return err
} }
if err := w.Close(); err != nil { return w.Close()
return err
}
return nil
} }
func writeMultipartSignedRFC822(header message.Header, body []byte, sig proton.Signature, buf *bytes.Buffer) error { 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 return err
} }
if err := mw.Close(); err != nil { return mw.Close()
return err
}
return nil
} }
func writeMultipartEncryptedRFC822(header message.Header, body []byte, buf *bytes.Buffer) error { func writeMultipartEncryptedRFC822(header message.Header, body []byte, buf *bytes.Buffer) error {

View File

@ -673,6 +673,40 @@ func TestParsePanic(t *testing.T) {
require.Error(t, err) 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" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, 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" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, 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 { func getFileReader(filename string) io.Reader {
f, err := os.Open(filepath.Join("testdata", filename)) f, err := os.Open(filepath.Join("testdata", filename))
if err != nil { if err != nil {
@ -684,6 +718,6 @@ func getFileReader(filename string) io.Reader {
type panicReader struct{} type panicReader struct{}
func (panicReader) Read(p []byte) (int, error) { func (panicReader) Read(_ []byte) (int, error) {
panic("lol") panic("lol")
} }

View File

@ -0,0 +1,24 @@
Content-Type: multipart/mixed; boundary="------------nq8WTMHkJcymWO6pWfby0uY3"
To: "Receiver" <receiver@pm.me>
From: "Sender" <sender@pm.me>
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--

View File

@ -0,0 +1,25 @@
Content-Type: multipart/mixed; boundary="------------bYzsV6z0EdKTbltmCDZgIM15"
To: "Receiver" <receiver@pm.me>
From: "Sender" <sender@pm.me>
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--

View File

@ -106,6 +106,7 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^the user agent is "([^"]*)"$`, s.theUserAgentIs) ctx.Step(`^the user agent is "([^"]*)"$`, s.theUserAgentIs)
ctx.Step(`^the header in the "([^"]*)" request to "([^"]*)" has "([^"]*)" set to "([^"]*)"$`, s.theHeaderInTheRequestToHasSetTo) 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 "([^"]*)" 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 API requires bridge version at least "([^"]*)"$`, s.theAPIRequiresBridgeVersion)
ctx.Step(`^the network port (\d+) is busy$`, s.networkPortIsBusy) ctx.Step(`^the network port (\d+) is busy$`, s.networkPortIsBusy)
ctx.Step(`^the network port range (\d+)-(\d+) is busy$`, s.networkPortRangeIsBusy) 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 "([^"]*)" is not listed$`, s.userIsNotListed)
ctx.Step(`^user "([^"]*)" finishes syncing$`, s.userFinishesSyncing) ctx.Step(`^user "([^"]*)" finishes syncing$`, s.userFinishesSyncing)
ctx.Step(`^user "([^"]*)" has telemetry set to (\d+)$`, s.userHasTelemetrySetTo) 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 ==== // ==== IMAP ====
ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient) ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient)

View File

@ -60,11 +60,11 @@ func (s *scenario) theAPIRequiresBridgeVersion(version string) error {
} }
func (s *scenario) theUserChangesTheIMAPPortTo(port int) 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 { 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 { func (s *scenario) theUserSetsTheAddressModeOfUserTo(user, mode string) error {
@ -144,11 +144,11 @@ func (s *scenario) theUserHasEnabledAlternativeRouting() error {
} }
func (s *scenario) theUserSetIMAPModeToSSL() 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 { func (s *scenario) theUserSetSMTPModeToSSL() error {
return s.t.bridge.SetSMTPSSL(true) return s.t.bridge.SetSMTPSSL(context.Background(), true)
} }
func (s *scenario) theUserReportsABug() error { func (s *scenario) theUserReportsABug() error {

View File

@ -114,6 +114,7 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) {
} else if corrupt { } else if corrupt {
return nil, fmt.Errorf("vault is corrupt") return nil, fmt.Errorf("vault is corrupt")
} }
t.vault = vault
// Create the underlying cookie jar. // Create the underlying cookie jar.
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
@ -351,8 +352,8 @@ func (t *testCtx) expectProxyCtlAllowProxy() {
type mockRestarter struct{} 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) {}

View File

@ -19,6 +19,7 @@ package tests
import ( import (
"fmt" "fmt"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/emersion/go-imap/client" "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 { 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 { if err != nil {
return err return err
} }
t.imapClients[clientID] = &imapClient{ t.imapClients[clientID] = &imapClient{
userID: userID, userID: userID,
client: client, client: cli,
} }
return nil 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) { func (t *testCtx) getIMAPClient(clientID string) (string, *client.Client) {
return t.imapClients[clientID].userID, t.imapClients[clientID].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)
}

View File

@ -36,6 +36,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge" "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
frontend "github.com/ProtonMail/proton-bridge/v3/internal/frontend/grpc" 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/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
"github.com/cucumber/godog" "github.com/cucumber/godog"
"github.com/emersion/go-imap/client" "github.com/emersion/go-imap/client"
@ -135,6 +136,7 @@ type testCtx struct {
// bridge holds the bridge app under test. // bridge holds the bridge app under test.
bridge *bridge.Bridge bridge *bridge.Bridge
vault *vault.Vault
// service holds the gRPC frontend service under test. // service holds the gRPC frontend service under test.
service *frontend.Service service *frontend.Service
@ -165,6 +167,9 @@ type testCtx struct {
// This slice contains the dummy listeners that are intended to block network ports. // This slice contains the dummy listeners that are intended to block network ports.
dummyListeners []net.Listener dummyListeners []net.Listener
imapServerStarted bool
smtpServerStarted bool
} }
type imapClient struct { type imapClient struct {

View File

@ -110,3 +110,27 @@ func (s *scenario) theBodyInTheRequestToIs(method, path string, value *godog.Doc
return nil 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
}

View File

@ -28,7 +28,7 @@ var (
preCompKeyPEM []byte preCompKeyPEM []byte
) )
func FastGenerateCert(template *x509.Certificate) ([]byte, []byte, error) { func FastGenerateCert(_ *x509.Certificate) ([]byte, []byte, error) {
return preCompCertPEM, preCompKeyPEM, nil return preCompCertPEM, preCompKeyPEM, nil
} }

View File

@ -1,7 +1,9 @@
Feature: Send Telemetry Heartbeat Feature: Send Telemetry Heartbeat
Background: Background:
Given there exists an account with username "[user:user1]" and password "password" 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 Scenario: Send at first start - one user default settings

View File

@ -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 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:user]" has additional address "[alias:alias]@[domain]"
And the account "[user:user2]" has additional disabled address "[alias:alias2]@[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:user]" and password "password"
And the user logs in with username "[user:user2]" and password "password2" And the user logs in with username "[user:user2]" and password "password2"
Then it succeeds
Scenario: IMAP client can authenticate successfully Scenario: IMAP client can authenticate successfully
When user "[user:user]" connects IMAP client "1" When user "[user:user]" connects IMAP client "1"

View File

@ -1,8 +1,10 @@
Feature: The IMAP ID is propagated to bridge Feature: The IMAP ID is propagated to bridge
Background: Background:
Given there exists an account with username "[user:user]" and password "password" 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 logs in with username "[user:user]" and password "password"
Then it succeeds
Scenario: Initial user agent before an IMAP client announces its ID Scenario: Initial user agent before an IMAP client announces its ID
When user "[user:user]" connects IMAP client "1" When user "[user:user]" connects IMAP client "1"

View File

@ -7,10 +7,12 @@ Feature: IMAP create mailbox
| f2 | folder | | f2 | folder |
| l1 | label | | l1 | label |
| l2 | 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 the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1" And user "[user:user]" connects and authenticates IMAP client "1"
Then it succeeds
Scenario: Create folder Scenario: Create folder
When IMAP client "1" creates "Folders/mbox" When IMAP client "1" creates "Folders/mbox"

View File

@ -6,10 +6,12 @@ Feature: IMAP delete mailbox
| one | folder | | one | folder |
| two | folder | | two | folder |
| three | label | | three | label |
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:user]" and password "password"
And user "[user:user]" finishes syncing And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1" And user "[user:user]" connects and authenticates IMAP client "1"
Then it succeeds
Scenario: Delete folder Scenario: Delete folder
When IMAP client "1" deletes "Folders/one" When IMAP client "1" deletes "Folders/one"

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