chore: merge branch release/perth_narrows into release/Quebec
This commit is contained in:
commit
a11559fe58
143
.gitlab-ci.yml
143
.gitlab-ci.yml
|
@ -32,7 +32,7 @@ stages:
|
|||
|
||||
.rules-branch-and-MR-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- when: never
|
||||
|
@ -54,6 +54,28 @@ stages:
|
|||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.after-script-code-coverage:
|
||||
after_script:
|
||||
- go get github.com/boumenot/gocover-cobertura
|
||||
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
|
||||
- "go tool cover -func=/tmp/coverage.out | grep total:"
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
|
@ -65,46 +87,62 @@ lint:
|
|||
tags:
|
||||
- medium
|
||||
|
||||
test-linux:
|
||||
|
||||
.test-base:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
script:
|
||||
- make test
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .test-base
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
- .after-script-code-coverage
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-linux-race:
|
||||
stage: test
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-integration:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-integration-race:
|
||||
stage: test
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
tags:
|
||||
- large
|
||||
|
||||
dependency-updates:
|
||||
|
||||
.windows-base:
|
||||
before_script:
|
||||
- export GOROOT=/c/Go1.18
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go18
|
||||
- export GO111MODULE=on
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export MSYSTEM=
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
test-windows:
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
- .windows-base
|
||||
stage: test
|
||||
script:
|
||||
- make updates
|
||||
- make test
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
|
@ -120,31 +158,20 @@ dependency-updates:
|
|||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(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 url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
# Note: The latest artifacts for refs are locked against deletion, and kept
|
||||
# regardless of the expiry time. Introduced in GitLab 13.0 behind a
|
||||
# disabled feature flag, and made the default behavior in GitLab 13.4.
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends: .build-base
|
||||
|
||||
.linux-build-setup:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
|
@ -153,19 +180,29 @@ build-linux:
|
|||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
artifacts:
|
||||
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(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 url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .build-base
|
||||
- .linux-build-setup
|
||||
|
||||
build-linux-qa:
|
||||
extends: build-linux
|
||||
extends:
|
||||
- build-linux
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
|
||||
.build-darwin-base:
|
||||
extends: .build-base
|
||||
.darwin-build-setup:
|
||||
before_script:
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
|
@ -177,30 +214,22 @@ build-linux-qa:
|
|||
- 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 url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
script:
|
||||
- go version
|
||||
- make build-nogui
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS
|
||||
|
||||
build-darwin:
|
||||
extends: .build-darwin-base
|
||||
artifacts:
|
||||
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
|
||||
extends:
|
||||
- .build-base
|
||||
- .darwin-build-setup
|
||||
|
||||
build-darwin-qa:
|
||||
extends: .build-darwin-base
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
name: "bridge-darwin-qa-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
|
||||
.build-windows-base:
|
||||
extends: .build-base
|
||||
.windows-build-setup:
|
||||
before_script:
|
||||
- export GOROOT=/c/Go1.18/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
|
@ -214,23 +243,19 @@ build-darwin-qa:
|
|||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
||||
- $(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 url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
script:
|
||||
- make build-nogui
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
cache: {}
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
build-windows:
|
||||
extends: .build-windows-base
|
||||
artifacts:
|
||||
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
|
||||
extends:
|
||||
- .build-base
|
||||
- .windows-build-setup
|
||||
|
||||
build-windows-qa:
|
||||
extends: .build-windows-base
|
||||
extends:
|
||||
- build-windows
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
||||
|
|
|
@ -23,7 +23,6 @@ issues:
|
|||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
|
@ -32,7 +31,6 @@ issues:
|
|||
- path: test
|
||||
linters:
|
||||
- dupl
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
|
@ -64,7 +62,6 @@ linters:
|
|||
- 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]
|
||||
- dupl # Tool for code clone detection [fast: true, auto-fix: false]
|
||||
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
|
||||
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
|
||||
- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
|
||||
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
||||
|
|
|
@ -26,7 +26,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||
* [gluon](https://github.com/ProtonMail/gluon) available under [license](https://github.com/ProtonMail/gluon/blob/master/LICENSE)
|
||||
* [go-autostart](https://github.com/ProtonMail/go-autostart) available under [license](https://github.com/ProtonMail/go-autostart/blob/master/LICENSE)
|
||||
* [go-proton-api](https://github.com/ProtonMail/go-proton-api) available under [license](https://github.com/ProtonMail/go-proton-api/blob/master/LICENSE)
|
||||
* [go-rfc5322](https://github.com/ProtonMail/go-rfc5322) available under [license](https://github.com/ProtonMail/go-rfc5322/blob/master/LICENSE)
|
||||
* [gopenpgp](https://github.com/ProtonMail/gopenpgp/v2) available under [license](https://github.com/ProtonMail/gopenpgp/v2/blob/master/LICENSE)
|
||||
* [goquery](https://github.com/PuerkitoBio/goquery) available under [license](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
|
||||
* [ishell](https://github.com/abiosoft/ishell) available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
|
||||
|
@ -53,6 +52,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
||||
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
||||
* [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE)
|
||||
* [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE)
|
||||
* [profile](https://github.com/pkg/profile) available under [license](https://github.com/pkg/profile/blob/master/LICENSE)
|
||||
* [logrus](https://github.com/sirupsen/logrus) available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
|
||||
|
@ -76,8 +76,9 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||
* [levenshtein](https://github.com/agext/levenshtein) available under [license](https://github.com/agext/levenshtein/blob/master/LICENSE)
|
||||
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
||||
* [antlr](https://github.com/antlr/antlr4/runtime/Go/antlr) available under [license](https://github.com/antlr/antlr4/runtime/Go/antlr/blob/master/LICENSE)
|
||||
* [go-textseg](https://github.com/apparentlymart/go-textseg/v13) available under [license](https://github.com/apparentlymart/go-textseg/v13/blob/master/LICENSE)
|
||||
* [sonic](https://github.com/bytedance/sonic) available under [license](https://github.com/bytedance/sonic/blob/master/LICENSE)
|
||||
* [base64x](https://github.com/chenzhuoyu/base64x) available under [license](https://github.com/chenzhuoyu/base64x/blob/master/LICENSE)
|
||||
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||
* [circl](https://github.com/cloudflare/circl) available under [license](https://github.com/cloudflare/circl/blob/master/LICENSE)
|
||||
* [go-md2man](https://github.com/cpuguy83/go-md2man/v2) available under [license](https://github.com/cpuguy83/go-md2man/v2/blob/master/LICENSE)
|
||||
|
@ -88,6 +89,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
||||
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
|
||||
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
|
||||
|
@ -97,6 +99,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
||||
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
|
||||
* [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE)
|
||||
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
|
||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
||||
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
||||
|
@ -104,6 +107,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||
* [hcl](https://github.com/hashicorp/hcl/v2) available under [license](https://github.com/hashicorp/hcl/v2/blob/master/LICENSE)
|
||||
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
|
||||
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
|
||||
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
|
||||
* [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE)
|
||||
* [go-colorable](https://github.com/mattn/go-colorable) available under [license](https://github.com/mattn/go-colorable/blob/master/LICENSE)
|
||||
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
||||
|
@ -114,22 +118,24 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
|
||||
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
||||
* [go-toml](https://github.com/pelletier/go-toml/v2) available under [license](https://github.com/pelletier/go-toml/v2/blob/master/LICENSE)
|
||||
* [lz4](https://github.com/pierrec/lz4/v4) available under [license](https://github.com/pierrec/lz4/v4/blob/master/LICENSE)
|
||||
* [go-difflib](https://github.com/pmezard/go-difflib) available under [license](https://github.com/pmezard/go-difflib/blob/master/LICENSE)
|
||||
* [procfs](https://github.com/prometheus/procfs) available under [license](https://github.com/prometheus/procfs/blob/master/LICENSE)
|
||||
* [uniseg](https://github.com/rivo/uniseg) available under [license](https://github.com/rivo/uniseg/blob/master/LICENSE)
|
||||
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
|
||||
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
|
||||
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
|
||||
* [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE)
|
||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||
* [go-cty](https://github.com/zclconf/go-cty) available under [license](https://github.com/zclconf/go-cty/blob/master/LICENSE)
|
||||
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
||||
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||
* [genproto](https://google.golang.org/genproto)
|
||||
gopkg.in/yaml.v2
|
||||
gopkg.in/yaml.v3
|
||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
|
|
100
Changelog.md
100
Changelog.md
|
@ -2,6 +2,73 @@
|
|||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Bridge 3.1.0] Quebec
|
||||
|
||||
### Changed
|
||||
* Update GPA to include detailed error messages.
|
||||
* GODT-2479: Ensure messages always have a text body part.
|
||||
* GODT-2482: More attachment to relevant exceptions.
|
||||
* GODT-2224: Refactor bridge sync to use less memory.
|
||||
* GODT-2448: Supported Answered flag.
|
||||
* GODT-2382: Added bridge-gui settings file with 'UseSoftwareRenderer' value.
|
||||
* GODT-2411: Allow qmake executable to be named qmake6.
|
||||
* GODT-2273: Menu with "Close window" and "Quit Bridge" button in main window.
|
||||
* GODT-2261: Sync progress in GUI.
|
||||
* GODT-2385: Gluon cache fallback.
|
||||
* GODT-2366: Handle failed message updates as creates.
|
||||
* GODT-2201: Bump Gluon to use pure Go IMAP parser.
|
||||
* GODT-2374: Import TLS certs via shell.
|
||||
* GODT-2361: Bump GPA to use simple encrypter.
|
||||
* GODT-1264: Constraint on Scheduled mailbox in connector + Integration tests.
|
||||
* GODT-1264: Creation and visibility of the 'Scheduled' system label.
|
||||
* GODT-2283: Limit max import size to 30MB (bump GPA to v0.4.0).
|
||||
* GODT-2352: Only copy resource file when needed.
|
||||
* GODT-2352: Use go-build-finalize macro to build vault-editor for both mac arch.
|
||||
* GODT-2278: Properly override server_name for go.
|
||||
* GODT-2255: Randomize the focus service port.
|
||||
* GODT-2144: Handle IMAP/SMTP server errors via event stream.
|
||||
* GODT-2144: Delay IMAP/SMTP server start until all users are loaded.
|
||||
* GODT-2295: Notifications for IMAP login when signed out.
|
||||
* GODT-2278: Improve sentry logs.
|
||||
* GODT-2289: UIDValidity as Timestamp.
|
||||
|
||||
### Fixed
|
||||
* GODT-2455: Upper limit for number of merged events.
|
||||
* GODT-2480: Do not override X-Original-Date with invalid Date.
|
||||
* GODT-2473: Fix handling of complex mime types.
|
||||
* GODT-2469: Fix sentry revision hash for cmake on windows.
|
||||
* GODT-2424: Sync Builder Message Split.
|
||||
* GODT-2419: Use connector.ErrOperationNotAllowed.
|
||||
* GODT-2418: Ensure child folders are updated when parent is.
|
||||
* GODT-1945: Handle disabled addresses correctly.
|
||||
* GODT-2390: Add reports for uncaught json and net.opErr.
|
||||
* GODT-2393: Improved handling of unrecoverable error.
|
||||
* GODT-2394: Bump Gluon for golang.org/x/text DoS risk.
|
||||
* GODT-2387: Ensure vault can be unlocked after factory reset.
|
||||
* GODT-2389: Close bridge on exception and add max termination wait time.
|
||||
* GODT-2201: Add missing rfc5322.CharsetReader initialization.
|
||||
* GODT-1804: Preserve MIME parameters when uploading attachments.
|
||||
* GODT-2312: Used space is properly updated.
|
||||
* GODT-2319: Seed the math/rand RNG on app startup.
|
||||
* GODT-2272: Use shorter filename for gRPC file socket.
|
||||
* GODT-2318: Remove gluon DB if label sync was incomplete.
|
||||
* GODT-2326: Only run sync after addIMAPUser().
|
||||
* GODT-2323: Fix Expunge not issued for move.
|
||||
* GODT-2224: Properly handle context cancellation during sync.
|
||||
* GODT-2328: Ignore labels that aren't part of user label set.
|
||||
* GODT-2326: Fix potential Win32 API deadlock.
|
||||
* GODT-1804: Only promote content headers if non-empty.
|
||||
* GODT-2327: Remove unnecessary sync when changing address mode.
|
||||
* GODT-2343: Only poll after send if sync is complete.
|
||||
* GODT-2336: Recover from changed address order while bridge is down.
|
||||
* GODT-2347: Prevent updates from being dropped if goroutine doesn't start fast.
|
||||
* GODT-2351: Bump GPA to properly handle net.OpError and add tests.
|
||||
* GODT-2351: Bump GPA to automatically retry on net.OpError.
|
||||
* GODT-2365: Use predictable remote ID for placeholder mailboxes.
|
||||
* GODT-2381: Unset draft flag on sent messages.
|
||||
* GODT-2380: Only set external ID in header if non-empty.
|
||||
|
||||
|
||||
## [Bridge 3.0.21] Perth Narrows
|
||||
|
||||
### Added
|
||||
|
@ -24,24 +91,25 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||
* GODT-2442: Allow user to re-sync DB without logout.
|
||||
|
||||
### Changed
|
||||
GODT-2419: Reduce sentry reports.
|
||||
GODT-2458: Wait for both bridge and bridge-gui to be ended before restarting on crash.
|
||||
GODT-2457: Include address if GetPublickKeys() error message.
|
||||
GODT-2446: Attach logs to sentry reports for relevant bridge-gui exceptions.
|
||||
GODT-2425: Out of sync messages and read status.
|
||||
GODT-2435: Group report exception by message if exception message looks corrupted.
|
||||
GODT-2356: Unify sentry release description and add more context to it.
|
||||
GODT-2357: Hide DSN_SENTRY and use single setting point for DSN_SENTRY.
|
||||
GODT-2444: Bad event info.
|
||||
GODT-2447: Don't assume timestamp exists in log filename.
|
||||
GODT-2333: Do not allow modifications to All Mail label.
|
||||
GODT-2429: Do not report context cancel to sentry.
|
||||
* GODT-2419: Reduce sentry reports.
|
||||
* GODT-2458: Wait for both bridge and bridge-gui to be ended before restarting on crash.
|
||||
* GODT-2457: Include address if GetPublickKeys() error message.
|
||||
* GODT-2446: Attach logs to sentry reports for relevant bridge-gui exceptions.
|
||||
* GODT-2425: Out of sync messages and read status.
|
||||
* GODT-2435: Group report exception by message if exception message looks corrupted.
|
||||
* GODT-2356: Unify sentry release description and add more context to it.
|
||||
* GODT-2357: Hide DSN_SENTRY and use single setting point for DSN_SENTRY.
|
||||
* GODT-2444: Bad event info.
|
||||
* GODT-2447: Don't assume timestamp exists in log filename.
|
||||
* GODT-2333: Do not allow modifications to All Mail label.
|
||||
* GODT-2429: Do not report context cancel to sentry.
|
||||
|
||||
### Fixed
|
||||
GODT-2449: fix bug in Bridge-GUI's Exception::what().
|
||||
GODT-2427: Parsing header issues.
|
||||
GODT-2426: Fix crash on user delete.
|
||||
GODT-2417: Do not request gluon recovered message from API.
|
||||
* GODT-2467: elide long email adresses in 'bad event' QML notification dialog.
|
||||
* GODT-2449: fix bug in Bridge-GUI's Exception::what().
|
||||
* GODT-2427: Parsing header issues.
|
||||
* GODT-2426: Fix crash on user delete.
|
||||
* GODT-2417: Do not request gluon recovered message from API.
|
||||
|
||||
|
||||
## [Bridge 3.0.19] Perth Narrows
|
||||
|
|
16
Makefile
16
Makefile
|
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=3.0.21+git
|
||||
BRIDGE_APP_VERSION?=3.1.0+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
|
@ -100,9 +100,9 @@ endif
|
|||
|
||||
ifeq "${GOOS}" "windows"
|
||||
go-build-finalize= \
|
||||
powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} && \
|
||||
$(call go-build,$(1),$(2),$(3)) && \
|
||||
powershell Remove-Item ${4} -Force
|
||||
$(if $(4),powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} &&,) \
|
||||
$(call go-build,$(1),$(2),$(3)) \
|
||||
$(if $(4), && powershell Remove-Item ${4} -Force,)
|
||||
endif
|
||||
|
||||
${EXE_NAME}: gofiles ${RESOURCE_FILE}
|
||||
|
@ -116,7 +116,7 @@ versioner:
|
|||
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
|
||||
|
||||
vault-editor:
|
||||
go build -tags debug -o vault-editor utils/vault-editor/main.go
|
||||
$(call go-build-finalize,"-tags=debug","vault-editor","./utils/vault-editor/main.go")
|
||||
|
||||
hasher:
|
||||
go build -o hasher utils/hasher/main.go
|
||||
|
@ -228,13 +228,13 @@ change-copyright-year:
|
|||
./utils/missing_license.sh change-year
|
||||
|
||||
test: gofiles
|
||||
go test -v -timeout=5m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
go test -v -timeout=10m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-race: gofiles
|
||||
go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-integration: gofiles
|
||||
go test -v -timeout=10m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
go test -v -timeout=20m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
|
||||
test-integration-debug: gofiles
|
||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||
|
@ -253,7 +253,7 @@ coverage: test
|
|||
mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/bridge TLSReporter,ProxyController,Autostarter > tmp
|
||||
mv tmp internal/bridge/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||
|
||||
|
|
34
README.md
34
README.md
|
@ -1,5 +1,5 @@
|
|||
# Proton Mail Bridge and Import Export app
|
||||
Copyright (c) 2022 Proton AG
|
||||
Copyright (c) 2023 Proton AG
|
||||
|
||||
This repository holds the Proton Mail Bridge and the Proton Mail Import-Export applications.
|
||||
For a detailed build information see [BUILDS](./BUILDS.md).
|
||||
|
@ -48,9 +48,6 @@ major problems.
|
|||
|
||||
## Environment Variables
|
||||
|
||||
### Bridge application
|
||||
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
|
||||
|
||||
### Dev build or run
|
||||
- `APP_VERSION`: set the bridge app version used during testing or building
|
||||
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
|
||||
|
@ -70,25 +67,26 @@ There are now three types of system folders which Bridge recognises:
|
|||
|--------|-------------------------------------|-----------------------------------------------------|-------------------------------------|---------------------------------------|
|
||||
| config | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.config/protonmail/bridge-v3 | $XDG_CONFIG_HOME/protonmail/bridge-v3 |
|
||||
| cache | %LOCALAPPDATA%\protonmail\bridge-v3 | ~/Library/Caches/protonmail/bridge-v3 | ~/.cache/protonmail/bridge-v3 | $XDG_CACHE_HOME/protonmail/bridge-v3 |
|
||||
| data | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.local/share/protonmail/bridge-v3 | $XDG_DATA_HOME/protonmail/bridge-v3 |
|
||||
| data | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.local/share/protonmail/bridge-v3 | $XDG_DATA_HOME/protonmail/bridge-v3 |
|
||||
| temp | %LOCALAPPDATA%\Temp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp |
|
||||
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
| | Base Dir | Path |
|
||||
|-----------------------|----------|----------------------------|
|
||||
| bridge lock file | cache | bridge.lock |
|
||||
| bridge-gui lock file | cache | bridge-gui.lock |
|
||||
| vault | config | vault.enc |
|
||||
| gRPC server json | config | grpcServerConfig.json |
|
||||
| gRPC client json | config | grpcClientConfig_<id>.json |
|
||||
| Logs | data | logs |
|
||||
| gluon DB | data | gluon/backend/db |
|
||||
| gluon messages | sata | gluon/backend/store |
|
||||
| Update files | data | updates |
|
||||
| sentry cache | data | sentry_cache |
|
||||
| Mac/Linux File Socket | temp | bridge_{RANDOM_UUID}.sock |
|
||||
| | Base Dir | Path |
|
||||
|------------------------|----------|----------------------------|
|
||||
| bridge lock file | cache | bridge.lock |
|
||||
| bridge-gui lock file | cache | bridge-gui.lock |
|
||||
| vault | config | vault.enc |
|
||||
| gRPC server json | config | grpcServerConfig.json |
|
||||
| gRPC client json | config | grpcClientConfig_<id>.json |
|
||||
| gRPC Focus server json | config | grpcFocusServerConfig.json |
|
||||
| Logs | data | logs |
|
||||
| gluon DB | data | gluon/backend/db |
|
||||
| gluon messages | data | gluon/backend/store |
|
||||
| Update files | data | updates |
|
||||
| sentry cache | data | sentry_cache |
|
||||
| Mac/Linux File Socket | temp | bridge{4_DIGITS} |
|
||||
|
||||
|
||||
|
|
86
go.mod
86
go.mod
|
@ -4,21 +4,20 @@ go 1.18
|
|||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230322121010-763723ee7bbc
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.15.1-0.20230331095629-e23a7a1be2a8
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230321105220-39e9131e1a68
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230331115846-7ba084061eaa
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||
github.com/bradenaw/juniper v0.8.0
|
||||
github.com/bradenaw/juniper v0.10.2
|
||||
github.com/cucumber/godog v0.12.5
|
||||
github.com/cucumber/messages-go/v16 v16.0.1
|
||||
github.com/docker/docker-credential-helpers v0.6.3
|
||||
github.com/elastic/go-sysinfo v1.8.1
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/emersion/go-message v0.16.0
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||
|
@ -26,7 +25,7 @@ require (
|
|||
github.com/fatih/color v1.13.0
|
||||
github.com/getsentry/sentry-go v0.15.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
github.com/goccy/go-json v0.9.11
|
||||
github.com/goccy/go-json v0.10.0
|
||||
github.com/godbus/dbus v4.1.0+incompatible
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
|
@ -35,36 +34,38 @@ require (
|
|||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
||||
github.com/keybase/go-keychain v0.0.0
|
||||
github.com/miekg/dns v1.1.50
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/profile v1.6.0
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/urfave/cli/v2 v2.20.3
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.24.4
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/goleak v1.2.0
|
||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/text v0.4.0
|
||||
google.golang.org/grpc v1.50.1
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/text v0.7.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
howett.net/plist v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.7.0 // indirect
|
||||
entgo.io/ent v0.11.2 // indirect
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
|
||||
entgo.io/ent v0.11.8 // indirect
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.5 // indirect
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.8.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/chzyer/test v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
||||
|
@ -73,48 +74,53 @@ require (
|
|||
github.com/elastic/go-windows v1.0.1 // indirect
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.8.1 // indirect
|
||||
github.com/gin-gonic/gin v1.9.0 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.2 // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.14.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.16.1 // indirect
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.10 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/zclconf/go-cty v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
github.com/zclconf/go-cty v1.12.1 // indirect
|
||||
golang.org/x/arch v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
|
223
go.sum
223
go.sum
|
@ -1,5 +1,5 @@
|
|||
ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k=
|
||||
ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
|
@ -13,44 +13,41 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
|
|||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
entgo.io/ent v0.11.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto=
|
||||
entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU=
|
||||
entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
|
||||
entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230322121010-763723ee7bbc h1:qLHEYjr7BJaZxeMyqhEBpenuAnduFNZqBA26gT9LXGo=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230322121010-763723ee7bbc/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
||||
github.com/ProtonMail/gluon v0.15.1-0.20230331095629-e23a7a1be2a8 h1:USMR8imbxkP4Ailch4ceV3hCZTaANMIGHhb5rpZFYn4=
|
||||
github.com/ProtonMail/gluon v0.15.1-0.20230331095629-e23a7a1be2a8/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230321105220-39e9131e1a68 h1:CExt0Vd19dsUtf+IBSa/l96/DTHEmgXi4IbWG99Vs1E=
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230321105220-39e9131e1a68/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 h1:dS7r5z4iGS0qCjM7UwWdsEMzQesUQbGcXdSm2/tWboA=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230331115846-7ba084061eaa h1:0JKWkz/gIYf+eky0dCFeBWrjEDLf59lS8HOlXtvn6Nk=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230331115846-7ba084061eaa/go.mod h1:RfpLBcTIhfjOIcBhh7f36LtAOEi0mqPd3t8gyLWmCZM=
|
||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
||||
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
|
@ -63,9 +60,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:2
|
|||
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/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
|
@ -75,19 +69,27 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
|||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bradenaw/juniper v0.8.0 h1:sdanLNdJbLjcLj993VYIwUHlUVkLzvgiD/x9O7cvvxk=
|
||||
github.com/bradenaw/juniper v0.8.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bradenaw/juniper v0.10.2 h1:EY7r8SJJrigJ7lvWk6ews3K5RD4XTG9z+WSwHJKijP4=
|
||||
github.com/bradenaw/juniper v0.10.2/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
|
||||
github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
|
||||
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
|
||||
github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk=
|
||||
github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
|
@ -96,7 +98,6 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
|
||||
|
@ -120,8 +121,8 @@ github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VR
|
|||
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317 h1:i0cBrdFLm8A/3hWEjn/BwdXLBplFJoZtu63p7bjrmaI=
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
|
@ -136,6 +137,8 @@ github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5
|
|||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
|
@ -144,8 +147,8 @@ github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2Ht
|
|||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
|
@ -153,20 +156,19 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
|||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
||||
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
|
@ -198,6 +200,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
|||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -239,12 +243,13 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
||||
github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg=
|
||||
github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
|
@ -261,16 +266,17 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
|||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
|
@ -282,13 +288,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
|
@ -314,17 +321,20 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
|
|||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM=
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
|
@ -344,9 +354,7 @@ github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
|||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
@ -355,7 +363,6 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
|
|||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
|
@ -376,24 +383,26 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cma
|
|||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/urfave/cli/v2 v2.20.3 h1:lOgGidH/N5loaigd9HjFsOIhXSTrzl7tBpHswZ428w4=
|
||||
github.com/urfave/cli/v2 v2.20.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
|
||||
github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
|
||||
github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
|
@ -402,16 +411,20 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
|
|||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
|
||||
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY=
|
||||
github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.2.0 h1:W1sUEHXiJTfjaFJ5SLo0N6lZn+0eO5gWD1MFeTGqQEY=
|
||||
golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
@ -420,18 +433,16 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
|||
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-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
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-20221023144134-a1e5550cf13e h1:SkwG94eNiiYJhbeDE018Grw09HIN/KB9NlRmZsrzfWs=
|
||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
@ -440,18 +451,16 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
|
|||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -471,9 +480,9 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
|||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -483,8 +492,10 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A=
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804/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-20220819030929-7fc1605a5dde/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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -494,7 +505,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -508,15 +518,18 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
@ -524,8 +537,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -546,11 +559,11 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa h1:uKcci2q7Qtp6nMTC/AAvfNUAldFtJuHWV9/5QWiypts=
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 h1:/X0t/E4VxbZE7MLS7auvE7YICHeVvbIa9vkOVvYW/24=
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -573,13 +586,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
|||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ=
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
|
||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
|
@ -598,11 +611,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -612,3 +622,4 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
|
|||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
|
|
@ -81,7 +81,7 @@ const (
|
|||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
)
|
||||
|
||||
func New() *cli.App { //nolint:funlen
|
||||
func New() *cli.App {
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = constants.FullAppName
|
||||
|
@ -156,7 +156,7 @@ func New() *cli.App { //nolint:funlen
|
|||
return app
|
||||
}
|
||||
|
||||
func run(c *cli.Context) error { //nolint:funlen
|
||||
func run(c *cli.Context) error {
|
||||
// Seed the default RNG from the math/rand package.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
|
@ -185,14 +185,14 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||
exe = os.Args[0]
|
||||
}
|
||||
|
||||
migrationErr := migrateOldVersions()
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||
migrationErr := migrateOldVersions()
|
||||
|
||||
// Run with profiling if requested.
|
||||
return withProfiler(c, func() error {
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||
// Run with profiling if requested.
|
||||
return withProfiler(c, func() error {
|
||||
// Load the locations where we store our files.
|
||||
return WithLocations(func(locations *locations.Locations) error {
|
||||
// Migrate the keychain helper.
|
||||
|
@ -208,9 +208,14 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||
}
|
||||
|
||||
// Ensure we are the only instance running.
|
||||
return withSingleInstance(locations, version, func() error {
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get settings path")
|
||||
}
|
||||
|
||||
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||
// Unlock the encrypted vault.
|
||||
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
||||
return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||
// Report insecure vault.
|
||||
if insecure {
|
||||
_ = reporter.ReportMessageWithContext("Vault is insecure", map[string]interface{}{})
|
||||
|
@ -221,27 +226,27 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||
_ = reporter.ReportMessageWithContext("Vault is corrupt", map[string]interface{}{})
|
||||
}
|
||||
|
||||
if !vault.Migrated() {
|
||||
if !v.Migrated() {
|
||||
// Migrate old settings into the vault.
|
||||
if err := migrateOldSettings(vault); err != nil {
|
||||
if err := migrateOldSettings(v); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate old settings")
|
||||
}
|
||||
|
||||
// Migrate old accounts into the vault.
|
||||
if err := migrateOldAccounts(locations, vault); err != nil {
|
||||
if err := migrateOldAccounts(locations, v); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate old accounts")
|
||||
}
|
||||
|
||||
// The vault has been migrated.
|
||||
if err := vault.SetMigrated(); err != nil {
|
||||
if err := v.SetMigrated(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to mark vault as migrated")
|
||||
}
|
||||
}
|
||||
|
||||
// Load the cookies from the vault.
|
||||
return withCookieJar(vault, func(cookieJar http.CookieJar) error {
|
||||
return withCookieJar(v, func(cookieJar http.CookieJar) error {
|
||||
// Create a new bridge instance.
|
||||
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, vault, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
|
||||
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
|
||||
if insecure {
|
||||
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
||||
b.PushError(bridge.ErrVaultInsecure)
|
||||
|
@ -266,15 +271,15 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||
}
|
||||
|
||||
// If there's another instance already running, try to raise it and exit.
|
||||
func withSingleInstance(locations *locations.Locations, version *semver.Version, fn func() error) error {
|
||||
func withSingleInstance(settingPath, lockFile string, version *semver.Version, fn func() error) error {
|
||||
logrus.Debug("Checking for other instances")
|
||||
defer logrus.Debug("Single instance stopped")
|
||||
|
||||
lock, err := checkSingleInstance(locations.GetLockFile(), version)
|
||||
lock, err := checkSingleInstance(settingPath, lockFile, version)
|
||||
if err != nil {
|
||||
logrus.Info("Another instance is already running; raising it")
|
||||
|
||||
if ok := focus.TryRaise(); !ok {
|
||||
if ok := focus.TryRaise(settingPath); !ok {
|
||||
return fmt.Errorf("another instance is already running but it could not be raised")
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"runtime"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
|
@ -40,13 +41,11 @@ import (
|
|||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const vaultSecretName = "bridge-vault-key"
|
||||
|
||||
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
||||
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
||||
|
||||
// withBridge creates creates and tears down the bridge.
|
||||
func withBridge( //nolint:funlen
|
||||
func withBridge(
|
||||
c *cli.Context,
|
||||
exe string,
|
||||
locations *locations.Locations,
|
||||
|
@ -79,7 +78,7 @@ func withBridge( //nolint:funlen
|
|||
)
|
||||
|
||||
// Create a proxy dialer which switches to a proxy if the request fails.
|
||||
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost)
|
||||
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost, crashHandler)
|
||||
|
||||
// Create the autostarter.
|
||||
autostarter := newAutostarter(exe)
|
||||
|
@ -110,6 +109,7 @@ func withBridge( //nolint:funlen
|
|||
// Crash and report stuff
|
||||
crashHandler,
|
||||
reporter,
|
||||
imap.DefaultEpochUIDValidityGenerator(),
|
||||
|
||||
// The logging stuff.
|
||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||
|
|
|
@ -46,7 +46,7 @@ func runFrontend(
|
|||
|
||||
switch {
|
||||
case c.Bool(flagCLI):
|
||||
return bridgeCLI.New(bridge, restarter, eventCh).Loop()
|
||||
return bridgeCLI.New(bridge, restarter, eventCh, crashHandler).Loop()
|
||||
|
||||
case c.Bool(flagNonInteractive):
|
||||
select {}
|
||||
|
|
|
@ -212,7 +212,6 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
|
|||
return nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
||||
var prefs struct {
|
||||
IMAPPort int `json:"user_port_imap,,string"`
|
||||
|
@ -290,14 +289,6 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
|||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate show all mail: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetSyncWorkers(prefs.FetchWorkers); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync workers: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetSyncAttPool(prefs.AttachmentWorkers); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync attachment pool: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetCookies([]byte(prefs.Cookies)); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate cookies: %w", err))
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||
|
@ -40,7 +41,7 @@ import (
|
|||
|
||||
func TestMigratePrefsToVaultWithKeys(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
|
@ -53,14 +54,15 @@ func TestMigratePrefsToVaultWithKeys(t *testing.T) {
|
|||
// Check Json Settings
|
||||
validateJSONPrefs(t, vault)
|
||||
|
||||
cert, key := vault.GetBridgeTLSCert()
|
||||
// Check the keys were found and collected.
|
||||
require.Equal(t, "-----BEGIN CERTIFICATE-----", string(vault.GetBridgeTLSCert()))
|
||||
require.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(vault.GetBridgeTLSKey()))
|
||||
require.Equal(t, "-----BEGIN CERTIFICATE-----", string(cert))
|
||||
require.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(key))
|
||||
}
|
||||
|
||||
func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
|
@ -70,12 +72,16 @@ func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
|
|||
// Migrate the old prefs file to the new vault.
|
||||
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||
|
||||
// Migrate the old prefs file to the new vault.
|
||||
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||
|
||||
// Check Json Settings
|
||||
validateJSONPrefs(t, vault)
|
||||
|
||||
// Check the keys were found and collected.
|
||||
require.NotEqual(t, []byte("-----BEGIN CERTIFICATE-----"), vault.GetBridgeTLSCert())
|
||||
require.NotEqual(t, []byte("-----BEGIN RSA PRIVATE KEY-----"), vault.GetBridgeTLSKey())
|
||||
cert, key := vault.GetBridgeTLSCert()
|
||||
require.NotEqual(t, []byte("-----BEGIN CERTIFICATE-----"), cert)
|
||||
require.NotEqual(t, []byte("-----BEGIN RSA PRIVATE KEY-----"), key)
|
||||
}
|
||||
|
||||
func TestKeychainMigration(t *testing.T) {
|
||||
|
@ -168,7 +174,7 @@ func TestUserMigration(t *testing.T) {
|
|||
token, err := crypto.RandomToken(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token)
|
||||
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
|
@ -206,8 +212,6 @@ func validateJSONPrefs(t *testing.T, vault *vault.Vault) {
|
|||
require.True(t, vault.GetAutostart())
|
||||
|
||||
// Check that the other app settings have been migrated.
|
||||
require.Equal(t, 16, vault.SyncWorkers())
|
||||
require.Equal(t, 16, vault.SyncAttPool())
|
||||
require.False(t, vault.GetProxyAllowed())
|
||||
require.False(t, vault.GetShowAllMail())
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import (
|
|||
//
|
||||
// For macOS and Linux when already running version is older than this instance
|
||||
// it will kill old and continue with this new bridge (i.e. no error returned).
|
||||
func checkSingleInstance(lockFilePath string, curVersion *semver.Version) (*os.File, error) {
|
||||
func checkSingleInstance(settingPath, lockFilePath string, curVersion *semver.Version) (*os.File, error) {
|
||||
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
|
||||
logrus.WithField("path", lockFilePath).Debug("Created lock file; no other instance is running")
|
||||
return lock, nil
|
||||
|
@ -44,7 +44,7 @@ func checkSingleInstance(lockFilePath string, curVersion *semver.Version) (*os.F
|
|||
|
||||
// We couldn't create the lock file, so another instance is probably running.
|
||||
// Check if it's an older version of the app.
|
||||
lastVersion, ok := focus.TryVersion()
|
||||
lastVersion, ok := focus.TryVersion(settingPath)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to determine version of running instance")
|
||||
}
|
||||
|
|
|
@ -18,26 +18,24 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool) error) error {
|
||||
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
||||
logrus.Debug("Creating vault")
|
||||
defer logrus.Debug("Vault stopped")
|
||||
|
||||
// Create the encVault.
|
||||
encVault, insecure, corrupt, err := newVault(locations)
|
||||
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
@ -51,7 +49,9 @@ func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool)
|
|||
if installed := encVault.GetCertsInstalled(); !installed {
|
||||
logrus.Debug("Installing certificates")
|
||||
|
||||
if err := certs.NewInstaller().InstallCert(encVault.GetBridgeTLSCert()); err != nil {
|
||||
certPEM, _ := encVault.GetBridgeTLSCert()
|
||||
|
||||
if err := certs.NewInstaller().InstallCert(certPEM); err != nil {
|
||||
return fmt.Errorf("failed to install certs: %w", err)
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool)
|
|||
return fn(encVault, insecure, corrupt)
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) {
|
||||
func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||
|
@ -80,7 +80,7 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
|||
insecure bool
|
||||
)
|
||||
|
||||
if key, err := getVaultKey(vaultDir); err != nil {
|
||||
if key, err := loadVaultKey(vaultDir); err != nil {
|
||||
logrus.WithError(err).Error("Could not load/create vault key")
|
||||
insecure = true
|
||||
|
||||
|
@ -95,7 +95,7 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
|||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey)
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
@ -103,42 +103,25 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
|||
return vault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
func getVaultKey(vaultDir string) ([]byte, error) {
|
||||
func loadVaultKey(vaultDir string) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
keychain, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
secrets, err := keychain.List()
|
||||
has, err := vault.HasVaultKey(kc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not list keychain: %w", err)
|
||||
return nil, fmt.Errorf("could not check for vault key: %w", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(secrets, vaultSecretName) {
|
||||
tok, err := crypto.RandomToken(32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate random token: %w", err)
|
||||
}
|
||||
|
||||
if err := keychain.Put(vaultSecretName, base64.StdEncoding.EncodeToString(tok)); err != nil {
|
||||
return nil, fmt.Errorf("could not put keychain item: %w", err)
|
||||
}
|
||||
if has {
|
||||
return vault.GetVaultKey(kc)
|
||||
}
|
||||
|
||||
_, keyEnc, err := keychain.Get(vaultSecretName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain item: %w", err)
|
||||
}
|
||||
|
||||
keyDec, err := base64.StdEncoding.DecodeString(keyEnc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not decode keychain item: %w", err)
|
||||
}
|
||||
|
||||
return keyDec, nil
|
||||
return vault.NewVaultKey(kc)
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
// 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 async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Abortable collects groups of functions that can be aborted by calling Abort.
|
||||
type Abortable struct {
|
||||
abortFunc []context.CancelFunc
|
||||
abortLock sync.RWMutex
|
||||
}
|
||||
|
||||
func (a *Abortable) Do(ctx context.Context, fn func(context.Context)) {
|
||||
fn(a.newCancelCtx(ctx))
|
||||
}
|
||||
|
||||
func (a *Abortable) Abort() {
|
||||
a.abortLock.RLock()
|
||||
defer a.abortLock.RUnlock()
|
||||
|
||||
for _, fn := range a.abortFunc {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Abortable) newCancelCtx(ctx context.Context) context.Context {
|
||||
a.abortLock.Lock()
|
||||
defer a.abortLock.Unlock()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
a.abortFunc = append(a.abortFunc, cancel)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// RangeContext iterates over the given channel until the context is canceled or the
|
||||
// channel is closed.
|
||||
func RangeContext[T any](ctx context.Context, ch <-chan T, fn func(T)) {
|
||||
for {
|
||||
select {
|
||||
case v, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fn(v)
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ForwardContext forwards all values from the src channel to the dst channel until the
|
||||
// context is canceled or the src channel is closed.
|
||||
func ForwardContext[T any](ctx context.Context, dst chan<- T, src <-chan T) {
|
||||
RangeContext(ctx, src, func(v T) {
|
||||
select {
|
||||
case dst <- v:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
// 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 async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PanicHandler interface {
|
||||
HandlePanic()
|
||||
}
|
||||
|
||||
// Group is forked and improved version of "github.com/bradenaw/juniper/xsync.Group".
|
||||
//
|
||||
// It manages a group of goroutines. The main change to original is posibility
|
||||
// to wait passed function to finish without canceling it's context and adding
|
||||
// PanicHandler.
|
||||
type Group struct {
|
||||
baseCtx context.Context
|
||||
ctx context.Context
|
||||
jobCtx context.Context
|
||||
cancel context.CancelFunc
|
||||
finish context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
panicHandler PanicHandler
|
||||
}
|
||||
|
||||
// NewGroup returns a Group ready for use. The context passed to any of the f functions will be a
|
||||
// descendant of ctx.
|
||||
func NewGroup(ctx context.Context, panicHandler PanicHandler) *Group {
|
||||
bgCtx, cancel := context.WithCancel(ctx)
|
||||
jobCtx, finish := context.WithCancel(ctx)
|
||||
return &Group{
|
||||
baseCtx: ctx,
|
||||
ctx: bgCtx,
|
||||
jobCtx: jobCtx,
|
||||
cancel: cancel,
|
||||
finish: finish,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Once calls f once from another goroutine.
|
||||
func (g *Group) Once(f func(ctx context.Context)) {
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
f(g.ctx)
|
||||
g.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// jitterDuration returns a random duration in [d - jitter, d + jitter].
|
||||
func jitterDuration(d time.Duration, jitter time.Duration) time.Duration {
|
||||
return d + time.Duration(float64(jitter)*((rand.Float64()*2)-1)) //nolint:gosec
|
||||
}
|
||||
|
||||
// Periodic spawns a goroutine that calls f once per interval +/- jitter.
|
||||
func (g *Group) Periodic(
|
||||
interval time.Duration,
|
||||
jitter time.Duration,
|
||||
f func(ctx context.Context),
|
||||
) {
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
t := time.NewTimer(jitterDuration(interval, jitter))
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Trigger spawns a goroutine which calls f whenever the returned function is called. If f is
|
||||
// already running when triggered, f will run again immediately when it finishes.
|
||||
func (g *Group) Trigger(f func(ctx context.Context)) func() {
|
||||
c := make(chan struct{}, 1)
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-c:
|
||||
}
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
select {
|
||||
case c <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PeriodicOrTrigger spawns a goroutine which calls f whenever the returned function is called. If
|
||||
// f is already running when triggered, f will run again immediately when it finishes. Also calls f
|
||||
// when it has been interval+/-jitter since the last trigger.
|
||||
func (g *Group) PeriodicOrTrigger(
|
||||
interval time.Duration,
|
||||
jitter time.Duration,
|
||||
f func(ctx context.Context),
|
||||
) func() {
|
||||
c := make(chan struct{}, 1)
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
t := time.NewTimer(jitterDuration(interval, jitter))
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
case <-c:
|
||||
if !t.Stop() {
|
||||
<-t.C
|
||||
}
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
}
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
select {
|
||||
case c <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) resetCtx() {
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
g.ctx, g.cancel = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
// Cancel is send to all of the spawn goroutines and ends periodic
|
||||
// or trigger routines.
|
||||
func (g *Group) Cancel() {
|
||||
g.cancel()
|
||||
g.finish()
|
||||
g.resetCtx()
|
||||
}
|
||||
|
||||
// Finish will ends all periodic or polls routines. It will let
|
||||
// currently running functions to finish (cancel is not sent).
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) Finish() {
|
||||
g.finish()
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
// CancelAndWait cancels the context passed to any of the spawned goroutines and waits for all spawned
|
||||
// goroutines to exit.
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) CancelAndWait() {
|
||||
g.finish()
|
||||
g.cancel()
|
||||
g.wg.Wait()
|
||||
g.resetCtx()
|
||||
}
|
||||
|
||||
// WaitToFinish will ends all periodic or polls routines. It will wait for
|
||||
// currently running functions to finish (cancel is not sent).
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) WaitToFinish() {
|
||||
g.finish()
|
||||
g.wg.Wait()
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
func (g *Group) handlePanic() {
|
||||
if g.panicHandler != nil {
|
||||
g.panicHandler.HandlePanic()
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -32,14 +33,14 @@ func defaultAPIOptions(
|
|||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
panicHandler async.PanicHandler,
|
||||
) []proton.Option {
|
||||
return []proton.Option{
|
||||
proton.WithHostURL(apiURL),
|
||||
proton.WithAppVersion(constants.AppVersion(version.Original())),
|
||||
proton.WithCookieJar(cookieJar),
|
||||
proton.WithTransport(transport),
|
||||
proton.WithAttPoolSize(poolSize),
|
||||
proton.WithLogger(logrus.StandardLogger()),
|
||||
proton.WithPanicHandler(panicHandler),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
|
@ -32,7 +33,7 @@ func newAPIOptions(
|
|||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
panicHandler async.PanicHandler,
|
||||
) []proton.Option {
|
||||
return defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
||||
return defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
|
@ -33,9 +34,9 @@ func newAPIOptions(
|
|||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
panicHandler async.PanicHandler,
|
||||
) []proton.Option {
|
||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
|
||||
|
||||
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
|
||||
opt = append(opt, proton.WithHostURL(host))
|
||||
|
|
|
@ -30,11 +30,12 @@ import (
|
|||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/watcher"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
|
@ -92,8 +93,8 @@ type Bridge struct {
|
|||
// locator is the bridge's locator.
|
||||
locator Locator
|
||||
|
||||
// crashHandler
|
||||
crashHandler async.PanicHandler
|
||||
// panicHandler
|
||||
panicHandler async.PanicHandler
|
||||
|
||||
// reporter
|
||||
reporter reporter.Reporter
|
||||
|
@ -124,10 +125,12 @@ type Bridge struct {
|
|||
|
||||
// goUpdate triggers a check/install of updates.
|
||||
goUpdate func()
|
||||
|
||||
uidValidityGenerator imap.UIDValidityGenerator
|
||||
}
|
||||
|
||||
// New creates a new bridge.
|
||||
func New( //nolint:funlen
|
||||
func New(
|
||||
locator Locator, // the locator to provide paths to store data
|
||||
vault *vault.Vault, // the bridge's encrypted data store
|
||||
autostarter Autostarter, // the autostarter to manage autostart settings
|
||||
|
@ -140,17 +143,18 @@ func New( //nolint:funlen
|
|||
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||
proxyCtl ProxyController, // the DoH controller
|
||||
crashHandler async.PanicHandler,
|
||||
panicHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
|
||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||
logSMTP bool, // whether to log SMTP activity
|
||||
) (*Bridge, <-chan events.Event, error) {
|
||||
// api is the user's API manager.
|
||||
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, vault.SyncAttPool())...)
|
||||
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, panicHandler)...)
|
||||
|
||||
// tasks holds all the bridge's background tasks.
|
||||
tasks := async.NewGroup(context.Background(), crashHandler)
|
||||
tasks := async.NewGroup(context.Background(), panicHandler)
|
||||
|
||||
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
|
||||
imapEventCh := make(chan imapEvents.Event)
|
||||
|
@ -165,12 +169,13 @@ func New( //nolint:funlen
|
|||
autostarter,
|
||||
updater,
|
||||
curVersion,
|
||||
crashHandler,
|
||||
panicHandler,
|
||||
reporter,
|
||||
|
||||
api,
|
||||
identifier,
|
||||
proxyCtl,
|
||||
uidValidityGenerator,
|
||||
logIMAPClient, logIMAPServer, logSMTP,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -185,22 +190,9 @@ func New( //nolint:funlen
|
|||
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
|
||||
}
|
||||
|
||||
// Start serving IMAP.
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
logrus.WithError(err).Error("IMAP error")
|
||||
bridge.PushError(ErrServeIMAP)
|
||||
}
|
||||
|
||||
// Start serving SMTP.
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("SMTP error")
|
||||
bridge.PushError(ErrServeSMTP)
|
||||
}
|
||||
|
||||
return bridge, eventCh, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newBridge(
|
||||
tasks *async.Group,
|
||||
imapEventCh chan imapEvents.Event,
|
||||
|
@ -210,12 +202,13 @@ func newBridge(
|
|||
autostarter Autostarter,
|
||||
updater Updater,
|
||||
curVersion *semver.Version,
|
||||
crashHandler async.PanicHandler,
|
||||
panicHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
|
||||
api *proton.Manager,
|
||||
identifier Identifier,
|
||||
proxyCtl ProxyController,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
|
||||
logIMAPClient, logIMAPServer, logSMTP bool,
|
||||
) (*Bridge, error) {
|
||||
|
@ -254,12 +247,14 @@ func newBridge(
|
|||
logIMAPServer,
|
||||
imapEventCh,
|
||||
tasks,
|
||||
uidValidityGenerator,
|
||||
panicHandler,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||
}
|
||||
|
||||
focusService, err := focus.NewService(curVersion)
|
||||
focusService, err := focus.NewService(locator, curVersion, panicHandler)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
}
|
||||
|
@ -285,7 +280,7 @@ func newBridge(
|
|||
newVersion: curVersion,
|
||||
newVersionLock: safe.NewRWMutex(),
|
||||
|
||||
crashHandler: crashHandler,
|
||||
panicHandler: panicHandler,
|
||||
reporter: reporter,
|
||||
|
||||
focusService: focusService,
|
||||
|
@ -300,6 +295,8 @@ func newBridge(
|
|||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
|
||||
uidValidityGenerator: uidValidityGenerator,
|
||||
}
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
||||
|
@ -307,7 +304,6 @@ func newBridge(
|
|||
return bridge, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
// Enable or disable the proxy at startup.
|
||||
if bridge.vault.GetProxyAllowed() {
|
||||
|
@ -376,16 +372,32 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||
})
|
||||
})
|
||||
|
||||
// Attempt to lazy load users when triggered.
|
||||
// We need to load users before we can start the IMAP and SMTP servers.
|
||||
// We must only start the servers once.
|
||||
var once sync.Once
|
||||
|
||||
// Attempt to load users from the vault when triggered.
|
||||
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
||||
if err := bridge.loadUsers(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to load users")
|
||||
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
|
||||
sentry.ReportError(bridge.reporter, "Failed to load users", err)
|
||||
}
|
||||
} else {
|
||||
bridge.publish(events.AllUsersLoaded{})
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
|
@ -484,7 +496,7 @@ func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events
|
|||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
watcher := watcher.New(ofType...)
|
||||
watcher := watcher.New(bridge.panicHandler, ofType...)
|
||||
|
||||
bridge.watchers = append(bridge.watchers, watcher)
|
||||
|
||||
|
@ -545,7 +557,7 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
|||
}
|
||||
|
||||
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey())
|
||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -29,6 +30,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/go-proton-api/server/backend"
|
||||
|
@ -121,8 +124,11 @@ func TestBridge_Focus(t *testing.T) {
|
|||
raiseCh, done := bridge.GetEvents(events.Raise{})
|
||||
defer done()
|
||||
|
||||
settingsFolder, err := locator.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate a focus event.
|
||||
focus.TryRaise()
|
||||
focus.TryRaise(settingsFolder)
|
||||
|
||||
// Wait for the event.
|
||||
require.IsType(t, events.Raise{}, <-raiseCh)
|
||||
|
@ -496,6 +502,21 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginFailed(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
|
||||
defer done()
|
||||
|
||||
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, imapClient.Login("badUser", "badPass"))
|
||||
require.Equal(t, "badUser", (<-failCh).Username)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", password)
|
||||
|
@ -657,6 +678,9 @@ func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
|
|||
tests(mocks)
|
||||
}
|
||||
|
||||
// Needs to be global to survive bridge shutdown/startup in unit tests as they happen to fast.
|
||||
var testUIDValidityGenerator = imap.DefaultEpochUIDValidityGenerator()
|
||||
|
||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||
func withBridgeNoMocks(
|
||||
ctx context.Context,
|
||||
|
@ -676,7 +700,7 @@ func withBridgeNoMocks(
|
|||
require.NoError(t, err)
|
||||
|
||||
// Create the vault.
|
||||
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey)
|
||||
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new cookie jar.
|
||||
|
@ -702,6 +726,7 @@ func withBridgeNoMocks(
|
|||
mocks.ProxyCtl,
|
||||
mocks.CrashHandler,
|
||||
mocks.Reporter,
|
||||
testUIDValidityGenerator,
|
||||
|
||||
// The logging stuff.
|
||||
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
||||
|
@ -713,6 +738,10 @@ func withBridgeNoMocks(
|
|||
|
||||
// Wait for bridge to finish loading users.
|
||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||
// Wait for bridge to start the IMAP server.
|
||||
waitForEvent(t, eventCh, events.IMAPServerReady{})
|
||||
// Wait for bridge to start the SMTP server.
|
||||
waitForEvent(t, eventCh, events.SMTPServerReady{})
|
||||
|
||||
// Set random IMAP and SMTP ports for the tests.
|
||||
require.NoError(t, bridge.SetIMAPPort(0))
|
||||
|
@ -742,7 +771,7 @@ func withBridge(
|
|||
})
|
||||
}
|
||||
|
||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, _ T) {
|
||||
t.Helper()
|
||||
|
||||
for event := range eventCh {
|
||||
|
|
|
@ -37,7 +37,7 @@ const (
|
|||
MaxCompressedFilesCount = 6
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { //nolint:funlen
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
||||
var account string
|
||||
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
|
|
|
@ -22,10 +22,7 @@ import "errors"
|
|||
var (
|
||||
ErrVaultInsecure = errors.New("the vault is insecure")
|
||||
ErrVaultCorrupt = errors.New("the vault is corrupt")
|
||||
|
||||
ErrServeIMAP = errors.New("failed to serve IMAP")
|
||||
ErrServeSMTP = errors.New("failed to serve SMTP")
|
||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||
|
||||
ErrNoSuchUser = errors.New("no such user")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
|
|
|
@ -27,11 +27,14 @@ import (
|
|||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/store"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/gluon/store/fallback_v0"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
|
@ -44,26 +47,42 @@ const (
|
|||
)
|
||||
|
||||
func (bridge *Bridge) serveIMAP() error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
port, err := func() (int, error) {
|
||||
if bridge.imapServer == nil {
|
||||
return 0, fmt.Errorf("no IMAP server instance running")
|
||||
}
|
||||
|
||||
logrus.Info("Starting IMAP server")
|
||||
logrus.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
|
||||
}()
|
||||
|
||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||
bridge.publish(events.IMAPServerError{
|
||||
Error: err,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.imapListener = imapListener
|
||||
|
||||
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
|
||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to store IMAP port in vault: %w", err)
|
||||
}
|
||||
bridge.publish(events.IMAPServerReady{
|
||||
Port: port,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -75,6 +94,8 @@ func (bridge *Bridge) restartIMAP() error {
|
|||
if err := bridge.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
}
|
||||
|
||||
return bridge.serveIMAP()
|
||||
|
@ -87,6 +108,7 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
|||
if err := bridge.imapServer.Close(ctx); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP server: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapServer = nil
|
||||
}
|
||||
|
||||
|
@ -96,12 +118,12 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addIMAPUser connects the given user to gluon.
|
||||
//
|
||||
//nolint:funlen
|
||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
|
@ -242,6 +264,13 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
|||
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
|
||||
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
|
||||
}
|
||||
|
||||
case imapEvents.LoginFailed:
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"sessionID": event.SessionID,
|
||||
"username": event.Username,
|
||||
}).Info("Received IMAP login failure notification")
|
||||
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,7 +290,6 @@ func ApplyGluonConfigPathSuffix(basePath string) string {
|
|||
return filepath.Join(basePath, "backend", "db")
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newIMAPServer(
|
||||
gluonCacheDir, gluonConfigDir string,
|
||||
version *semver.Version,
|
||||
|
@ -270,6 +298,8 @@ func newIMAPServer(
|
|||
logClient, logServer bool,
|
||||
eventCh chan<- imapEvents.Event,
|
||||
tasks *async.Group,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
panicHandler async.PanicHandler,
|
||||
) (*gluon.Server, error) {
|
||||
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
||||
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
||||
|
@ -313,6 +343,8 @@ func newIMAPServer(
|
|||
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||
getGluonVersionInfo(version),
|
||||
gluon.WithReporter(reporter),
|
||||
gluon.WithUIDValidityGenerator(uidValidityGenerator),
|
||||
gluon.WithPanicHandler(panicHandler),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -348,7 +380,7 @@ func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, e
|
|||
return store.NewOnDiskStore(
|
||||
filepath.Join(path, userID),
|
||||
passphrase,
|
||||
store.WithCompressor(new(store.GZipCompressor)),
|
||||
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
var uidValidities = make(map[string]uint32, len(names))
|
||||
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
@ -73,7 +74,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1000), status.UidValidity)
|
||||
uidValidities[name] = status.UidValidity
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -106,7 +107,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1001), status.UidValidity)
|
||||
require.Greater(t, status.UidValidity, uidValidities[name])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -330,3 +330,182 @@ func TestBridge_SendInvite(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
||||
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||
Subject: A new message
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Disposition: inline;
|
||||
filename=Cat_August_2010-4.jpeg
|
||||
Content-Type: image/jpeg;
|
||||
name="Cat_August_2010-4.jpeg"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
SGVsbG8gd29ybGQ=
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||
`
|
||||
|
||||
const messageMultipartWithText = `Content-Type: multipart/mixed;
|
||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||
Subject: A new message Part2
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Disposition: inline;
|
||||
filename=Cat_August_2010-4.jpeg
|
||||
Content-Type: image/jpeg;
|
||||
name="Cat_August_2010-4.jpeg"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
SGVsbG8gd29ybGQ=
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Type: text/html;charset=utf8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Hello world
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||
`
|
||||
|
||||
const messageWithTextOnly = `Content-Type: text/plain;charset=utf8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Subject: A new message Part3
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
Hello world
|
||||
|
||||
`
|
||||
|
||||
const messageMultipartWithoutTextWithTextAttachment = `Content-Type: multipart/mixed;
|
||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||
Subject: A new message Part4
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Type: text/plain; charset=UTF-8; name="text.txt"
|
||||
Content-Disposition: attachment; filename="text.txt"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
SGVsbG8gd29ybGQK
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||
`
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
_, _, err := s.CreateUser("recipient", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
messages := []string{
|
||||
messageMultipartWithoutText,
|
||||
messageMultipartWithText,
|
||||
messageWithTextOnly,
|
||||
messageMultipartWithoutTextWithTextAttachment,
|
||||
}
|
||||
|
||||
for _, m := range messages {
|
||||
// Dial the server.
|
||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||
require.NoError(t, err)
|
||||
defer client.Close() //nolint:errcheck
|
||||
|
||||
// Upgrade to TLS.
|
||||
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||
|
||||
// Authorize with SASL LOGIN.
|
||||
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||
senderInfo.Addresses[0],
|
||||
string(senderInfo.BridgePass)),
|
||||
))
|
||||
|
||||
// Send the message.
|
||||
require.NoError(t, client.SendMail(
|
||||
senderInfo.Addresses[0],
|
||||
[]string{recipientInfo.Addresses[0]},
|
||||
strings.NewReader(m),
|
||||
))
|
||||
}
|
||||
|
||||
// Connect the sender IMAP client.
|
||||
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
||||
defer senderIMAPClient.Logout() //nolint:errcheck
|
||||
|
||||
// Connect the recipient IMAP client.
|
||||
recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
||||
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, len(messages))
|
||||
|
||||
// messages may not be in order
|
||||
for _, message := range messages {
|
||||
switch {
|
||||
case message.Envelope.Subject == "A new message":
|
||||
// The message that was sent should now include an empty text/plain body part since there was none
|
||||
// in the original message.
|
||||
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||
|
||||
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||
require.Equal(t, "plain", message.BodyStructure.Parts[0].MIMESubType)
|
||||
require.Equal(t, uint32(0), message.BodyStructure.Parts[0].Size)
|
||||
require.Equal(t, "image", message.BodyStructure.Parts[1].MIMEType)
|
||||
require.Equal(t, "jpeg", message.BodyStructure.Parts[1].MIMESubType)
|
||||
|
||||
case message.Envelope.Subject == "A new message Part2":
|
||||
// This message already has a text body, should be unchanged
|
||||
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||
|
||||
require.Equal(t, "image", message.BodyStructure.Parts[1].MIMEType)
|
||||
require.Equal(t, "jpeg", message.BodyStructure.Parts[1].MIMESubType)
|
||||
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||
require.Equal(t, "html", message.BodyStructure.Parts[0].MIMESubType)
|
||||
|
||||
case message.Envelope.Subject == "A new message Part3":
|
||||
// This message already has a text body, should be unchanged
|
||||
require.Equal(t, 0, len(message.BodyStructure.Parts))
|
||||
|
||||
require.Equal(t, "text", message.BodyStructure.MIMEType)
|
||||
require.Equal(t, "plain", message.BodyStructure.MIMESubType)
|
||||
|
||||
case message.Envelope.Subject == "A new message Part4":
|
||||
// The message that was sent should now include an empty text/plain body part since even though
|
||||
// there was only a text/plain attachment in the original message.
|
||||
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||
|
||||
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||
require.Equal(t, "plain", message.BodyStructure.Parts[0].MIMESubType)
|
||||
require.Equal(t, uint32(0), message.BodyStructure.Parts[0].Size)
|
||||
require.Equal(t, "text", message.BodyStructure.Parts[1].MIMEType)
|
||||
require.Equal(t, "plain", message.BodyStructure.Parts[1].MIMESubType)
|
||||
require.Equal(t, "attachment", message.BodyStructure.Parts[1].Disposition)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -25,11 +25,9 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -131,26 +129,21 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
|||
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||
}
|
||||
|
||||
if err := bridge.stopEventLoops(); err != nil {
|
||||
return err
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := bridge.startEventLoops(ctx)
|
||||
if err != nil {
|
||||
panic(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 {
|
||||
panic(err)
|
||||
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get Gluon Database directory: %w", err))
|
||||
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
|
@ -163,13 +156,25 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
|||
bridge.logIMAPServer,
|
||||
bridge.imapEventCh,
|
||||
bridge.tasks,
|
||||
bridge.uidValidityGenerator,
|
||||
bridge.panicHandler,
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to create new IMAP server: %w", err))
|
||||
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)
|
||||
}
|
||||
|
@ -191,34 +196,6 @@ func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) stopEventLoops() error {
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) startEventLoops(ctx context.Context) error {
|
||||
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 {
|
||||
panic(fmt.Errorf("failed to serve IMAP: %w", err))
|
||||
}
|
||||
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
panic(fmt.Errorf("failed to serve SMTP: %w", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||
return bridge.vault.GetProxyAllowed()
|
||||
}
|
||||
|
@ -332,6 +309,9 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
|
|||
return bridge.vault.SetColorScheme(colorScheme)
|
||||
}
|
||||
|
||||
// FactoryReset deletes all users, wipes the vault, and deletes all files.
|
||||
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
|
||||
// which we need at next startup to decrypt the vault.
|
||||
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
// Delete all the users.
|
||||
safe.Lock(func() {
|
||||
|
@ -348,22 +328,10 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
|||
logrus.WithError(err).Error("Failed to reset vault")
|
||||
}
|
||||
|
||||
// Then delete all files.
|
||||
if err := bridge.locator.Clear(); err != nil {
|
||||
// Lastly, delete all files except the vault.
|
||||
if err := bridge.locator.Clear(bridge.vault.Path()); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear data paths")
|
||||
}
|
||||
|
||||
// Lastly clear the keychain.
|
||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get vault dir")
|
||||
} else if helper, err := vault.GetHelper(vaultDir); err != nil {
|
||||
logrus.WithError(err).Error("Failed to get keychain helper")
|
||||
} else if keychain, err := keychain.NewKeychain(helper, constants.KeyChainName); err != nil {
|
||||
logrus.WithError(err).Error("Failed to get keychain")
|
||||
} else if err := keychain.Clear(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear keychain")
|
||||
}
|
||||
}
|
||||
|
||||
func getPort(addr net.Addr) int {
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"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"
|
||||
|
@ -31,25 +32,41 @@ import (
|
|||
)
|
||||
|
||||
func (bridge *Bridge) serveSMTP() error {
|
||||
logrus.Info("Starting SMTP server")
|
||||
port, err := func() (int, error) {
|
||||
logrus.Info("Starting SMTP server")
|
||||
|
||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return 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")
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to store SMTP port in vault: %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
|
||||
}
|
||||
|
||||
|
@ -60,6 +77,8 @@ func (bridge *Bridge) restartSMTP() error {
|
|||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||
|
||||
return bridge.serveSMTP()
|
||||
|
@ -82,6 +101,8 @@ func (bridge *Bridge) closeSMTP() error {
|
|||
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
|
@ -351,7 +352,7 @@ func withClient(ctx context.Context, t *testing.T, s *server.Server, username st
|
|||
fn(ctx, c)
|
||||
}
|
||||
|
||||
func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) {
|
||||
func clientFetch(client *client.Client, mailbox string, extraItems ...imap.FetchItem) ([]*imap.Message, error) {
|
||||
status, err := client.Select(mailbox, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -363,10 +364,13 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error)
|
|||
|
||||
resCh := make(chan *imap.Message)
|
||||
|
||||
fetchItems := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure, "BODY.PEEK[]"}
|
||||
fetchItems = append(fetchItems, extraItems...)
|
||||
|
||||
go func() {
|
||||
if err := client.Fetch(
|
||||
&imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}},
|
||||
[]imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, "BODY.PEEK[]"},
|
||||
fetchItems,
|
||||
resCh,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
|
@ -425,13 +429,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||
keyPass, err := salt.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
|
||||
_, addrKRs, err := proton.Unlock(user, addr, keyPass, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok := addrKRs[addrID]
|
||||
require.True(t, ok)
|
||||
|
||||
res, err := stream.Collect(ctx, c.ImportMessages(
|
||||
str, err := c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
runtime.NumCPU(),
|
||||
|
@ -446,7 +450,10 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||
Message: message,
|
||||
}
|
||||
})...,
|
||||
))
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := stream.Collect(ctx, str)
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Map(res, func(res proton.ImportRes) string {
|
||||
|
|
|
@ -18,5 +18,9 @@
|
|||
package bridge
|
||||
|
||||
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
|
||||
return bridge.vault.GetBridgeTLSCert(), bridge.vault.GetBridgeTLSKey()
|
||||
return bridge.vault.GetBridgeTLSCert()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetBridgeTLSCertPath(certPath, keyPath string) error {
|
||||
return bridge.vault.SetBridgeTLSCertPath(certPath, keyPath)
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ type Locator interface {
|
|||
ProvideGluonDataPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear() error
|
||||
Clear(...string) error
|
||||
}
|
||||
|
||||
type Identifier interface {
|
||||
|
|
|
@ -32,19 +32,7 @@ func (bridge *Bridge) CheckForUpdates() {
|
|||
}
|
||||
|
||||
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"version": version.Version,
|
||||
"current": bridge.curVersion,
|
||||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
select {
|
||||
case bridge.installCh <- installJob{version: version, silent: false}:
|
||||
log.Info("The update will be installed manually")
|
||||
|
||||
default:
|
||||
log.Info("An update is already being installed")
|
||||
}
|
||||
bridge.installCh <- installJob{version: version, silent: false}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
||||
|
@ -89,17 +77,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
|||
|
||||
default:
|
||||
safe.RLock(func() {
|
||||
if version.Version.GreaterThan(bridge.newVersion) {
|
||||
log.Info("An update is available")
|
||||
|
||||
select {
|
||||
case bridge.installCh <- installJob{version: version, silent: true}:
|
||||
log.Info("The update will be installed silently")
|
||||
|
||||
default:
|
||||
log.Info("An update is already being installed")
|
||||
}
|
||||
}
|
||||
bridge.installCh <- installJob{version: version, silent: true}
|
||||
}, bridge.newVersionLock)
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +95,12 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
if !job.version.Version.GreaterThan(bridge.newVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
log.WithField("silent", job.silent).Info("An update is available")
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
Version: job.version,
|
||||
Compatible: true,
|
||||
|
@ -142,6 +126,7 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||
Silent: job.silent,
|
||||
Error: err,
|
||||
})
|
||||
|
||||
default:
|
||||
log.Info("The update was installed successfully")
|
||||
|
||||
|
|
|
@ -23,10 +23,10 @@ import (
|
|||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
|
@ -434,6 +434,7 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
|||
logrus.WithError(err).Warn("Failed to clear user secrets")
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to create API client: %w", err)
|
||||
}
|
||||
|
||||
|
@ -515,9 +516,9 @@ func (bridge *Bridge) addUserWithVault(
|
|||
client,
|
||||
bridge.reporter,
|
||||
apiUser,
|
||||
bridge.crashHandler,
|
||||
bridge.vault.SyncWorkers(),
|
||||
bridge.panicHandler,
|
||||
bridge.vault.GetShowAllMail(),
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
|
|
|
@ -20,18 +20,24 @@ package bridge_test
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"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/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
|
@ -192,12 +198,13 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
|||
|
||||
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, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
if len(messageIDs) < 3 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if strings.Contains(req.URL.Path, "/mail/v4/messages/"+messageIDs[2]) {
|
||||
return http.StatusUnprocessableEntity, true
|
||||
}
|
||||
|
@ -205,11 +212,6 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
|||
return 0, false
|
||||
})
|
||||
|
||||
// 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, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
// Remove messages
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||
|
@ -328,6 +330,24 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_AddressEventUpdatedForAddressThatDoesNotExist_NoBadEvent(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)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
_, err := s.CreateAddressAsUpdate(userID, "another@pm.me", password)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_Network_NoBadEvents(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
retVal := int32(0)
|
||||
|
@ -374,43 +394,392 @@ func TestBridge_User_Network_NoBadEvents(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBridge503DuringEventDoesNotCauseBadEvent(t *testing.T) {
|
||||
func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
dropListener := proton.NewListener(l, proton.NewDropConn)
|
||||
defer func() { _ = dropListener.Close() }()
|
||||
|
||||
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)
|
||||
_, addrID, err := s.CreateUser("user", password)
|
||||
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)
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 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
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
// 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)
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
|
||||
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 xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string {
|
||||
return "/mail/v4/messages/" + messageID
|
||||
}), req.URL.Path) < 0 {
|
||||
return 0, false
|
||||
if strings.Contains(req.URL.Path, "/mail/v4/messages") {
|
||||
if count++; count < 10 {
|
||||
dropListener.DropAll()
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusServiceUnavailable, true
|
||||
return 0, false
|
||||
})
|
||||
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
// The IMAP client will eventually see 20 messages.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||
return err == nil && status.Messages == 20
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}, server.WithListener(dropListener))
|
||||
}
|
||||
|
||||
func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a bridge user.
|
||||
_, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err := c.GetAddresses(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
salts, err := c.GetSalts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a draft (generating a "create draft message" event).
|
||||
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject",
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
// Update the draft (generating an "update draft message" event).
|
||||
require.NoError(t, getErr(c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject 2",
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body 2",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})))
|
||||
|
||||
// Import a message (generating a "create message" event).
|
||||
str, err := c.ImportMessages(ctx, addrKRs[addrs[0].ID], 1, 1, proton.ImportReq{
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: addrs[0].ID,
|
||||
Flags: proton.MessageFlagReceived,
|
||||
},
|
||||
Message: []byte("From: someone@example.com\r\nTo: blabla@example.com\r\n\r\nhello"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := stream.Collect(ctx, str)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
// Update the imported message (generating an "update message" event).
|
||||
require.NoError(t, c.MarkMessagesUnread(ctx, res[0].MessageID))
|
||||
|
||||
// Process those events.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a bridge user.
|
||||
_, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err := c.GetAddresses(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
salts, err := c.GetSalts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a draft (generating a "create draft message" event).
|
||||
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject",
|
||||
ToList: []*mail.Address{{Address: addrs[0].Email}},
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
messages, err := clientFetch(client, "Drafts")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||
})
|
||||
|
||||
// Send the draft (generating an "update message" event).
|
||||
{
|
||||
pubKeys, recType, err := c.GetPublicKeys(ctx, addrs[0].Email)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, recType, proton.RecipientTypeInternal)
|
||||
|
||||
var req proton.SendDraftReq
|
||||
|
||||
require.NoError(t, req.AddTextPackage(addrKRs[addrs[0].ID], "body", rfc822.TextPlain, map[string]proton.SendPreferences{
|
||||
addrs[0].Email: {
|
||||
Encrypt: true,
|
||||
PubKey: must(crypto.NewKeyRing(must(crypto.NewKeyFromArmored(pubKeys[0].PublicKey)))),
|
||||
SignatureType: proton.DetachedSignature,
|
||||
EncryptionScheme: proton.InternalScheme,
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
}, nil))
|
||||
|
||||
require.NoError(t, getErr(c.SendDraft(ctx, draft.ID, req)))
|
||||
}
|
||||
|
||||
// Process those events; the draft will move to the sent folder and lose the draft flag.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
messages, err := clientFetch(client, "Sent")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_DisableEnableAddress(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)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||
|
||||
// Initially we should list the address.
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, info.Addresses, "alias@"+s.GetDomain())
|
||||
})
|
||||
|
||||
// Disable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Eventually we shouldn't list the address.
|
||||
require.Eventually(t, func() bool {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) < 0
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
|
||||
// Enable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.EnableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Eventually we should list the address.
|
||||
require.Eventually(t, func() bool {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) >= 0
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_CreateDisabledAddress(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)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Immediately disable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||
|
||||
// Initially we shouldn't list the address.
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, info.Addresses, "alias@"+s.GetDomain())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
|
||||
info, err := bridge.QueryUserInfo(username)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
|
||||
parentName := uuid.NewString()
|
||||
childName := uuid.NewString()
|
||||
|
||||
// Create a folder.
|
||||
parentLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||
Name: parentName,
|
||||
Type: proton.LabelTypeFolder,
|
||||
Color: "#f66",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the parent folder to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
// Create a subfolder.
|
||||
childLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||
Name: childName,
|
||||
Type: proton.LabelTypeFolder,
|
||||
Color: "#f66",
|
||||
ParentID: parentLabel.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, parentLabel.ID, childLabel.ParentID)
|
||||
|
||||
// Wait for the parent folder to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
newParentName := uuid.NewString()
|
||||
|
||||
// Rename the parent folder.
|
||||
require.NoError(t, getErr(c.UpdateLabel(ctx, parentLabel.ID, proton.UpdateLabelReq{
|
||||
Color: "#f66",
|
||||
Name: newParentName,
|
||||
})))
|
||||
|
||||
// Wait for the parent folder to be renamed.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
// Wait for the child folder to be renamed.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -37,9 +37,14 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||
return fmt.Errorf("failed to handle user address created event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressUpdated:
|
||||
if err := bridge.handleUserAddressUpdated(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address updated event: %w", err)
|
||||
case events.UserAddressEnabled:
|
||||
if err := bridge.handleUserAddressEnabled(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address enabled event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressDisabled:
|
||||
if err := bridge.handleUserAddressDisabled(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address disabled event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressDeleted:
|
||||
|
@ -66,55 +71,84 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GODT-1948: Handle addresses that have been disabled!
|
||||
func (bridge *Bridge) handleUserAddressUpdated(_ context.Context, user *user.User, _ events.UserAddressUpdated) error {
|
||||
switch user.GetAddressMode() {
|
||||
case vault.CombinedMode:
|
||||
return fmt.Errorf("not implemented")
|
||||
func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.User, event events.UserAddressEnabled) error {
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
case vault.SplitMode:
|
||||
return fmt.Errorf("not implemented")
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.User, event events.UserAddressDisabled) error {
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -20,6 +20,8 @@ package bridge_test
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -61,6 +63,50 @@ func TestBridge_Login(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBridge_Login_DropConn(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
dropListener := proton.NewListener(l, proton.NewDropConn)
|
||||
defer func() { _ = dropListener.Close() }()
|
||||
|
||||
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) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
// Whether to allow the user to be created.
|
||||
var allowUser bool
|
||||
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
// Drop any request to the users endpoint.
|
||||
if !allowUser && req.URL.Path == "/core/v4/users" {
|
||||
dropListener.DropAll()
|
||||
}
|
||||
|
||||
// After the ping request, allow the user to be created.
|
||||
if req.URL.Path == "/tests/ping" {
|
||||
allowUser = true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is eventually connected.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 1
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}, server.WithListener(dropListener))
|
||||
}
|
||||
|
||||
func TestBridge_LoginTwice(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) {
|
||||
|
|
|
@ -98,6 +98,8 @@ func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
|
|||
|
||||
// Make sure the temporary file is deleted.
|
||||
go func() {
|
||||
defer recover() //nolint:errcheck
|
||||
|
||||
<-time.After(10 * time.Minute)
|
||||
_ = os.RemoveAll(dir)
|
||||
}()
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -40,17 +41,20 @@ type ProxyTLSDialer struct {
|
|||
allowProxy bool
|
||||
proxyProvider *proxyProvider
|
||||
proxyUseDuration time.Duration
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
// NewProxyTLSDialer constructs a dialer which provides a proxy-managing layer on top of an underlying dialer.
|
||||
func NewProxyTLSDialer(dialer TLSDialer, hostURL string) *ProxyTLSDialer {
|
||||
func NewProxyTLSDialer(dialer TLSDialer, hostURL string, panicHandler async.PanicHandler) *ProxyTLSDialer {
|
||||
return &ProxyTLSDialer{
|
||||
dialer: dialer,
|
||||
locker: sync.RWMutex{},
|
||||
directAddress: formatAsAddress(hostURL),
|
||||
proxyAddress: formatAsAddress(hostURL),
|
||||
proxyProvider: newProxyProvider(dialer, hostURL, DoHProviders),
|
||||
proxyProvider: newProxyProvider(dialer, hostURL, DoHProviders, panicHandler),
|
||||
proxyUseDuration: proxyUseDuration,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,6 +133,8 @@ func (d *ProxyTLSDialer) switchToReachableServer() error {
|
|||
// This means we want to disable it again in 24 hours.
|
||||
if d.proxyAddress == d.directAddress {
|
||||
go func() {
|
||||
defer async.HandlePanic(d.panicHandler)
|
||||
|
||||
<-time.After(d.proxyUseDuration)
|
||||
|
||||
d.locker.Lock()
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -67,11 +68,13 @@ type proxyProvider struct {
|
|||
canReachTimeout time.Duration
|
||||
|
||||
lastLookup time.Time // The time at which we last attempted to find a proxy.
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
// newProxyProvider creates a new proxyProvider that queries the given DoH providers
|
||||
// to retrieve DNS records for the given query string.
|
||||
func newProxyProvider(dialer TLSDialer, hostURL string, providers []string) (p *proxyProvider) {
|
||||
func newProxyProvider(dialer TLSDialer, hostURL string, providers []string, panicHandler async.PanicHandler) (p *proxyProvider) {
|
||||
p = &proxyProvider{
|
||||
dialer: dialer,
|
||||
hostURL: hostURL,
|
||||
|
@ -80,6 +83,7 @@ func newProxyProvider(dialer TLSDialer, hostURL string, providers []string) (p *
|
|||
cacheRefreshTimeout: proxyCacheRefreshTimeout,
|
||||
dohTimeout: proxyDoHTimeout,
|
||||
canReachTimeout: proxyCanReachTimeout,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
|
||||
// Use the default DNS lookup method; this can be overridden if necessary.
|
||||
|
@ -109,11 +113,13 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) {
|
|||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer async.HandlePanic(p.panicHandler)
|
||||
defer wg.Done()
|
||||
apiReachable = p.canReach(p.hostURL)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer async.HandlePanic(p.panicHandler)
|
||||
defer wg.Done()
|
||||
err = p.refreshProxyCache()
|
||||
}()
|
||||
|
@ -150,6 +156,8 @@ func (p *proxyProvider) refreshProxyCache() error {
|
|||
resultChan := make(chan []string)
|
||||
|
||||
go func() {
|
||||
defer async.HandlePanic(p.panicHandler)
|
||||
|
||||
for _, provider := range p.providers {
|
||||
if proxies, err := p.dohLookup(ctx, p.query, provider); err == nil {
|
||||
resultChan <- proxies
|
||||
|
@ -203,6 +211,7 @@ func (p *proxyProvider) defaultDoHLookup(ctx context.Context, query, dohProvider
|
|||
dataChan, errChan := make(chan []string), make(chan error)
|
||||
|
||||
go func() {
|
||||
defer async.HandlePanic(p.panicHandler)
|
||||
// Build new DNS request in RFC1035 format.
|
||||
dnsRequest := new(dns.Msg).SetQuestion(dns.Fqdn(query), dns.TypeTXT)
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
r "github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -31,7 +32,7 @@ func TestProxyProvider_FindProxy(t *testing.T) {
|
|||
proxy := getTrustedServer()
|
||||
defer closeServer(proxy)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
|
@ -47,7 +48,7 @@ func TestProxyProvider_FindProxy_ChooseReachableProxy(t *testing.T) {
|
|||
unreachableProxy := getTrustedServer()
|
||||
closeServer(unreachableProxy)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{reachableProxy.URL, unreachableProxy.URL}, nil
|
||||
}
|
||||
|
@ -68,7 +69,7 @@ func TestProxyProvider_FindProxy_ChooseTrustedProxy(t *testing.T) {
|
|||
checker := NewTLSPinChecker(TrustedAPIPins)
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
|
||||
|
||||
p := newProxyProvider(dialer, "", []string{"not used"})
|
||||
p := newProxyProvider(dialer, "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{untrustedProxy.URL, trustedProxy.URL}, nil
|
||||
}
|
||||
|
@ -85,7 +86,7 @@ func TestProxyProvider_FindProxy_FailIfNoneReachable(t *testing.T) {
|
|||
unreachableProxy2 := getTrustedServer()
|
||||
closeServer(unreachableProxy2)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{unreachableProxy1.URL, unreachableProxy2.URL}, nil
|
||||
}
|
||||
|
@ -105,7 +106,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
|
|||
checker := NewTLSPinChecker(TrustedAPIPins)
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
|
||||
|
||||
p := newProxyProvider(dialer, "", []string{"not used"})
|
||||
p := newProxyProvider(dialer, "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{untrustedProxy1.URL, untrustedProxy2.URL}, nil
|
||||
}
|
||||
|
@ -115,7 +116,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProxyProvider_FindProxy_RefreshCacheTimeout(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.cacheRefreshTimeout = 1 * time.Second
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { time.Sleep(2 * time.Second); return nil, nil }
|
||||
|
||||
|
@ -132,7 +133,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
|||
}))
|
||||
defer closeServer(slowProxy)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.canReachTimeout = 1 * time.Second
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{slowProxy.URL}, nil }
|
||||
|
||||
|
@ -144,7 +145,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9Provider)
|
||||
r.NoError(t, err)
|
||||
|
@ -155,7 +156,7 @@ func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
|
|||
// port filter. Basic functionality should be covered by other tests. Keeping
|
||||
// code here to be able to run it locally if needed.
|
||||
func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9PortProvider)
|
||||
r.NoError(t, err)
|
||||
|
@ -163,7 +164,7 @@ func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, GoogleProvider)
|
||||
r.NoError(t, err)
|
||||
|
@ -173,7 +174,7 @@ func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
|||
func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
|
@ -183,7 +184,7 @@ func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
|||
func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"https://unreachable", Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"https://unreachable", Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -141,8 +142,8 @@ func TestProxyDialer_UseProxy(t *testing.T) {
|
|||
trustedProxy := getTrustedServer()
|
||||
defer closeServer(trustedProxy)
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
|
||||
|
@ -159,8 +160,8 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
|
|||
proxy3 := getTrustedServer()
|
||||
defer closeServer(proxy3)
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL}, nil }
|
||||
|
||||
|
@ -189,8 +190,8 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
|
|||
trustedProxy := getTrustedServer()
|
||||
defer closeServer(trustedProxy)
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
d.proxyUseDuration = time.Second
|
||||
|
||||
|
@ -212,8 +213,8 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
|
|||
func TestProxyDialer_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable(t *testing.T) {
|
||||
trustedProxy := getTrustedServer()
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
|
||||
|
@ -242,8 +243,8 @@ func TestProxyDialer_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBloc
|
|||
proxy2 := getTrustedServer()
|
||||
defer closeServer(proxy2)
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
|
||||
|
||||
|
|
|
@ -35,6 +35,30 @@ func (event UserAddressCreated) String() string {
|
|||
return fmt.Sprintf("UserAddressCreated: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
|
||||
}
|
||||
|
||||
type UserAddressEnabled struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
AddressID string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (event UserAddressEnabled) String() string {
|
||||
return fmt.Sprintf("UserAddressEnabled: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
|
||||
}
|
||||
|
||||
type UserAddressDisabled struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
AddressID string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (event UserAddressDisabled) String() string {
|
||||
return fmt.Sprintf("UserAddressDisabled: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
|
||||
}
|
||||
|
||||
type UserAddressUpdated struct {
|
||||
eventBase
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
// 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 events
|
||||
|
||||
import "fmt"
|
||||
|
||||
type IMAPServerReady struct {
|
||||
eventBase
|
||||
|
||||
Port int
|
||||
}
|
||||
|
||||
func (event IMAPServerReady) String() string {
|
||||
return fmt.Sprintf("IMAPServerReady: Port %d", event.Port)
|
||||
}
|
||||
|
||||
type IMAPServerStopped struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
func (event IMAPServerStopped) String() string {
|
||||
return "IMAPServerStopped"
|
||||
}
|
||||
|
||||
type IMAPServerError struct {
|
||||
eventBase
|
||||
|
||||
Error error
|
||||
}
|
||||
|
||||
func (event IMAPServerError) String() string {
|
||||
return fmt.Sprintf("IMAPServerError: %v", event.Error)
|
||||
}
|
||||
|
||||
type SMTPServerReady struct {
|
||||
eventBase
|
||||
|
||||
Port int
|
||||
}
|
||||
|
||||
func (event SMTPServerReady) String() string {
|
||||
return fmt.Sprintf("SMTPServerReady: Port %d", event.Port)
|
||||
}
|
||||
|
||||
type SMTPServerStopped struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
func (event SMTPServerStopped) String() string {
|
||||
return "SMTPServerStopped"
|
||||
}
|
||||
|
||||
type SMTPServerError struct {
|
||||
eventBase
|
||||
|
||||
Error error
|
||||
}
|
||||
|
||||
func (event SMTPServerError) String() string {
|
||||
return fmt.Sprintf("SMTPServerError: %v", event.Error)
|
||||
}
|
|
@ -169,6 +169,29 @@ func (event AddressModeChanged) String() string {
|
|||
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
|
||||
}
|
||||
|
||||
// UsedSpaceChanged is emitted when the storage space used by the user has changed.
|
||||
type UsedSpaceChanged struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
|
||||
UsedSpace int
|
||||
}
|
||||
|
||||
func (event UsedSpaceChanged) String() string {
|
||||
return fmt.Sprintf("UsedSpaceChanged: UserID: %s, UsedSpace: %v", event.UserID, event.UsedSpace)
|
||||
}
|
||||
|
||||
type IMAPLoginFailed struct {
|
||||
eventBase
|
||||
|
||||
Username string
|
||||
}
|
||||
|
||||
func (event IMAPLoginFailed) String() string {
|
||||
return fmt.Sprintf("IMAPLoginFailed: Username: %s", event.Username)
|
||||
}
|
||||
|
||||
type UncategorizedEventError struct {
|
||||
eventBase
|
||||
|
||||
|
|
|
@ -21,9 +21,11 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus/proto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
@ -32,10 +34,10 @@ import (
|
|||
|
||||
// TryRaise tries to raise the application by dialing the focus service.
|
||||
// It returns true if the service is running and the application was told to raise.
|
||||
func TryRaise() bool {
|
||||
func TryRaise(settingsPath string) bool {
|
||||
var raised bool
|
||||
|
||||
if err := withClientConn(context.Background(), func(ctx context.Context, client proto.FocusClient) error {
|
||||
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
|
||||
if _, err := client.Raise(ctx, &emptypb.Empty{}); err != nil {
|
||||
return fmt.Errorf("failed to call client.Raise: %w", err)
|
||||
}
|
||||
|
@ -53,10 +55,10 @@ func TryRaise() bool {
|
|||
|
||||
// TryVersion tries to determine the version of the running application instance.
|
||||
// It returns the version and true if the version could be determined.
|
||||
func TryVersion() (*semver.Version, bool) {
|
||||
func TryVersion(settingsPath string) (*semver.Version, bool) {
|
||||
var version *semver.Version
|
||||
|
||||
if err := withClientConn(context.Background(), func(ctx context.Context, client proto.FocusClient) error {
|
||||
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
|
||||
raw, err := client.Version(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to call client.Version: %w", err)
|
||||
|
@ -78,10 +80,15 @@ func TryVersion() (*semver.Version, bool) {
|
|||
return version, true
|
||||
}
|
||||
|
||||
func withClientConn(ctx context.Context, fn func(context.Context, proto.FocusClient) error) error {
|
||||
func withClientConn(ctx context.Context, settingsPath string, fn func(context.Context, proto.FocusClient) error) error {
|
||||
var config = service.Config{}
|
||||
err := config.Load(filepath.Join(settingsPath, serverConfigFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cc, err := grpc.DialContext(
|
||||
ctx,
|
||||
net.JoinHostPort(Host, fmt.Sprint(Port)),
|
||||
net.JoinHostPort(Host, fmt.Sprint(config.Port)),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
@ -18,19 +18,25 @@
|
|||
package focus
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFocus_Raise(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
locations := locations.New(newTestLocationsProvider(tmpDir), "config-name")
|
||||
// Start the focus service.
|
||||
service, err := NewService(semver.MustParse("1.2.3"))
|
||||
service, err := NewService(locations, semver.MustParse("1.2.3"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
settingsFolder, err := locations.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
// Try to dial it, it should succeed.
|
||||
require.True(t, TryRaise())
|
||||
require.True(t, TryRaise(settingsFolder))
|
||||
|
||||
// The service should report a raise call.
|
||||
<-service.GetRaiseCh()
|
||||
|
@ -39,16 +45,60 @@ func TestFocus_Raise(t *testing.T) {
|
|||
service.Close()
|
||||
|
||||
// Try to dial it, it should fail.
|
||||
require.False(t, TryRaise())
|
||||
require.False(t, TryRaise(settingsFolder))
|
||||
}
|
||||
|
||||
func TestFocus_Version(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
locations := locations.New(newTestLocationsProvider(tmpDir), "config-name")
|
||||
// Start the focus service.
|
||||
_, err := NewService(semver.MustParse("1.2.3"))
|
||||
_, err := NewService(locations, semver.MustParse("1.2.3"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
settingsFolder, err := locations.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to dial it, it should succeed.
|
||||
version, ok := TryVersion()
|
||||
version, ok := TryVersion(settingsFolder)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "1.2.3", version.String())
|
||||
}
|
||||
|
||||
type TestLocationsProvider struct {
|
||||
config, data, cache string
|
||||
}
|
||||
|
||||
func newTestLocationsProvider(dir string) *TestLocationsProvider {
|
||||
config, err := os.MkdirTemp(dir, "config")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data, err := os.MkdirTemp(dir, "data")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cache, err := os.MkdirTemp(dir, "cache")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &TestLocationsProvider{
|
||||
config: config,
|
||||
data: data,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserConfig() string {
|
||||
return provider.config
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserData() string {
|
||||
return provider.data
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserCache() string {
|
||||
return provider.cache
|
||||
}
|
||||
|
|
|
@ -24,17 +24,18 @@ import (
|
|||
"net"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus/proto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// Host is the local host to listen on.
|
||||
const Host = "127.0.0.1"
|
||||
|
||||
// Port is the port to listen on.
|
||||
var Port = 1042 // nolint:gochecknoglobals
|
||||
const (
|
||||
Host = "127.0.0.1"
|
||||
serverConfigFileName = "grpcFocusServerConfig.json"
|
||||
)
|
||||
|
||||
// Service is a gRPC service that can be used to raise the application.
|
||||
type Service struct {
|
||||
|
@ -43,30 +44,48 @@ type Service struct {
|
|||
server *grpc.Server
|
||||
raiseCh chan struct{}
|
||||
version *semver.Version
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
// NewService creates a new focus service.
|
||||
// It listens on the local host and port 1042 (by default).
|
||||
func NewService(version *semver.Version) (*Service, error) {
|
||||
service := &Service{
|
||||
server: grpc.NewServer(),
|
||||
raiseCh: make(chan struct{}, 1),
|
||||
version: version,
|
||||
func NewService(locator service.Locator, version *semver.Version, panicHandler async.PanicHandler) (*Service, error) {
|
||||
serv := &Service{
|
||||
server: grpc.NewServer(),
|
||||
raiseCh: make(chan struct{}, 1),
|
||||
version: version,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
|
||||
proto.RegisterFocusServer(service.server, service)
|
||||
proto.RegisterFocusServer(serv.server, serv)
|
||||
|
||||
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(Port))); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to start focus service")
|
||||
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(0))); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to start focus serv")
|
||||
} else {
|
||||
config := service.Config{}
|
||||
// retrieve the port assigned by the system, so that we can put it in the config file.
|
||||
address, ok := listener.Addr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not retrieve gRPC service listener address")
|
||||
}
|
||||
config.Port = address.Port
|
||||
if path, err := service.SaveGRPCServerConfigFile(locator, &config, serverConfigFileName); err != nil {
|
||||
logrus.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
|
||||
} else {
|
||||
logrus.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := service.server.Serve(listener); err != nil {
|
||||
defer async.HandlePanic(serv.panicHandler)
|
||||
|
||||
if err := serv.server.Serve(listener); err != nil {
|
||||
fmt.Printf("failed to serve: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return service, nil
|
||||
return serv, nil
|
||||
}
|
||||
|
||||
// Raise implements the gRPC FocusService interface; it raises the application.
|
||||
|
@ -90,6 +109,8 @@ func (service *Service) GetRaiseCh() <-chan struct{} {
|
|||
// Close closes the service.
|
||||
func (service *Service) Close() {
|
||||
go func() {
|
||||
defer async.HandlePanic(service.panicHandler)
|
||||
|
||||
// we do this in a goroutine, as on Windows, the gRPC shutdown may take minutes if something tries to
|
||||
// interact with it in an invalid way (e.g. HTTP GET request from a Qt QNetworkManager instance).
|
||||
service.server.Stop()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
find_program(QMAKE_EXE "qmake")
|
||||
find_program(QMAKE_EXE NAMES "qmake" "qmake6")
|
||||
if (NOT QMAKE_EXE)
|
||||
message(FATAL_ERROR "Could not locate qmake executable, make sur you have Qt 6 installed in that qmake is in your PATH environment variable.")
|
||||
message(FATAL_ERROR "Could not locate qmake executable, make sure you have Qt 6 installed in that qmake is in your PATH environment variable.")
|
||||
endif()
|
||||
message(STATUS "Found qmake at ${QMAKE_EXE}")
|
||||
execute_process(COMMAND "${QMAKE_EXE}" -query QT_INSTALL_PREFIX OUTPUT_VARIABLE QT_DIR OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
set(CMAKE_PREFIX_PATH ${QT_DIR} ${CMAKE_PREFIX_PATH})
|
||||
set(CMAKE_PREFIX_PATH ${QT_DIR} ${CMAKE_PREFIX_PATH})
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "Cert.h"
|
||||
#include "GRPCService.h"
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCConfig.h>
|
||||
|
||||
|
@ -33,7 +34,6 @@ using namespace grpc;
|
|||
//****************************************************************************************************************************************************
|
||||
GRPCServerWorker::GRPCServerWorker(QObject *parent)
|
||||
: Worker(parent) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -81,7 +81,7 @@ void GRPCServerWorker::run() {
|
|||
|
||||
config.port = port;
|
||||
QString err;
|
||||
if (!config.save(grpcServerConfigPath(), &err)) {
|
||||
if (!config.save(grpcServerConfigPath(bridgepp::userConfigDir()), &err)) {
|
||||
throw Exception(QString("Could not save gRPC server config. %1").arg(err));
|
||||
}
|
||||
|
||||
|
|
|
@ -192,17 +192,6 @@ Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, Bool
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[out] response The response.
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::GoOs(ServerContext *, Empty const *, StringValue *response) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
response->set_value(app().mainWindow().settingsTab().os().toStdString());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
|
|
|
@ -51,7 +51,6 @@ public: // member functions.
|
|||
grpc::Status IsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status SetIsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
||||
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
|
|
|
@ -52,7 +52,11 @@ UsersTab::UsersTab(QWidget *parent)
|
|||
connect(ui_.tableUserList, &QTableView::doubleClicked, this, &UsersTab::onEditUserButton);
|
||||
connect(ui_.buttonRemoveUser, &QPushButton::clicked, this, &UsersTab::onRemoveUserButton);
|
||||
connect(ui_.buttonUserBadEvent, &QPushButton::clicked, this, &UsersTab::onSendUserBadEvent);
|
||||
connect(ui_.buttonImapLoginFailed, &QPushButton::clicked, this, &UsersTab::onSendIMAPLoginFailedEvent);
|
||||
connect(ui_.buttonUsedBytesChanged, &QPushButton::clicked, this, &UsersTab::onSendUsedBytesChangedEvent);
|
||||
connect(ui_.checkUsernamePasswordError, &QCheckBox::toggled, this, &UsersTab::updateGUIState);
|
||||
connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled);
|
||||
connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged);
|
||||
|
||||
users_.append(randomUser());
|
||||
|
||||
|
@ -155,16 +159,76 @@ void UsersTab::onSendUserBadEvent() {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::onSendUsedBytesChangedEvent() {
|
||||
SPUser const user = selectedUser();
|
||||
int const index = this->selectedIndex();
|
||||
|
||||
if (!user) {
|
||||
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
|
||||
return;
|
||||
}
|
||||
|
||||
if (UserState::Connected != user->state()) {
|
||||
app().log().error(QString("%1 failed. User is not connected").arg(__FUNCTION__));
|
||||
}
|
||||
|
||||
qint64 const usedBytes = qint64(ui_.spinUsedBytes->value());
|
||||
user->setUsedBytes(usedBytes);
|
||||
users_.touch(index);
|
||||
|
||||
GRPCService &grpc = app().grpc();
|
||||
if (grpc.isStreaming()) {
|
||||
QString const userID = user->id();
|
||||
grpc.sendEvent(newUsedBytesChangedEvent(userID, usedBytes));
|
||||
}
|
||||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::onSendIMAPLoginFailedEvent() {
|
||||
GRPCService &grpc = app().grpc();
|
||||
if (grpc.isStreaming()) {
|
||||
grpc.sendEvent(newIMAPLoginFailedEvent(ui_.editIMAPLoginFailedUsername->text()));
|
||||
}
|
||||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::updateGUIState() {
|
||||
SPUser const user = selectedUser();
|
||||
bool const hasSelectedUser = user.get();
|
||||
UserState const state = user ? user->state() : UserState::SignedOut;
|
||||
|
||||
ui_.buttonEditUser->setEnabled(hasSelectedUser);
|
||||
ui_.buttonRemoveUser->setEnabled(hasSelectedUser);
|
||||
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != user->state()));
|
||||
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != state));
|
||||
ui_.groupBoxUsedSpace->setEnabled(hasSelectedUser && (UserState::Connected == state));
|
||||
ui_.editUsernamePasswordError->setEnabled(ui_.checkUsernamePasswordError->isChecked());
|
||||
ui_.spinUsedBytes->setValue(user ? user->usedBytes() : 0.0);
|
||||
ui_.groupboxSync->setEnabled(user.get());
|
||||
|
||||
if (user)
|
||||
ui_.editIMAPLoginFailedUsername->setText(user->primaryEmailOrUsername());
|
||||
|
||||
QSignalBlocker b(ui_.checkSync);
|
||||
bool const syncing = user && user->isSyncing();
|
||||
ui_.checkSync->setChecked(syncing);
|
||||
b = QSignalBlocker(ui_.sliderSync);
|
||||
ui_.sliderSync->setEnabled(syncing);
|
||||
qint32 const progressPercent = syncing ? qint32(user->syncProgress() * 100.0f) : 0;
|
||||
ui_.sliderSync->setValue(progressPercent);
|
||||
ui_.labelSync->setText(syncing ? QString("%1%").arg(progressPercent) : "" );
|
||||
}
|
||||
|
||||
|
||||
|
@ -368,3 +432,44 @@ void UsersTab::processBadEventUserFeedback(QString const &userID, bool doResync)
|
|||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] checked Is the sync checkbox checked?
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::onCheckSyncToggled(bool checked) {
|
||||
SPUser const user = this->selectedUser();
|
||||
if ((!user) || (user->isSyncing() == checked)) {
|
||||
return;
|
||||
}
|
||||
|
||||
user->setIsSyncing(checked);
|
||||
user->setSyncProgress(0.0);
|
||||
GRPCService &grpc = app().grpc();
|
||||
|
||||
// we do not apply delay for these event.
|
||||
if (checked) {
|
||||
grpc.sendEvent(newSyncStartedEvent(user->id()));
|
||||
grpc.sendEvent(newSyncProgressEvent(user->id(), 0.0, 1, 1));
|
||||
} else {
|
||||
grpc.sendEvent(newSyncFinishedEvent(user->id()));
|
||||
}
|
||||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] value The value for the slider.
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::onSliderSyncValueChanged(int value) {
|
||||
SPUser const user = this->selectedUser();
|
||||
if ((!user) || (!user->isSyncing()) || user->syncProgress() == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
double const progress = value / 100.0;
|
||||
user->setSyncProgress(progress);
|
||||
app().grpc().sendEvent(newSyncProgressEvent(user->id(), progress, 1, 1)); // we do not simulate elapsed & remaining.
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
|
|
@ -62,6 +62,10 @@ private slots:
|
|||
void onRemoveUserButton(); ///< Remove the currently selected user.
|
||||
void onSelectionChanged(QItemSelection, QItemSelection); ///< Slot for the change of the selection.
|
||||
void onSendUserBadEvent(); ///< Slot for the 'Send Bad Event Error' button.
|
||||
void onSendUsedBytesChangedEvent(); ///< Slot for the 'Send Used Bytes Changed Event' button.
|
||||
void onSendIMAPLoginFailedEvent(); ///< Slot for the 'Send IMAP Login failure Event' button.
|
||||
void onCheckSyncToggled(bool checked); ///< Slot for the 'Synchronizing' check box.
|
||||
void onSliderSyncValueChanged(int value); ///< Slot for the sync 'Progress' slider.
|
||||
void updateGUIState(); ///< Update the GUI state.
|
||||
|
||||
private: // member functions.
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1225</width>
|
||||
<height>717</height>
|
||||
<width>1221</width>
|
||||
<height>894</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -66,6 +66,52 @@
|
|||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupboxSync">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Sync</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkSync">
|
||||
<property name="text">
|
||||
<string>Synchronizing</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelSync">
|
||||
<property name="text">
|
||||
<string>0%</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="sliderSync">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickInterval">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxBadEvent">
|
||||
<property name="minimumSize">
|
||||
|
@ -80,13 +126,6 @@
|
|||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelUserBadEvent">
|
||||
<property name="text">
|
||||
<string>Message: </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editUserBadEvent">
|
||||
<property name="minimumSize">
|
||||
|
@ -96,18 +135,102 @@
|
|||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Bad event error.</string>
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>error message</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUserBadEvent">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxUsedSpace">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Used Bytes Changed</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUserBadEvent">
|
||||
<property name="text">
|
||||
<string>Send Bad Event Error</string>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QHBoxLayout" name="hBoxUsedBytes" stretch="1,0">
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinUsedBytes">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::NoButtons</enum>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000000000000000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUsedBytesChanged">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxIMAPLoginFailed">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>IMAP Login Failure</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editIMAPLoginFailedUsername">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>username or primary email</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonImapLoginFailed">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
|
|
@ -85,7 +85,10 @@ int main(int argc, char **argv) {
|
|||
return exitCode;
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(e.qwhat());
|
||||
QString message = e.qwhat();
|
||||
if (!e.details().isEmpty())
|
||||
message += "\n\nDetails:\n" + e.details();
|
||||
QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(message);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "AppController.h"
|
||||
#include "QMLBackend.h"
|
||||
#include "SentryUtils.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/ProcessMonitor.h>
|
||||
|
@ -48,10 +49,19 @@ AppController &app() {
|
|||
AppController::AppController()
|
||||
: backend_(std::make_unique<QMLBackend>())
|
||||
, grpc_(std::make_unique<GRPCClient>())
|
||||
, log_(std::make_unique<Log>()) {
|
||||
, log_(std::make_unique<Log>())
|
||||
, settings_(new Settings) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
// The following is in the implementation file because of unique pointers with incomplete types in headers.
|
||||
// See https://stackoverflow.com/questions/6012157/is-stdunique-ptrt-required-to-know-the-full-definition-of-t
|
||||
//****************************************************************************************************************************************************
|
||||
AppController::~AppController() = default;
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The bridge worker, which can be null if the application was run in 'attach' mode (-a command-line switch).
|
||||
//****************************************************************************************************************************************************
|
||||
|
@ -71,6 +81,14 @@ ProcessMonitor *AppController::bridgeMonitor() const {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return A reference to the application settings.
|
||||
//****************************************************************************************************************************************************
|
||||
Settings &AppController::settings() {
|
||||
return *settings_;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] exception The exception that triggered the fatal error.
|
||||
//****************************************************************************************************************************************************
|
||||
|
@ -102,4 +120,4 @@ void AppController::restart(bool isCrashing) {
|
|||
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
|
||||
launcher_ = launcher;
|
||||
launcherArgs_ = args;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,9 @@
|
|||
#define BRIDGE_GUI_APP_CONTROLLER_H
|
||||
|
||||
|
||||
// @formatter:off
|
||||
//@formatter:off
|
||||
class QMLBackend;
|
||||
class Settings;
|
||||
namespace bridgepp {
|
||||
class Log;
|
||||
class Overseer;
|
||||
|
@ -29,7 +30,7 @@ class GRPCClient;
|
|||
class ProcessMonitor;
|
||||
class Exception;
|
||||
}
|
||||
// @formatter:off
|
||||
//@formatter:on
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
|
@ -42,7 +43,7 @@ Q_OBJECT
|
|||
public: // member functions.
|
||||
AppController(AppController const &) = delete; ///< Disabled copy-constructor.
|
||||
AppController(AppController &&) = delete; ///< Disabled assignment copy-constructor.
|
||||
~AppController() override = default; ///< Destructor.
|
||||
~AppController() override; ///< Destructor.
|
||||
AppController &operator=(AppController const &) = delete; ///< Disabled assignment operator.
|
||||
AppController &operator=(AppController &&) = delete; ///< Disabled move assignment operator.
|
||||
QMLBackend &backend() { return *backend_; } ///< Return a reference to the backend.
|
||||
|
@ -50,7 +51,8 @@ public: // member functions.
|
|||
bridgepp::Log &log() { return *log_; } ///< Return a reference to the log.
|
||||
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
||||
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
||||
void setLauncherArgs(const QString& launcher, const QStringList& args);
|
||||
Settings &settings();; ///< Return the application settings.
|
||||
void setLauncherArgs(const QString &launcher, const QStringList &args);
|
||||
|
||||
public slots:
|
||||
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
|
||||
|
@ -64,6 +66,7 @@ private: // data members
|
|||
std::unique_ptr<bridgepp::GRPCClient> grpc_; ///< The RPC client.
|
||||
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
||||
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
|
||||
std::unique_ptr<Settings> settings_; ///< The application settings.
|
||||
QString launcher_;
|
||||
QStringList launcherArgs_;
|
||||
};
|
||||
|
|
|
@ -19,12 +19,13 @@
|
|||
#ifndef BRIDGE_GUI_VERSION_H
|
||||
#define BRIDGE_GUI_VERSION_H
|
||||
|
||||
#define PROJECT_FULL_NAME "@BRIDGE_APP_FULL_NAME@"
|
||||
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
|
||||
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
|
||||
#define PROJECT_REVISION "@BRIDGE_REVISION@"
|
||||
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
|
||||
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
|
||||
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"
|
||||
#define PROJECT_FULL_NAME "@BRIDGE_APP_FULL_NAME@"
|
||||
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
|
||||
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
|
||||
#define PROJECT_REVISION "@BRIDGE_REVISION@"
|
||||
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
|
||||
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
|
||||
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"
|
||||
#define PROJECT_CRASHPAD_HANDLER_PATH "@BRIDGE_CRASHPAD_HANDLER_PATH@"
|
||||
|
||||
#endif // BRIDGE_GUI_VERSION_H
|
||||
|
|
|
@ -85,7 +85,6 @@ message(STATUS "Using Qt ${Qt6_VERSION}")
|
|||
#*****************************************************************************************************************************************************
|
||||
find_package(sentry CONFIG REQUIRED)
|
||||
|
||||
|
||||
#*****************************************************************************************************************************************************
|
||||
# Source files and output
|
||||
#*****************************************************************************************************************************************************
|
||||
|
@ -110,24 +109,23 @@ add_executable(bridge-gui
|
|||
Resources.qrc
|
||||
AppController.cpp AppController.h
|
||||
BridgeApp.cpp BridgeApp.h
|
||||
BuildConfig.h
|
||||
CommandLine.cpp CommandLine.h
|
||||
EventStreamWorker.cpp EventStreamWorker.h
|
||||
LogUtils.cpp LogUtils.h
|
||||
main.cpp
|
||||
Pch.h
|
||||
BuildConfig.h
|
||||
QMLBackend.cpp QMLBackend.h
|
||||
UserList.cpp UserList.h
|
||||
SentryUtils.cpp SentryUtils.h
|
||||
Settings.cpp Settings.h
|
||||
${DOCK_ICON_SRC_FILE} MacOS/DockIcon.h
|
||||
)
|
||||
|
||||
|
||||
if (APPLE)
|
||||
target_sources(bridge-gui PRIVATE MacOS/SecondInstance.mm MacOS/SecondInstance.h)
|
||||
endif(APPLE)
|
||||
|
||||
|
||||
if (WIN32) # on Windows, we add a (non-Qt) resource file that contains the application icon and version information.
|
||||
string(TIMESTAMP BRIDGE_BUILD_YEAR "%Y")
|
||||
set(REGEX_NUMBER "[0123456789]") # CMake matches does not support \d.
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
#include "Pch.h"
|
||||
#include "CommandLine.h"
|
||||
#include "Settings.h"
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
@ -28,7 +29,10 @@ namespace {
|
|||
|
||||
QString const launcherFlag = "--launcher"; ///< launcher flag parameter used for bridge.
|
||||
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
|
||||
QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag.
|
||||
QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution
|
||||
QString const setSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application.
|
||||
QString const setHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application.
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief parse a command-line string argument as expected by go's CLI package.
|
||||
|
@ -101,6 +105,14 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
|||
options.bridgeGuiArgs.append(arg);
|
||||
options.useSoftwareRenderer = true;
|
||||
}
|
||||
if (arg == setSoftwareRendererFlag) {
|
||||
app().settings().setUseSoftwareRenderer(true);
|
||||
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
|
||||
}
|
||||
if (arg == setHardwareRendererFlag) {
|
||||
app().settings().setUseSoftwareRenderer(false);
|
||||
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
|
||||
}
|
||||
if (arg == noWindowFlag) {
|
||||
options.noWindow = true;
|
||||
}
|
||||
|
|
|
@ -17,46 +17,42 @@
|
|||
|
||||
|
||||
#include "LogUtils.h"
|
||||
#include "BuildConfig.h"
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
namespace {
|
||||
qsizetype const logFileTailMaxLength = 25 * 1024; ///< The maximum length of the portion of log returned by tailOfLatestBridgeLog()
|
||||
}
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return A reference to the log.
|
||||
//****************************************************************************************************************************************************
|
||||
Log &initLog() {
|
||||
Log &log = app().log();
|
||||
log.registerAsQtMessageHandler();
|
||||
log.setEchoInConsole(true);
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Return the path of the latest bridge log.
|
||||
/// \return The path of the latest bridge log file.
|
||||
/// \return An empty string if no bridge log file was found.
|
||||
//****************************************************************************************************************************************************
|
||||
QString latestBridgeLogPath() {
|
||||
// remove old gui log files
|
||||
QDir const logsDir(userLogsDir());
|
||||
if (logsDir.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
|
||||
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
|
||||
return lhs.birthTime() < rhs.birthTime();
|
||||
});
|
||||
return files.back().absoluteFilePath();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// Return the maxSize last bytes of the latest bridge log.
|
||||
//****************************************************************************************************************************************************
|
||||
QByteArray tailOfLatestBridgeLog() {
|
||||
QString path = latestBridgeLogPath();
|
||||
if (path.isEmpty()) {
|
||||
return QByteArray();
|
||||
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
|
||||
QFile(fileInfo.absoluteFilePath()).remove();
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::Text | QIODevice::ReadOnly) ? file.readAll().right(logFileTailMaxLength) : QByteArray();
|
||||
// create new GUI log file
|
||||
QString error;
|
||||
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
log.info("bridge-gui starting");
|
||||
QString const qtCompileTimeVersion = QT_VERSION_STR;
|
||||
QString const qtRuntimeVersion = qVersion();
|
||||
QString msg = QString("Using Qt %1").arg(qtRuntimeVersion);
|
||||
if (qtRuntimeVersion != qtCompileTimeVersion) {
|
||||
msg += QString(" (compiled against %1)").arg(qtCompileTimeVersion);
|
||||
}
|
||||
log.info(msg);
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -20,7 +20,10 @@
|
|||
#define BRIDGE_GUI_LOG_UTILS_H
|
||||
|
||||
|
||||
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
|
||||
#include <bridgepp/Log/Log.h>
|
||||
|
||||
|
||||
bridgepp::Log &initLog(); ///< Initialize the application log.
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_LOG_UTILS_H
|
||||
|
|
|
@ -17,11 +17,12 @@
|
|||
|
||||
|
||||
#include "QMLBackend.h"
|
||||
#include "EventStreamWorker.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "LogUtils.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include "EventStreamWorker.h"
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
@ -58,7 +59,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
|||
app().grpc().setLog(&log);
|
||||
this->connectGrpcEvents();
|
||||
|
||||
app().grpc().connectToServer(serviceConfig, app().bridgeMonitor());
|
||||
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
|
||||
QString bridgeVer;
|
||||
|
@ -178,16 +179,6 @@ void QMLBackend::setShowSplashScreen(bool show) {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'showSplashScreen' property.
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::showSplashScreen() const {
|
||||
HANDLE_EXCEPTION_RETURN_BOOL(
|
||||
return showSplashScreen_;
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'GOOS' property.
|
||||
//****************************************************************************************************************************************************
|
||||
|
@ -198,6 +189,16 @@ QString QMLBackend::goos() const {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'showSplashScreen' property.
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::showSplashScreen() const {
|
||||
HANDLE_EXCEPTION_RETURN_BOOL(
|
||||
return showSplashScreen_;
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'logsPath' property.
|
||||
//****************************************************************************************************************************************************
|
||||
|
@ -465,6 +466,7 @@ bool QMLBackend::isDoHEnabled() const {
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'isAutomaticUpdateOn' property.
|
||||
//****************************************************************************************************************************************************
|
||||
|
@ -910,6 +912,24 @@ void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] username The username (or primary email address)
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::onIMAPLoginFailed(QString const &username) {
|
||||
HANDLE_EXCEPTION(
|
||||
SPUser const user = users_->getUserWithUsernameOrEmail(username);
|
||||
if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected
|
||||
return;
|
||||
}
|
||||
if (user->isInIMAPLoginFailureCooldown())
|
||||
return;
|
||||
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);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
|
@ -1018,6 +1038,8 @@ void QMLBackend::connectGrpcEvents() {
|
|||
// user events
|
||||
connect(client, &GRPCClient::userDisconnected, this, &QMLBackend::userDisconnected);
|
||||
connect(client, &GRPCClient::userBadEvent, this, &QMLBackend::onUserBadEvent);
|
||||
connect(client, &GRPCClient::imapLoginFailed, this, &QMLBackend::onIMAPLoginFailed);
|
||||
|
||||
users_->connectGRPCEvents();
|
||||
}
|
||||
|
||||
|
|
|
@ -181,6 +181,7 @@ public slots: // slot for signals received from gRPC that need transformation in
|
|||
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
|
||||
void onLoginAlreadyLoggedIn(QString const &userID); ///< Slot for the LoginAlreadyLoggedIn gRPC event.
|
||||
void onUserBadEvent(QString const& userID, QString const& errorMessage); ///< Slot for the userBadEvent gRPC event.
|
||||
void onIMAPLoginFailed(QString const& username); ///< Slot the the imapLoginFailed event.
|
||||
|
||||
signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
|
||||
|
@ -234,6 +235,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
|
||||
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
||||
|
||||
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
|
||||
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.
|
||||
|
|
|
@ -20,10 +20,8 @@
|
|||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCryptographicHash>
|
||||
#include <QString>
|
||||
#include <QSysInfo>
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
static constexpr const char *LoggerName = "bridge-gui";
|
||||
|
@ -46,25 +44,54 @@ QString sentryAttachmentFilePath() {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Get a hash of the computer's host name
|
||||
//****************************************************************************************************************************************************
|
||||
QByteArray getProtectedHostname() {
|
||||
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
|
||||
return hostname.toHex();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The OS String used by sentry
|
||||
//****************************************************************************************************************************************************
|
||||
QString getApiOS() {
|
||||
#if defined(Q_OS_DARWIN)
|
||||
return "macos";
|
||||
#elif defined(Q_OS_WINDOWS)
|
||||
return "windows";
|
||||
#else
|
||||
return "linux";
|
||||
#endif
|
||||
switch (os()) {
|
||||
case OS::MacOS:
|
||||
return "macos";
|
||||
case OS::Windows:
|
||||
return "windows";
|
||||
case OS::Linux:
|
||||
default:
|
||||
return "linux";
|
||||
}
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The application version number.
|
||||
//****************************************************************************************************************************************************
|
||||
QString appVersion(const QString& version) {
|
||||
return QString("%1-bridge@%2").arg(getApiOS()).arg(version);
|
||||
return QString("%1-bridge@%2").arg(getApiOS(), version);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void initSentry() {
|
||||
sentry_options_t *sentryOptions = newSentryOptions(PROJECT_DSN_SENTRY, sentryCacheDir().toStdString().c_str());
|
||||
if (!QString(PROJECT_CRASHPAD_HANDLER_PATH).isEmpty())
|
||||
sentry_options_set_handler_path(sentryOptions, PROJECT_CRASHPAD_HANDLER_PATH);
|
||||
|
||||
if (sentry_init(sentryOptions) != 0) {
|
||||
QTextStream(stderr) << "Failed to initialize sentry\n";
|
||||
}
|
||||
setSentryReportScope();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void setSentryReportScope() {
|
||||
sentry_set_tag("OS", bridgepp::goos().toUtf8());
|
||||
sentry_set_tag("Client", PROJECT_FULL_NAME);
|
||||
|
@ -76,6 +103,10 @@ void setSentryReportScope() {
|
|||
sentry_set_user(user);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir) {
|
||||
sentry_options_t *sentryOptions = sentry_options_new();
|
||||
sentry_options_set_dsn(sentryOptions, sentryDNS);
|
||||
|
@ -92,12 +123,18 @@ sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir)
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message) {
|
||||
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
||||
return sentry_capture_event(event);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
sentry_uuid_t reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception) {
|
||||
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
||||
sentry_event_add_exception(event, sentry_value_new_exception(exceptionType, exception));
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
#include <sentry.h>
|
||||
|
||||
|
||||
void initSentry();
|
||||
void setSentryReportScope();
|
||||
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
|
||||
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// 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/>.
|
||||
|
||||
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
QString const settingsFileName = "bridge-gui.ini"; ///< The name of the settings file.
|
||||
QString const keyUseSoftwareRenderer = "UseSoftwareRenderer"; ///< The key for storing the 'Use software rendering' setting.
|
||||
|
||||
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
Settings::Settings()
|
||||
: settings_(QDir(userConfigDir()).absoluteFilePath("bridge-gui.ini"), QSettings::Format::IniFormat) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'Use software renderer' setting.
|
||||
//****************************************************************************************************************************************************
|
||||
bool Settings::useSoftwareRenderer() const {
|
||||
return settings_.value(keyUseSoftwareRenderer, onWindows()).toBool();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] value The value for the 'Use software renderer' setting.
|
||||
//****************************************************************************************************************************************************
|
||||
void Settings::setUseSoftwareRenderer(bool value) {
|
||||
settings_.setValue(keyUseSoftwareRenderer, value);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// 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/>.
|
||||
|
||||
|
||||
#ifndef BRIDGE_GUI_SETTINGS_H
|
||||
#define BRIDGE_GUI_SETTINGS_H
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Application settings class
|
||||
//****************************************************************************************************************************************************
|
||||
class Settings {
|
||||
public: // member functions.
|
||||
Settings(Settings const&) = delete; ///< Disabled copy-constructor.
|
||||
Settings(Settings&&) = delete; ///< Disabled assignment copy-constructor.
|
||||
~Settings() = default; ///< Destructor.
|
||||
Settings& operator=(Settings const&) = delete; ///< Disabled assignment operator.
|
||||
Settings& operator=(Settings&&) = delete; ///< Disabled move assignment operator.
|
||||
|
||||
bool useSoftwareRenderer() const; ///< Get the 'Use software renderer' settings value.
|
||||
void setUseSoftwareRenderer(bool value); ///< Set the 'Use software renderer' settings value.
|
||||
|
||||
private: // member functions.
|
||||
Settings(); ///< Default constructor.
|
||||
|
||||
private: // data members.
|
||||
QSettings settings_; ///< The settings.
|
||||
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_SETTINGS_H
|
|
@ -38,6 +38,10 @@ void UserList::connectGRPCEvents() const {
|
|||
GRPCClient &client = app().grpc();
|
||||
connect(&client, &GRPCClient::userChanged, this, &UserList::onUserChanged);
|
||||
connect(&client, &GRPCClient::toggleSplitModeFinished, this, &UserList::onToggleSplitModeFinished);
|
||||
connect(&client, &GRPCClient::usedBytesChanged, this, &UserList::onUsedBytesChanged);
|
||||
connect(&client, &GRPCClient::syncStarted, this, &UserList::onSyncStarted);
|
||||
connect(&client, &GRPCClient::syncFinished, this, &UserList::onSyncFinished);
|
||||
connect(&client, &GRPCClient::syncProgress, this, &UserList::onSyncProgress);
|
||||
}
|
||||
|
||||
|
||||
|
@ -148,6 +152,19 @@ bridgepp::SPUser UserList::getUserWithID(QString const &userID) const {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] username The username or email.
|
||||
/// \return The user with the given ID.
|
||||
/// \return A null pointer if the user could not be found.
|
||||
//****************************************************************************************************************************************************
|
||||
bridgepp::SPUser UserList::getUserWithUsernameOrEmail(QString const &username) const {
|
||||
QList<SPUser>::const_iterator it = std::find_if(users_.begin(), users_.end(), [username](SPUser const &user) -> bool {
|
||||
return user && ((username.compare(user->username(), Qt::CaseInsensitive) == 0) || user->addresses().contains(username, Qt::CaseInsensitive));
|
||||
});
|
||||
return (it == users_.end()) ? nullptr : *it;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] row The row.
|
||||
//****************************************************************************************************************************************************
|
||||
|
@ -223,3 +240,61 @@ void UserList::onToggleSplitModeFinished(QString const &userID) {
|
|||
int UserList::count() const {
|
||||
return users_.size();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] usedBytes The used space, in bytes.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserList::onUsedBytesChanged(QString const &userID, qint64 usedBytes) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received usedBytesChanged event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setUsedBytes(usedBytes);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserList::onSyncStarted(QString const &userID) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncStarted event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setIsSyncing(true);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserList::onSyncFinished(QString const &userID) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setIsSyncing(false);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] progress The sync progress ratio.
|
||||
/// \param[in] elapsedMs The elapsed sync time in milliseconds.
|
||||
/// \param[in] remainingMs The remaining sync time in milliseconds.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserList::onSyncProgress(QString const &userID, double progress, float elapsedMs, float remainingMs) {
|
||||
Q_UNUSED(elapsedMs)
|
||||
Q_UNUSED(remainingMs)
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setSyncProgress(progress);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ public: // member functions.
|
|||
void appendUser(bridgepp::SPUser const &user); ///< Add a new user.
|
||||
void updateUserAtRow(int row, bridgepp::User const &user); ///< Update the user at given row.
|
||||
bridgepp::SPUser getUserWithID(QString const &userID) const; ///< Retrieve the user with the given ID.
|
||||
bridgepp::SPUser getUserWithUsernameOrEmail(QString const& username) const; ///< Retrieve the user with the given primary email address or username
|
||||
|
||||
// the userCount property.
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
|
@ -59,7 +60,11 @@ public:
|
|||
public slots: ///< handler for signals coming from the gRPC service
|
||||
void onUserChanged(QString const &userID);
|
||||
void onToggleSplitModeFinished(QString const &userID);
|
||||
|
||||
void onUsedBytesChanged(QString const &userID, qint64 usedBytes); ///< Slot for usedBytesChanged events.
|
||||
void onSyncStarted(QString const &userID); ///< Slot for syncStarted events.
|
||||
void onSyncFinished(QString const &userID); ///< Slot for syncFinished events.
|
||||
void onSyncProgress(QString const &userID, double progress, float elapsedMs, float remainingMs); ///< Slot for syncFinished events.
|
||||
|
||||
private: // data members
|
||||
QList<bridgepp::SPUser> users_; ///< The user list.
|
||||
};
|
||||
|
|
|
@ -16,20 +16,19 @@
|
|||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "Pch.h"
|
||||
#include "BridgeApp.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "CommandLine.h"
|
||||
#include "LogUtils.h"
|
||||
#include "QMLBackend.h"
|
||||
#include "SentryUtils.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "LogUtils.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>
|
||||
#include <bridgepp/Log/Log.h>
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/ProcessMonitor.h>
|
||||
#include <sentry.h>
|
||||
#include <SentryUtils.h>
|
||||
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
|
@ -99,38 +98,6 @@ void initQtApplication() {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return A reference to the log.
|
||||
//****************************************************************************************************************************************************
|
||||
Log &initLog() {
|
||||
Log &log = app().log();
|
||||
log.registerAsQtMessageHandler();
|
||||
log.setEchoInConsole(true);
|
||||
|
||||
// remove old gui log files
|
||||
QDir const logsDir(userLogsDir());
|
||||
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
|
||||
QFile(fileInfo.absoluteFilePath()).remove();
|
||||
}
|
||||
|
||||
// create new GUI log file
|
||||
QString error;
|
||||
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
log.info("bridge-gui starting");
|
||||
QString const qtCompileTimeVersion = QT_VERSION_STR;
|
||||
QString const qtRuntimeVersion = qVersion();
|
||||
QString msg = QString("Using Qt %1").arg(qtRuntimeVersion);
|
||||
if (qtRuntimeVersion != qtCompileTimeVersion) {
|
||||
msg += QString(" (compiled against %1)").arg(qtCompileTimeVersion);
|
||||
}
|
||||
log.info(msg);
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] engine The QML component.
|
||||
|
@ -150,8 +117,9 @@ QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine) {
|
|||
|
||||
rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml"));
|
||||
if (rootComponent->status() != QQmlComponent::Status::Ready) {
|
||||
app().log().error(rootComponent->errorString());
|
||||
throw Exception("Could not load QML component");
|
||||
QString const &err = rootComponent->errorString();
|
||||
app().log().error(err);
|
||||
throw Exception("Could not load QML component", err);
|
||||
}
|
||||
return rootComponent;
|
||||
}
|
||||
|
@ -218,7 +186,7 @@ QUrl getApiUrl() {
|
|||
/// \return true if an instance of bridge is already running.
|
||||
//****************************************************************************************************************************************************
|
||||
bool isBridgeRunning() {
|
||||
QLockFile lockFile(QDir(userCacheDir()).absoluteFilePath(bridgeLock));
|
||||
QLockFile lockFile(QDir(bridgepp::userCacheDir()).absoluteFilePath(bridgeLock));
|
||||
return (!lockFile.tryLock()) && (lockFile.error() == QLockFile::LockFailedError);
|
||||
}
|
||||
|
||||
|
@ -229,9 +197,22 @@ bool isBridgeRunning() {
|
|||
void focusOtherInstance() {
|
||||
try {
|
||||
FocusGRPCClient client;
|
||||
GRPCConfig sc;
|
||||
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
|
||||
QFile file(path);
|
||||
if (file.exists()) {
|
||||
if (!sc.load(path)) {
|
||||
throw Exception("The gRPC focus service configuration file is invalid.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw Exception("Server did not provide gRPC Focus service configuration.");
|
||||
}
|
||||
|
||||
|
||||
QString error;
|
||||
if (!client.connectToServer(5000, &error)) {
|
||||
throw Exception(QString("Could not connect to bridge focus service for a raise call: %1").arg(error));
|
||||
if (!client.connectToServer(5000, sc.port, &error)) {
|
||||
throw Exception("Could not connect to bridge focus service for a raise call.", error);
|
||||
}
|
||||
if (!client.raise().ok()) {
|
||||
throw Exception(QString("The raise call to the bridge focus service failed."));
|
||||
|
@ -240,13 +221,14 @@ void focusOtherInstance() {
|
|||
catch (Exception const &e) {
|
||||
app().log().error(e.qwhat());
|
||||
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
|
||||
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(e.qwhat()));
|
||||
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param [in] args list of arguments to pass to bridge.
|
||||
/// \return bridge executable path
|
||||
//****************************************************************************************************************************************************
|
||||
const QString launchBridge(QStringList const &args) {
|
||||
UPOverseer &overseer = app().bridgeOverseer();
|
||||
|
@ -276,12 +258,8 @@ void closeBridgeApp() {
|
|||
app().grpc().quit(); // this will cause the grpc service and the bridge app to close.
|
||||
|
||||
UPOverseer &overseer = app().bridgeOverseer();
|
||||
if (!overseer) { // The app was run in 'attach' mode and attached to an existing instance of Bridge. We're not monitoring it.
|
||||
return;
|
||||
}
|
||||
|
||||
while (!overseer->isFinished()) {
|
||||
QThread::msleep(20);
|
||||
if (overseer) { // A null overseer means the app was run in 'attach' mode. We're not monitoring it.
|
||||
overseer->wait(Overseer::maxTerminationWaitTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,24 +270,20 @@ void closeBridgeApp() {
|
|||
/// \return The exit code for the application.
|
||||
//****************************************************************************************************************************************************
|
||||
int main(int argc, char *argv[]) {
|
||||
// Init sentry.
|
||||
sentry_options_t *sentryOptions = newSentryOptions(PROJECT_DSN_SENTRY, sentryCacheDir().toStdString().c_str());
|
||||
|
||||
if (sentry_init(sentryOptions) != 0) {
|
||||
std::cerr << "Failed to initialize sentry" << std::endl;
|
||||
}
|
||||
setSentryReportScope();
|
||||
auto sentryClose = qScopeGuard([] { sentry_close(); });
|
||||
|
||||
// The application instance is needed to display system message boxes. As we may have to do it in the exception handler,
|
||||
// application instance is create outside the try/catch clause.
|
||||
if (QSysInfo::productType() != "windows") {
|
||||
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL);
|
||||
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); // must be called before instantiating the BridgeApp
|
||||
}
|
||||
|
||||
BridgeApp guiApp(argc, argv);
|
||||
initSentry();
|
||||
auto sentryCloser = qScopeGuard([] { sentry_close(); });
|
||||
|
||||
try {
|
||||
QString const& configDir = bridgepp::userConfigDir();
|
||||
|
||||
|
||||
initQtApplication();
|
||||
|
||||
Log &log = initLog();
|
||||
|
@ -335,18 +309,20 @@ int main(int argc, char *argv[]) {
|
|||
if (!cliOptions.attach) {
|
||||
if (isBridgeRunning()) {
|
||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||
QString(), QString(), tailOfLatestBridgeLog());
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||
}
|
||||
|
||||
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
||||
GRPCClient::removeServiceConfigFile();
|
||||
FocusGRPCClient::removeServiceConfigFile(configDir);
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
bridgeexec = launchBridge(cliOptions.bridgeArgs);
|
||||
}
|
||||
|
||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
|
||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath(configDir))));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(configDir, cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs,
|
||||
app().bridgeMonitor()));
|
||||
if (!cliOptions.attach) {
|
||||
GRPCClient::removeServiceConfigFile();
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
}
|
||||
|
||||
// gRPC communication is established. From now on, log events will be sent to bridge via gRPC. bridge will write these to file,
|
||||
|
@ -359,7 +335,7 @@ int main(int argc, char *argv[]) {
|
|||
// The following allows to render QML content in software with a 'Rendering Hardware Interface' (OpenGL, Vulkan, Metal, Direct3D...)
|
||||
// Note that it is different from the Qt::AA_UseSoftwareOpenGL attribute we use on some platforms that instruct Qt that we would like
|
||||
// to use a software-only implementation of OpenGL.
|
||||
QQuickWindow::setSceneGraphBackend(cliOptions.useSoftwareRenderer ? "software" : "rhi");
|
||||
QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi");
|
||||
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
|
||||
|
||||
|
||||
|
@ -367,7 +343,7 @@ int main(int argc, char *argv[]) {
|
|||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
||||
if (!rootObject) {
|
||||
throw Exception("Could not create root object.");
|
||||
throw Exception("Could not create QML root object.");
|
||||
}
|
||||
|
||||
ProcessMonitor *bridgeMonitor = app().bridgeMonitor();
|
||||
|
@ -412,7 +388,7 @@ int main(int argc, char *argv[]) {
|
|||
|
||||
QObject::disconnect(connection);
|
||||
app().grpc().stopEventStreamReader();
|
||||
if (!app().backend().waitForEventStreamReaderToFinish(5000)) {
|
||||
if (!app().backend().waitForEventStreamReaderToFinish(Overseer::maxTerminationWaitTimeMs)) {
|
||||
log.warn("Event stream reader took too long to finish.");
|
||||
}
|
||||
|
||||
|
|
|
@ -29,14 +29,19 @@ Item {
|
|||
|
||||
property var _spacing: 12 * ProtonStyle.px
|
||||
|
||||
property color usedSpaceColor : {
|
||||
property color progressColor : {
|
||||
if (!root.enabled) return root.colorScheme.text_weak
|
||||
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
|
||||
if (root.usedFraction < .50) return root.colorScheme.signal_success
|
||||
if (root.usedFraction < .75) return root.colorScheme.signal_warning
|
||||
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak
|
||||
if (root.progressRatio < .50) return root.colorScheme.signal_success
|
||||
if (root.progressRatio < .75) return root.colorScheme.signal_warning
|
||||
return root.colorScheme.signal_danger
|
||||
}
|
||||
property real usedFraction: root.user ? reasonableFraction(root.user.usedBytes, root.user.totalBytes) : 0
|
||||
property real progressRatio: {
|
||||
if (!root.user)
|
||||
return 0
|
||||
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
|
||||
}
|
||||
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
||||
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
||||
|
||||
|
@ -171,18 +176,21 @@ Item {
|
|||
case EUserState.Locked:
|
||||
return qsTr("Connecting") + dotsTimer.dots
|
||||
case EUserState.Connected:
|
||||
return root.usedSpace
|
||||
if (root.user.isSyncing)
|
||||
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots
|
||||
else
|
||||
return root.usedSpace
|
||||
}
|
||||
}
|
||||
|
||||
Timer { // dots animation while connecting. 1 sec cycle, roughly similar to the webmail loading page.
|
||||
Timer { // dots animation while connecting & syncing.
|
||||
id:dotsTimer
|
||||
property string dots: ""
|
||||
interval: 250;
|
||||
interval: 500;
|
||||
repeat: true;
|
||||
running: (root.user != null) && (root.user.state === EUserState.Locked)
|
||||
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
|
||||
onTriggered: {
|
||||
dots = dots + "."
|
||||
dots += "."
|
||||
if (dots.length > 3)
|
||||
dots = ""
|
||||
}
|
||||
|
@ -191,7 +199,7 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
color: root.usedSpaceColor
|
||||
color: root.progressColor
|
||||
type: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Caption
|
||||
|
@ -202,7 +210,7 @@ Item {
|
|||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: root.user && root.user.state == EUserState.Connected ? " / " + root.totalSpace : ""
|
||||
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
||||
color: root.colorScheme.text_weak
|
||||
type: {
|
||||
switch (root.type) {
|
||||
|
@ -213,26 +221,27 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
|
||||
|
||||
Rectangle {
|
||||
id: storage_bar
|
||||
id: progress_bar
|
||||
visible: root.user ? root.type == AccountDelegate.LargeView : false
|
||||
width: 140 * ProtonStyle.px
|
||||
height: 4 * ProtonStyle.px
|
||||
radius: ProtonStyle.storage_bar_radius
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
color: root.colorScheme.border_weak
|
||||
|
||||
Rectangle {
|
||||
id: storage_bar_filled
|
||||
radius: ProtonStyle.storage_bar_radius
|
||||
color: root.usedSpaceColor
|
||||
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected) : false
|
||||
id: progress_bar_filled
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
color: root.progressColor
|
||||
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected): false
|
||||
anchors {
|
||||
top : parent.top
|
||||
bottom : parent.bottom
|
||||
left : parent.left
|
||||
}
|
||||
width: Math.min(1,Math.max(0.02,root.usedFraction)) * parent.width
|
||||
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,8 @@ Item {
|
|||
property var notifications
|
||||
|
||||
signal showSetupGuide(var user, string address)
|
||||
signal closeWindow()
|
||||
signal quitBridge()
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
@ -107,7 +109,7 @@ Item {
|
|||
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 9
|
||||
Layout.rightMargin: 16
|
||||
Layout.rightMargin: 4
|
||||
|
||||
horizontalPadding: 0
|
||||
|
||||
|
@ -115,6 +117,55 @@ Item {
|
|||
|
||||
onClicked: rightContent.showGeneralSettings()
|
||||
}
|
||||
|
||||
Button {
|
||||
id: dotMenuButton
|
||||
Layout.bottomMargin: 9
|
||||
Layout.maximumHeight: 36
|
||||
Layout.maximumWidth: 36
|
||||
Layout.minimumHeight: 36
|
||||
Layout.minimumWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.preferredWidth: 36
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
||||
|
||||
onClicked: {
|
||||
dotMenu.open()
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: dotMenu
|
||||
colorScheme: root.colorScheme
|
||||
modal: true
|
||||
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
|
||||
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Close window")
|
||||
onClicked: {
|
||||
root.closeWindow()
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Quit Bridge")
|
||||
onClicked: {
|
||||
root.quitBridge()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
parent.checked = false
|
||||
}
|
||||
onOpened: {
|
||||
parent.checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {implicitHeight:10}
|
||||
|
|
|
@ -142,6 +142,17 @@ ApplicationWindow {
|
|||
onShowSetupGuide: function(user, address) {
|
||||
root.showSetup(user,address)
|
||||
}
|
||||
|
||||
onCloseWindow: {
|
||||
root.close()
|
||||
}
|
||||
|
||||
onQuitBridge: {
|
||||
// If we ever want to add a confirmation dialog before quitting:
|
||||
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
|
||||
root.close()
|
||||
Backend.quit()
|
||||
}
|
||||
}
|
||||
|
||||
WelcomeGuide { // 1
|
||||
|
|
|
@ -138,4 +138,9 @@ Item {
|
|||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericError
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericQuestion
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ QtObject {
|
|||
signal askResetBridge()
|
||||
signal askChangeAllMailVisibility(var isVisibleNow)
|
||||
signal askDeleteAccount(var user)
|
||||
|
||||
signal askQuestion(var title, var description, var option1, var option2, var action1, var action2)
|
||||
enum Group {
|
||||
Connection = 1,
|
||||
Update = 2,
|
||||
|
@ -81,7 +81,9 @@ QtObject {
|
|||
root.apiCertIssue,
|
||||
root.noActiveKeyForRecipient,
|
||||
root.userBadEvent,
|
||||
root.genericError
|
||||
root.imapLoginWhileSignedOut,
|
||||
root.genericError,
|
||||
root.genericQuestion,
|
||||
]
|
||||
|
||||
// Connection
|
||||
|
@ -1143,6 +1145,34 @@ QtObject {
|
|||
|
||||
}
|
||||
|
||||
property Notification imapLoginWhileSignedOut: Notification {
|
||||
title: qsTr("IMAP Login failed")
|
||||
brief: title
|
||||
description: "#PlaceHolderText"
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Connection
|
||||
|
||||
Connections {
|
||||
target: Backend
|
||||
function onImapLoginWhileSignedOut(username) {
|
||||
root.imapLoginWhileSignedOut.description = qsTr("An email client tried to connect to the account %1, but this account is signed " +
|
||||
"out. Please sign-in to continue.").arg(username)
|
||||
root.imapLoginWhileSignedOut.active = true
|
||||
}
|
||||
}
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("OK")
|
||||
|
||||
onTriggered: {
|
||||
root.imapLoginWhileSignedOut.active = false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
property Notification genericError: Notification {
|
||||
title: "#PlaceholderText#"
|
||||
description: "#PlaceholderText#"
|
||||
|
@ -1168,4 +1198,50 @@ QtObject {
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
property Notification genericQuestion: Notification {
|
||||
title: ""
|
||||
brief: ""
|
||||
description: ""
|
||||
type: Notification.NotificationType.Warning
|
||||
group: Notifications.Group.Dialogs
|
||||
property var option1: ""
|
||||
property var option2: ""
|
||||
property variant action1: null
|
||||
property variant action2: null
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onAskQuestion(title, description, option1, option2, action1, action2) {
|
||||
root.genericQuestion.title = title
|
||||
root.genericQuestion.description = description
|
||||
root.genericQuestion.option1 = option1
|
||||
root.genericQuestion.option2 = option2
|
||||
root.genericQuestion.action1 = action1
|
||||
root.genericQuestion.action2 = action2
|
||||
root.genericQuestion.active = true
|
||||
}
|
||||
}
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: root.genericQuestion.option1
|
||||
|
||||
onTriggered: {
|
||||
root.genericQuestion.active = false
|
||||
if (root.genericQuestion.action1)
|
||||
root.genericQuestion.action1()
|
||||
}
|
||||
},
|
||||
Action {
|
||||
text: root.genericQuestion.option2
|
||||
|
||||
onTriggered: {
|
||||
root.genericQuestion.active = false
|
||||
if (root.genericQuestion.action2)
|
||||
root.genericQuestion.action2()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,13 +26,12 @@ T.MenuItem {
|
|||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
width: parent.width // required. Other item overflows to the right of the menu and get clipped.
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding,
|
||||
implicitIndicatorHeight + topPadding + bottomPadding)
|
||||
|
||||
padding: 6
|
||||
padding: 12
|
||||
spacing: 6
|
||||
|
||||
icon.width: 24
|
||||
|
|
|
@ -362,7 +362,7 @@ QtObject {
|
|||
property real banner_radius : 12 * root.px // px
|
||||
property real dialog_radius : 12 * root.px // px
|
||||
property real card_radius : 12 * root.px // px
|
||||
property real storage_bar_radius : 3 * root.px // px
|
||||
property real progress_bar_radius : 3 * root.px // px
|
||||
property real tooltip_radius : 8 * root.px // px
|
||||
|
||||
property int heading_font_size: 28
|
||||
|
|
|
@ -146,6 +146,7 @@ add_library(bridgepp
|
|||
${FOCUS_PROTO_CPP_FILE} ${FOCUS_PROTO_H_FILE} ${FOCUS_GRPC_CPP_FILE} ${FOCUS_GRPC_H_FILE}
|
||||
bridgepp/FocusGRPC/FocusGRPCClient.cpp bridgepp/FocusGRPC/FocusGRPCClient.h
|
||||
bridgepp/Log/Log.h bridgepp/Log/Log.cpp
|
||||
bridgepp/Log/LogUtils.h bridgepp/Log/LogUtils.cpp
|
||||
bridgepp/ProcessMonitor.cpp bridgepp/ProcessMonitor.h
|
||||
bridgepp/User/User.cpp bridgepp/User/User.h
|
||||
bridgepp/Worker/Worker.h bridgepp/Worker/Overseer.h bridgepp/Worker/Overseer.cpp)
|
||||
|
|
|
@ -156,16 +156,6 @@ QString userDataDir() {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return user logs directory used by bridge.
|
||||
//****************************************************************************************************************************************************
|
||||
QString userLogsDir() {
|
||||
QString const path = QDir(userDataDir()).absoluteFilePath("logs");
|
||||
QDir().mkpath(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return sentry cache directory used by bridge.
|
||||
//****************************************************************************************************************************************************
|
||||
|
|
|
@ -38,7 +38,7 @@ enum class OS {
|
|||
|
||||
QString userConfigDir(); ///< Get the path of the user configuration folder.
|
||||
QString userCacheDir(); ///< Get the path of the user cache folder.
|
||||
QString userLogsDir(); ///< Get the path of the user logs folder.
|
||||
QString userDataDir(); ///< Get the path of the user data folder.
|
||||
QString sentryCacheDir(); ///< Get the path of the sentry cache folder.
|
||||
QString goos(); ///< return the value of Go's GOOS for the current platform ("darwin", "linux" and "windows" are supported).
|
||||
qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n)
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include <stdexcept>
|
||||
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
|
||||
|
||||
|
@ -44,6 +45,9 @@ public: // member functions
|
|||
QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
|
||||
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).
|
||||
|
||||
public: // static data members
|
||||
static qsizetype const attachmentMaxLength {25 * 1024}; ///< The maximum length text attachment sent in Sentry reports, in bytes.
|
||||
|
||||
private: // data members
|
||||
QString const qwhat_; ///< The description of the exception.
|
||||
QByteArray const what_; ///< The c-string version of the qwhat message. Stored as a QByteArray for automatic lifetime management.
|
||||
|
|
|
@ -29,7 +29,6 @@ namespace {
|
|||
|
||||
|
||||
Empty empty; ///< Empty protobuf message, re-used across calls.
|
||||
qint64 const port = 1042; ///< The port for the focus service.
|
||||
QString const hostname = "127.0.0.1"; ///< The hostname of the focus service.
|
||||
|
||||
|
||||
|
@ -39,12 +38,43 @@ QString const hostname = "127.0.0.1"; ///< The hostname of the focus service.
|
|||
namespace bridgepp {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the gRPC Focus server config file name
|
||||
//****************************************************************************************************************************************************
|
||||
QString grpcFocusServerConfigFilename() {
|
||||
return "grpcFocusServerConfig.json";
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The absolute path of the focus service config path.
|
||||
//****************************************************************************************************************************************************
|
||||
QString FocusGRPCClient::grpcFocusServerConfigPath(QString const &configDir) {
|
||||
return QDir(configDir).absoluteFilePath(grpcFocusServerConfigFilename());
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void FocusGRPCClient::removeServiceConfigFile(QString const &configDir) {
|
||||
QString const path = grpcFocusServerConfigPath(configDir);
|
||||
if (!QFile(path).exists()) {
|
||||
return;
|
||||
}
|
||||
if (!QFile().remove(path)) {
|
||||
throw Exception("Could not remove gRPC focus service config file.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] timeoutMs The timeout for the connexion.
|
||||
/// \param[in] port The gRPC server port.
|
||||
/// \param[out] outError if not null and the function returns false.
|
||||
/// \return true iff the connexion was successfully established.
|
||||
//****************************************************************************************************************************************************
|
||||
bool FocusGRPCClient::connectToServer(qint64 timeoutMs, QString *outError) {
|
||||
bool FocusGRPCClient::connectToServer(qint64 timeoutMs, quint16 port, QString *outError) {
|
||||
try {
|
||||
QString const address = QString("%1:%2").arg(hostname).arg(port);
|
||||
channel_ = grpc::CreateChannel(address.toStdString(), grpc::InsecureChannelCredentials());
|
||||
|
|
|
@ -27,10 +27,14 @@
|
|||
namespace bridgepp {
|
||||
|
||||
|
||||
//**********************************************************************************************************************
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Focus GRPC client class
|
||||
//**********************************************************************************************************************
|
||||
//****************************************************************************************************************************************************
|
||||
class FocusGRPCClient {
|
||||
public: // static member functions
|
||||
static void removeServiceConfigFile(QString const &configDir); ///< Delete the service config file.
|
||||
static QString grpcFocusServerConfigPath(QString const &configDir); ///< Return the path of the gRPC Focus server config file.
|
||||
|
||||
public: // member functions.
|
||||
FocusGRPCClient() = default; ///< Default constructor.
|
||||
FocusGRPCClient(FocusGRPCClient const &) = delete; ///< Disabled copy-constructor.
|
||||
|
@ -38,8 +42,8 @@ public: // member functions.
|
|||
~FocusGRPCClient() = default; ///< Destructor.
|
||||
FocusGRPCClient &operator=(FocusGRPCClient const &) = delete; ///< Disabled assignment operator.
|
||||
FocusGRPCClient &operator=(FocusGRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||
bool connectToServer(qint64 timeoutMs, QString *outError = nullptr); ///< Connect to the focus server
|
||||
|
||||
bool connectToServer(qint64 timeoutMs, quint16 port, QString *outError = nullptr); ///< Connect to the focus server
|
||||
grpc::Status raise(); ///< Performs the 'raise' call.
|
||||
grpc::Status version(QString &outVersion); ///< Performs the 'version' call.
|
||||
|
||||
|
|
|
@ -574,6 +574,78 @@ SPStreamEvent newUserBadEvent(QString const &userID, QString const &errorMessage
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] usedBytes The number of used bytes.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newUsedBytesChangedEvent(QString const &userID, qint64 usedBytes) {
|
||||
auto event = new grpc::UsedBytesChangedEvent;
|
||||
event->set_userid(userID.toStdString());
|
||||
event->set_usedbytes(usedBytes);
|
||||
auto userEvent = new grpc::UserEvent;
|
||||
userEvent->set_allocated_usedbyteschangedevent(event);
|
||||
return wrapUserEvent(userEvent);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] username The username that was provided for the failed IMAP login attempt.
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newIMAPLoginFailedEvent(QString const &username) {
|
||||
auto event = new grpc::ImapLoginFailedEvent;
|
||||
event->set_username(username.toStdString());
|
||||
auto userEvent = new grpc::UserEvent;
|
||||
userEvent->set_allocated_imaploginfailedevent(event);
|
||||
return wrapUserEvent(userEvent);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newSyncStartedEvent(QString const &userID) {
|
||||
auto event = new grpc::SyncStartedEvent;
|
||||
event->set_userid(userID.toStdString());
|
||||
auto userEvent = new grpc::UserEvent;
|
||||
userEvent->set_allocated_syncstartedevent(event);
|
||||
return wrapUserEvent(userEvent);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newSyncFinishedEvent(QString const &userID) {
|
||||
auto event = new grpc::SyncFinishedEvent;
|
||||
event->set_userid(userID.toStdString());
|
||||
auto userEvent = new grpc::UserEvent;
|
||||
userEvent->set_allocated_syncfinishedevent(event);
|
||||
return wrapUserEvent(userEvent);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] progress The progress ratio.
|
||||
/// \param[in] elapsedMs The elapsed time in milliseconds.
|
||||
/// \param[in] remainingMs The remaining time in milliseconds.
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newSyncProgressEvent(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs) {
|
||||
auto event = new grpc::SyncProgressEvent;
|
||||
event->set_userid(userID.toStdString());
|
||||
event->set_progress(progress);
|
||||
event->set_elapsedms(elapsedMs);
|
||||
event->set_remainingms(remainingMs);
|
||||
auto userEvent = new grpc::UserEvent;
|
||||
userEvent->set_allocated_syncprogressevent(event);
|
||||
return wrapUserEvent(userEvent);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] errorCode The error errorCode.
|
||||
/// \return The event.
|
||||
|
|
|
@ -78,6 +78,11 @@ SPStreamEvent newToggleSplitModeFinishedEvent(QString const &userID); ///< Creat
|
|||
SPStreamEvent newUserDisconnectedEvent(QString const &username); ///< Create a new UserDisconnectedEvent event.
|
||||
SPStreamEvent newUserChangedEvent(QString const &userID); ///< Create a new UserChangedEvent event.
|
||||
SPStreamEvent newUserBadEvent(QString const &userID, QString const& errorMessage); ///< Create a new UserBadEvent event.
|
||||
SPStreamEvent newUsedBytesChangedEvent(QString const &userID, qint64 usedBytes); ///< Create a new UsedBytesChangedEvent event.
|
||||
SPStreamEvent newIMAPLoginFailedEvent(QString const &username); ///< Create a new ImapLoginFailedEvent event.
|
||||
SPStreamEvent newSyncStartedEvent(QString const &userID); ///< Create a new SyncStarted event.
|
||||
SPStreamEvent newSyncFinishedEvent(QString const &userID); ///< Create a new SyncFinished event.
|
||||
SPStreamEvent newSyncProgressEvent(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs); ///< Create a new SyncFinished event.
|
||||
|
||||
// Generic error event
|
||||
SPStreamEvent newGenericErrorEvent(grpc::ErrorCode errorCode); ///< Create a new GenericErrrorEvent event.
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include "../BridgeUtils.h"
|
||||
#include "../Exception/Exception.h"
|
||||
#include "../ProcessMonitor.h"
|
||||
#include "../Log/LogUtils.h"
|
||||
|
||||
|
||||
using namespace google::protobuf;
|
||||
|
@ -45,8 +46,8 @@ qint64 const grpcConnectionRetryDelayMs = 10000; ///< Retry delay for the gRPC c
|
|||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCClient::removeServiceConfigFile() {
|
||||
QString const path = grpcServerConfigPath();
|
||||
void GRPCClient::removeServiceConfigFile(QString const &configDir) {
|
||||
QString const path = grpcServerConfigPath(configDir);
|
||||
if (!QFile(path).exists()) {
|
||||
return;
|
||||
}
|
||||
|
@ -61,8 +62,8 @@ void GRPCClient::removeServiceConfigFile() {
|
|||
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
|
||||
/// \return The service config.
|
||||
//****************************************************************************************************************************************************
|
||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMonitor *serverProcess) {
|
||||
QString const path = grpcServerConfigPath();
|
||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qint64 timeoutMs, ProcessMonitor *serverProcess) {
|
||||
QString const path = grpcServerConfigPath(configDir);
|
||||
QFile file(path);
|
||||
|
||||
QElapsedTimer timer;
|
||||
|
@ -70,7 +71,8 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMon
|
|||
bool found = false;
|
||||
while (true) {
|
||||
if (serverProcess && serverProcess->getStatus().ended) {
|
||||
throw Exception("Bridge application exited before providing a gRPC service configuration file.");
|
||||
throw Exception("Bridge application exited before providing a gRPC service configuration file.", QString(), __FUNCTION__,
|
||||
tailOfLatestBridgeLog());
|
||||
}
|
||||
|
||||
if (file.exists()) {
|
||||
|
@ -84,13 +86,20 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMon
|
|||
}
|
||||
|
||||
if (!found) {
|
||||
throw Exception("Server did not provide gRPC service configuration in time.");
|
||||
throw Exception("Server did not provide gRPC service configuration in time.", QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||
}
|
||||
|
||||
GRPCConfig sc;
|
||||
QString err;
|
||||
if (!sc.load(path, &err)) {
|
||||
throw Exception("The gRPC service configuration file is invalid.", err);
|
||||
// include the file content in the exception, if any
|
||||
QByteArray array;
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
file.readAll();
|
||||
array = array.right(Exception::attachmentMaxLength);
|
||||
}
|
||||
|
||||
throw Exception("The gRPC service configuration file is invalid.", err, __FUNCTION__, array);
|
||||
}
|
||||
|
||||
return sc;
|
||||
|
@ -109,7 +118,7 @@ void GRPCClient::setLog(Log *log) {
|
|||
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
|
||||
/// \return true iff the connection was successful.
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess) {
|
||||
void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &config, ProcessMonitor *serverProcess) {
|
||||
try {
|
||||
serverToken_ = config.token.toStdString();
|
||||
QString address;
|
||||
|
@ -126,19 +135,20 @@ void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
|
|||
|
||||
channel_ = CreateCustomChannel(address.toStdString(), grpc::SslCredentials(opts), chanArgs);
|
||||
if (!channel_) {
|
||||
throw Exception("Channel creation failed.");
|
||||
throw Exception("gRPC channel creation failed.");
|
||||
}
|
||||
|
||||
stub_ = Bridge::NewStub(channel_);
|
||||
if (!stub_) {
|
||||
throw Exception("Stub creation failed.");
|
||||
throw Exception("gRPC stub creation failed.");
|
||||
}
|
||||
|
||||
QDateTime const giveUpTime = QDateTime::currentDateTime().addMSecs(grpcConnectionWaitTimeoutMs); // if we reach giveUpTime without connecting, we give up
|
||||
int i = 0;
|
||||
while (true) {
|
||||
if (serverProcess && serverProcess->getStatus().ended) {
|
||||
throw Exception("Bridge application ended before gRPC connexion could be established.");
|
||||
throw Exception("Bridge application ended before gRPC connexion could be established.", QString(), __FUNCTION__,
|
||||
tailOfLatestBridgeLog());
|
||||
}
|
||||
|
||||
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
|
||||
|
@ -147,8 +157,10 @@ void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
|
|||
break;
|
||||
} // connection established.
|
||||
|
||||
if (QDateTime::currentDateTime() > giveUpTime)
|
||||
throw Exception("Connection to the RPC server failed.");
|
||||
if (QDateTime::currentDateTime() > giveUpTime) {
|
||||
throw Exception("Connection to the gRPC server failed because of a timeout.", QString(), __FUNCTION__,
|
||||
tailOfLatestBridgeLog());
|
||||
}
|
||||
}
|
||||
|
||||
if (channel_->GetState(true) != GRPC_CHANNEL_READY) {
|
||||
|
@ -159,7 +171,7 @@ void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
|
|||
|
||||
QString const clientToken = QUuid::createUuid().toString();
|
||||
QString error;
|
||||
QString clientConfigPath = createClientConfigFile(clientToken, &error);
|
||||
QString clientConfigPath = createClientConfigFile(configDir, clientToken, &error);
|
||||
if (clientConfigPath.isEmpty()) {
|
||||
throw Exception("gRPC client config could not be saved.", error);
|
||||
}
|
||||
|
@ -179,11 +191,19 @@ void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
|
|||
log_->info("gRPC token was validated");
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
throw Exception("Cannot connect to Go backend via gRPC: " + e.qwhat(), e.details());
|
||||
throw Exception("Cannot connect to Go backend via gRPC: " + e.qwhat(), e.details(), __FUNCTION__, e.attachment());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return true if the gRPC client is connected to the server.
|
||||
//****************************************************************************************************************************************************
|
||||
bool GRPCClient::isConnected() const {
|
||||
return stub_.get();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] clientConfigPath The path to the gRPC client config path.-
|
||||
/// \param[in] serverToken The token obtained from the server config file.
|
||||
|
@ -223,8 +243,9 @@ grpc::Status GRPCClient::addLogEntry(Log::Level level, QString const &package, Q
|
|||
grpc::Status GRPCClient::guiReady(bool &outShowSplashScreen) {
|
||||
GuiReadyResponse response;
|
||||
Status status = this->logGRPCCallStatus(stub_->GuiReady(this->clientContext().get(), empty, &response), __FUNCTION__);
|
||||
if (status.ok())
|
||||
if (status.ok()) {
|
||||
outShowSplashScreen = response.showsplashscreen();
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
|
@ -395,6 +416,8 @@ grpc::Status GRPCClient::setIsDoHEnabled(bool enabled) {
|
|||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCClient::quit() {
|
||||
// quitting will shut down the gRPC service, to we may get an 'Unavailable' response for the call
|
||||
if (!this->isConnected())
|
||||
return Status::OK; // We're not even connected, we return OK. This maybe be an attempt to do 'a proper' shutdown after an unrecoverable error.
|
||||
return this->logGRPCCallStatus(stub_->Quit(this->clientContext().get(), empty, &empty), __FUNCTION__, { StatusCode::UNAVAILABLE });
|
||||
}
|
||||
|
||||
|
@ -458,15 +481,6 @@ grpc::Status GRPCClient::showOnStartup(bool &outValue) {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[out] outGoos The value for the property.
|
||||
/// \return The status for the gRPC call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCClient::goos(QString &outGoos) {
|
||||
return this->logGRPCCallStatus(this->getString(&Bridge::Stub::GoOs, outGoos), __FUNCTION__);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[out] outPath The value for the property.
|
||||
/// \return The status for the gRPC call.
|
||||
|
@ -476,6 +490,15 @@ grpc::Status GRPCClient::logsPath(QUrl &outPath) {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[out] outGoos The value for the property.
|
||||
/// \return The status for the gRPC call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCClient::goos(QString &outGoos) {
|
||||
return this->logGRPCCallStatus(this->getString(&Bridge::Stub::GoOs, outGoos), __FUNCTION__);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[out] outPath The value for the property.
|
||||
/// \return The status for the gRPC call.
|
||||
|
@ -1377,13 +1400,50 @@ void GRPCClient::processUserEvent(UserEvent const &event) {
|
|||
break;
|
||||
}
|
||||
case UserEvent::kUserBadEvent: {
|
||||
UserBadEvent const& e = event.userbadevent();
|
||||
UserBadEvent const &e = event.userbadevent();
|
||||
QString const userID = QString::fromStdString(e.userid());
|
||||
QString const errorMessage = QString::fromStdString(e.errormessage());
|
||||
this->logTrace(QString("User event received: UserBadEvent (userID = %1, errorMessage = %2).").arg(userID, errorMessage));
|
||||
emit userBadEvent(userID, errorMessage);
|
||||
break;
|
||||
}
|
||||
case UserEvent::kUsedBytesChangedEvent: {
|
||||
UsedBytesChangedEvent const &e = event.usedbyteschangedevent();
|
||||
QString const userID = QString::fromStdString(e.userid());
|
||||
qint64 const usedBytes = e.usedbytes();
|
||||
this->logTrace(QString("User event received: UsedBytesChangedEvent (userID = %1, usedBytes = %2).").arg(userID).arg(usedBytes));
|
||||
emit usedBytesChanged(userID, usedBytes);
|
||||
break;
|
||||
}
|
||||
case UserEvent::kImapLoginFailedEvent: {
|
||||
ImapLoginFailedEvent const &e = event.imaploginfailedevent();
|
||||
QString const username = QString::fromStdString(e.username());
|
||||
this->logTrace(QString("User event received: IMAPLoginFailed (username = %1).:").arg(username));
|
||||
emit imapLoginFailed(username);
|
||||
break;
|
||||
}
|
||||
case UserEvent::kSyncStartedEvent: {
|
||||
SyncStartedEvent const &e = event.syncstartedevent();
|
||||
QString const &userID = QString::fromStdString(e.userid());
|
||||
this->logTrace(QString("User event received: SyncStarted (userID = %1).:").arg(userID));
|
||||
emit syncStarted(userID);
|
||||
break;
|
||||
}
|
||||
case UserEvent::kSyncFinishedEvent: {
|
||||
SyncFinishedEvent const &e = event.syncfinishedevent();
|
||||
QString const &userID = QString::fromStdString(e.userid());
|
||||
this->logTrace(QString("User event received: SyncFinished (userID = %1).:").arg(userID));
|
||||
emit syncFinished(userID);
|
||||
break;
|
||||
}
|
||||
case UserEvent::kSyncProgressEvent: {
|
||||
SyncProgressEvent const &e = event.syncprogressevent();
|
||||
QString const &userID = QString::fromStdString(e.userid());
|
||||
this->logTrace(QString("User event received SyncProgress (userID = %1, progress = %2, elapsedMs = %3, remainingMs = %4).").arg(userID)
|
||||
.arg(e.progress()).arg(e.elapsedms()).arg(e.remainingms()));
|
||||
emit syncProgress(userID, e.progress(), e.elapsedms(), e.remainingms());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this->logError("Unknown User event received.");
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ typedef std::unique_ptr<grpc::ClientContext> UPClientContext;
|
|||
class GRPCClient : public QObject {
|
||||
Q_OBJECT
|
||||
public: // static member functions
|
||||
static void removeServiceConfigFile(); ///< Delete the service config file.
|
||||
static GRPCConfig waitAndRetrieveServiceConfig(qint64 timeoutMs, class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
|
||||
static void removeServiceConfigFile(QString const &configDir); ///< Delete the service config file.
|
||||
static GRPCConfig waitAndRetrieveServiceConfig(QString const &configDir, qint64 timeoutMs, class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
|
||||
|
||||
public: // member functions.
|
||||
GRPCClient() = default; ///< Default constructor.
|
||||
|
@ -59,7 +59,8 @@ public: // member functions.
|
|||
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
||||
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||
void setLog(Log *log); ///< Set the log for the client.
|
||||
void connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
|
||||
void connectToServer(QString const &configDir, GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
|
||||
bool isConnected() const; ///< Check whether the gRPC client is connected to the server.
|
||||
|
||||
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
|
||||
grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call.
|
||||
|
@ -180,6 +181,11 @@ signals:
|
|||
void userDisconnected(QString const &username);
|
||||
void userChanged(QString const &userID);
|
||||
void userBadEvent(QString const &userID, QString const& errorMessage);
|
||||
void usedBytesChanged(QString const &userID, qint64 usedBytes);
|
||||
void imapLoginFailed(QString const& username);
|
||||
void syncStarted(QString const &userID);
|
||||
void syncFinished(QString const &userID);
|
||||
void syncProgress(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs);
|
||||
|
||||
public: // keychain related calls
|
||||
grpc::Status availableKeychains(QStringList &outKeychains);
|
||||
|
|
|
@ -59,18 +59,19 @@ bool useFileSocketForGRPC() {
|
|||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] configDir The folder containing the configuration files.
|
||||
/// \return The absolute path of the service config path.
|
||||
//****************************************************************************************************************************************************
|
||||
QString grpcServerConfigPath() {
|
||||
return QDir(userConfigDir()).absoluteFilePath(grpcServerConfigFilename());
|
||||
QString grpcServerConfigPath(QString const &configDir) {
|
||||
return QDir(configDir).absoluteFilePath(grpcServerConfigFilename());
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The absolute path of the service config path.
|
||||
//****************************************************************************************************************************************************
|
||||
QString grpcClientConfigBasePath() {
|
||||
return QDir(userConfigDir()).absoluteFilePath(grpcClientConfigBaseFilename());
|
||||
QString grpcClientConfigBasePath(QString const &configDir) {
|
||||
return QDir(configDir).absoluteFilePath(grpcClientConfigBaseFilename());
|
||||
}
|
||||
|
||||
|
||||
|
@ -81,8 +82,8 @@ QString grpcClientConfigBasePath() {
|
|||
/// \return The path of the created file.
|
||||
/// \return A null string if the file could not be saved.
|
||||
//****************************************************************************************************************************************************
|
||||
QString createClientConfigFile(QString const &token, QString *outError) {
|
||||
QString const basePath = grpcClientConfigBasePath();
|
||||
QString createClientConfigFile(QString const &configDir, QString const &token, QString *outError) {
|
||||
QString const basePath = grpcClientConfigBasePath(configDir);
|
||||
QString path, error;
|
||||
for (qint32 i = 0; i < 1000; ++i) // we try a decent amount of times
|
||||
{
|
||||
|
@ -255,4 +256,4 @@ QString getAvailableFileSocketPath() {
|
|||
}
|
||||
|
||||
|
||||
} // namespace bridgepp
|
||||
} // namespace bridgepp
|
||||
|
|
|
@ -34,9 +34,9 @@ extern std::string const grpcMetadataServerTokenKey; ///< The key for the server
|
|||
typedef std::shared_ptr<grpc::StreamEvent> SPStreamEvent; ///< Type definition for shared pointer to grpc::StreamEvent.
|
||||
|
||||
|
||||
QString grpcServerConfigPath(); ///< Return the path of the gRPC server config file.
|
||||
QString grpcClientConfigBasePath(); ///< Return the path of the gRPC client config file.
|
||||
QString createClientConfigFile(QString const &token, QString *outError); ///< Create the client config file the server will retrieve and return its path.
|
||||
QString grpcServerConfigPath(QString const &configDir); ///< Return the path of the gRPC server config file.
|
||||
QString grpcClientConfigBasePath(QString const &configDir); ///< Return the path of the gRPC client config file.
|
||||
QString createClientConfigFile(QString const &configDir, QString const &token, QString *outError); ///< Create the client config file the server will retrieve and return its path.
|
||||
grpc::LogLevel logLevelToGRPC(Log::Level level); ///< Convert a Log::Level to gRPC enum value.
|
||||
Log::Level logLevelFromGRPC(grpc::LogLevel level); ///< Convert a grpc::LogLevel to a Log::Level.
|
||||
grpc::UserState userStateToGRPC(UserState state); ///< Convert a bridgepp::UserState to a grpc::UserState.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,68 @@
|
|||
// 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/>.
|
||||
|
||||
|
||||
#include "LogUtils.h"
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return user logs directory used by bridge.
|
||||
//****************************************************************************************************************************************************
|
||||
QString userLogsDir() {
|
||||
QString const path = QDir(bridgepp::userDataDir()).absoluteFilePath("logs");
|
||||
QDir().mkpath(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Return the path of the latest bridge log.
|
||||
/// \return The path of the latest bridge log file.
|
||||
/// \return An empty string if no bridge log file was found.
|
||||
//****************************************************************************************************************************************************
|
||||
QString latestBridgeLogPath() {
|
||||
QDir const logsDir(userLogsDir());
|
||||
if (logsDir.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
|
||||
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
|
||||
return lhs.birthTime() < rhs.birthTime();
|
||||
});
|
||||
return files.back().absoluteFilePath();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// Return the maxSize last bytes of the latest bridge log.
|
||||
//****************************************************************************************************************************************************
|
||||
QByteArray tailOfLatestBridgeLog() {
|
||||
QString path = latestBridgeLogPath();
|
||||
if (path.isEmpty()) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::Text | QIODevice::ReadOnly) ? file.readAll().right(Exception::attachmentMaxLength) : QByteArray();
|
||||
}
|
||||
|
||||
|
||||
} // namespace bridgepp
|
|
@ -0,0 +1,33 @@
|
|||
// 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/>.
|
||||
|
||||
|
||||
#ifndef BRIDGE_PP_LOG_UTILS_H
|
||||
#define BRIDGE_PP_LOG_UTILS_H
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
|
||||
|
||||
QString userLogsDir(); ///< Return the path of the user logs dir.
|
||||
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
|
||||
|
||||
|
||||
} // namespace bridgepp
|
||||
|
||||
|
||||
#endif //BRIDGE_PP_LOG_UTILS_H
|
|
@ -34,7 +34,8 @@ SPUser User::newUser(QObject *parent) {
|
|||
/// \param[in] parent The parent object.
|
||||
//****************************************************************************************************************************************************
|
||||
User::User(QObject *parent)
|
||||
: QObject(parent) {
|
||||
: QObject(parent)
|
||||
, imapFailureCooldownEndTime_(QDateTime::currentDateTime()) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -293,6 +294,48 @@ void User::setTotalBytes(float totalBytes) {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return true iff a sync is in progress.
|
||||
//****************************************************************************************************************************************************
|
||||
bool User::isSyncing() const {
|
||||
return isSyncing_;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] syncing The new value for the sync state.
|
||||
//****************************************************************************************************************************************************
|
||||
void User::setIsSyncing(bool syncing) {
|
||||
if (isSyncing_ == syncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSyncing_ = syncing;
|
||||
emit isSyncingChanged(syncing);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The sync progress ratio
|
||||
//****************************************************************************************************************************************************
|
||||
float User::syncProgress() const {
|
||||
return syncProgress_;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] progress The progress ratio.
|
||||
//****************************************************************************************************************************************************
|
||||
void User::setSyncProgress(float progress) {
|
||||
if (qAbs(syncProgress_ - progress) < 0.00001) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncProgress_ = progress;
|
||||
emit syncProgressChanged(progress);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] state The user state.
|
||||
/// \return A string describing the state.
|
||||
|
@ -311,4 +354,24 @@ QString User::stateToString(UserState state) {
|
|||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// We display a notification and pop the application window if an IMAP client tries to connect to a signed out account, but we do not want to
|
||||
/// do it repeatedly, as it's an intrusive action. This function let's you define a period of time during which the notification should not be
|
||||
/// displayed.
|
||||
///
|
||||
/// \param durationMSecs The duration of the period in milliseconds.
|
||||
//****************************************************************************************************************************************************
|
||||
void User::startImapLoginFailureCooldown(qint64 durationMSecs) {
|
||||
imapFailureCooldownEndTime_ = QDateTime::currentDateTime().addMSecs(durationMSecs);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return true if we currently are in a cooldown period for the notification
|
||||
//****************************************************************************************************************************************************
|
||||
bool User::isInIMAPLoginFailureCooldown() const {
|
||||
return QDateTime::currentDateTime() < imapFailureCooldownEndTime_;
|
||||
}
|
||||
|
||||
|
||||
} // namespace bridgepp
|
||||
|
|
|
@ -74,6 +74,8 @@ public: // member functions.
|
|||
User &operator=(User &&) = delete; ///< Disabled move assignment operator.
|
||||
void update(User const &user); ///< Update the user.
|
||||
Q_INVOKABLE QString primaryEmailOrUsername() const; ///< Return the user primary email, or, if unknown its username.
|
||||
void startImapLoginFailureCooldown(qint64 durationMSecs); ///< Start the user cooldown period for the IMAP login attempt while signed-out notification.
|
||||
bool isInIMAPLoginFailureCooldown() const; ///< Check if the user in a IMAP login failure notification.
|
||||
|
||||
public slots:
|
||||
// slots for QML generated calls
|
||||
|
@ -99,6 +101,8 @@ public:
|
|||
Q_PROPERTY(bool splitMode READ splitMode WRITE setSplitMode NOTIFY splitModeChanged)
|
||||
Q_PROPERTY(float usedBytes READ usedBytes WRITE setUsedBytes NOTIFY usedBytesChanged)
|
||||
Q_PROPERTY(float totalBytes READ totalBytes WRITE setTotalBytes NOTIFY totalBytesChanged)
|
||||
Q_PROPERTY(bool isSyncing READ isSyncing WRITE setIsSyncing NOTIFY isSyncingChanged)
|
||||
Q_PROPERTY(float syncProgress READ syncProgress WRITE setSyncProgress NOTIFY syncProgressChanged)
|
||||
|
||||
QString id() const;
|
||||
void setID(QString const &id);
|
||||
|
@ -118,6 +122,10 @@ public:
|
|||
void setUsedBytes(float usedBytes);
|
||||
float totalBytes() const;
|
||||
void setTotalBytes(float totalBytes);
|
||||
bool isSyncing() const;
|
||||
void setIsSyncing(bool syncing);
|
||||
float syncProgress() const;
|
||||
void setSyncProgress(float progress);
|
||||
|
||||
signals:
|
||||
// signals used for Qt properties
|
||||
|
@ -132,11 +140,14 @@ signals:
|
|||
void usedBytesChanged(float byteCount);
|
||||
void totalBytesChanged(float byteCount);
|
||||
void toggleSplitModeFinished();
|
||||
void isSyncingChanged(bool syncing);
|
||||
void syncProgressChanged(float syncProgress);
|
||||
|
||||
private: // member functions.
|
||||
User(QObject *parent); ///< Default constructor.
|
||||
|
||||
private: // data members.
|
||||
QDateTime imapFailureCooldownEndTime_; ///< The end date/time for the IMAP login failure notification cooldown period.
|
||||
QString id_; ///< The userID.
|
||||
QString username_; ///< The username
|
||||
QString password_; ///< The IMAP password of the user.
|
||||
|
@ -146,6 +157,8 @@ private: // data members.
|
|||
bool splitMode_ { false }; ///< Is split mode active.
|
||||
float usedBytes_ { 0.0f }; ///< The storage used by the user.
|
||||
float totalBytes_ { 1.0f }; ///< The storage quota of the user.
|
||||
bool isSyncing_ { false }; ///< Is a sync in progress for the user.
|
||||
float syncProgress_ { 0.0f }; ///< The sync progress.
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -84,7 +84,8 @@ void Overseer::releaseWorker() {
|
|||
if (thread_) {
|
||||
if (!thread_->isFinished()) {
|
||||
thread_->quit();
|
||||
thread_->wait();
|
||||
if (!thread_->wait(maxTerminationWaitTimeMs))
|
||||
thread_->terminate();
|
||||
}
|
||||
thread_->deleteLater();
|
||||
thread_ = nullptr;
|
||||
|
|
|
@ -46,6 +46,9 @@ public slots:
|
|||
void startWorker(bool autorelease) const; ///< Run the worker.
|
||||
void releaseWorker(); ///< Delete the worker and its thread.
|
||||
|
||||
public: // static data members
|
||||
static qint64 const maxTerminationWaitTimeMs { 10000 }; ///< The maximum wait time for the termination of a thread
|
||||
|
||||
public: // data members.
|
||||
QThread *thread_ { nullptr }; ///< The thread.
|
||||
Worker *worker_ { nullptr }; ///< The worker.
|
||||
|
|
|
@ -115,7 +115,7 @@ func (f *frontendCLI) showAccountAddressInfo(user bridge.UserInfo, address strin
|
|||
f.Println("")
|
||||
}
|
||||
|
||||
func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen
|
||||
func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue