mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3210709810 | |||
| 8fd988d7c5 | |||
| bf89d548d3 | |||
| 51229cbb68 | |||
| 36c5c37dac | |||
| 5a434fafbc | |||
| ea1c2534df | |||
| 1cafbfcaaa | |||
| 2d44ccaee0 | |||
| 96517b7fb1 | |||
| bc381407a7 | |||
| ddc5e775b9 | |||
| ea26188dc0 | |||
| 159e1cee7d | |||
| 4394ad0e9b | |||
| 856bdd1321 | |||
| ff288145df | |||
| 83bbdbd63e | |||
| fa430ee0fb | |||
| 0303ba38e8 | |||
| 2a78b5c144 | |||
| a00b3cdb92 | |||
| 8d3e04679f | |||
| 21ff7b4b97 | |||
| 4ea161f7ad | |||
| dc584ea29b | |||
| 4a01c46aed | |||
| e8d9534b9c | |||
| 96904b160f | |||
| b535be72f8 | |||
| 40f2d8b30f | |||
| 95a1acec0d | |||
| 5ff074cc49 | |||
| 4f0660bb8c | |||
| 708184439e | |||
| b8a33b9618 | |||
| 1c385d5c9b | |||
| 96773f3225 | |||
| 0f320dbd80 | |||
| 6cb233473a | |||
| 1ac4e70115 | |||
| 07f93d276b | |||
| d29571fb01 | |||
| d6000d025e | |||
| 09ef3b20db | |||
| 405331d59b | |||
| eff7df2136 | |||
| 5823e3a99f | |||
| 26d866bbbd | |||
| d3f7be059d | |||
| b52706a3ca | |||
| aebe7baed0 | |||
| ef31e2917c | |||
| 9eea26459a | |||
| 5747b85543 | |||
| ff78a23084 | |||
| 2a95e1ab41 | |||
| ab76cab533 | |||
| dda2a5d01a | |||
| c2afb42fd4 | |||
| 1d53044803 | |||
| d3f8297eb4 | |||
| b02203e3d3 | |||
| 5c7e4e04f9 | |||
| d7dadd7578 | |||
| ab9a758d63 | |||
| cb0935be96 | |||
| 441b388f62 | |||
| cdbcd30d15 | |||
| acc7ca8d4a | |||
| 42e1dd4c41 | |||
| 4cbd3ca832 | |||
| de0b6c0737 | |||
| 1c344211d1 | |||
| c11a87c16a | |||
| 3bf4282037 |
@ -18,6 +18,10 @@
|
|||||||
---
|
---
|
||||||
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
||||||
|
|
||||||
|
default:
|
||||||
|
tags:
|
||||||
|
- shared-small
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
GOPRIVATE: gitlab.protontech.ch
|
GOPRIVATE: gitlab.protontech.ch
|
||||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||||
@ -118,7 +122,7 @@ stages:
|
|||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||||
tags:
|
tags:
|
||||||
- large
|
- shared-large
|
||||||
|
|
||||||
# Stage: TEST
|
# Stage: TEST
|
||||||
|
|
||||||
@ -129,7 +133,7 @@ lint:
|
|||||||
script:
|
script:
|
||||||
- make lint
|
- make lint
|
||||||
tags:
|
tags:
|
||||||
- medium
|
- shared-medium
|
||||||
|
|
||||||
bug-report-preview:
|
bug-report-preview:
|
||||||
stage: test
|
stage: test
|
||||||
@ -138,7 +142,7 @@ bug-report-preview:
|
|||||||
script:
|
script:
|
||||||
- make lint-bug-report-preview
|
- make lint-bug-report-preview
|
||||||
tags:
|
tags:
|
||||||
- medium
|
- shared-medium
|
||||||
|
|
||||||
.script-test:
|
.script-test:
|
||||||
stage: test
|
stage: test
|
||||||
@ -154,7 +158,7 @@ test-linux:
|
|||||||
extends:
|
extends:
|
||||||
- .script-test
|
- .script-test
|
||||||
tags:
|
tags:
|
||||||
- large
|
- shared-large
|
||||||
|
|
||||||
fuzz-linux:
|
fuzz-linux:
|
||||||
stage: test
|
stage: test
|
||||||
@ -163,7 +167,7 @@ fuzz-linux:
|
|||||||
script:
|
script:
|
||||||
- make fuzz
|
- make fuzz
|
||||||
tags:
|
tags:
|
||||||
- large
|
- shared-large
|
||||||
|
|
||||||
test-linux-race:
|
test-linux-race:
|
||||||
extends:
|
extends:
|
||||||
@ -218,7 +222,7 @@ test-coverage:
|
|||||||
- test-integration
|
- test-integration
|
||||||
- test-integration-nightly
|
- test-integration-nightly
|
||||||
tags:
|
tags:
|
||||||
- small
|
- shared-small
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- coverage*
|
- coverage*
|
||||||
@ -282,7 +286,7 @@ build-windows-qa:
|
|||||||
variables:
|
variables:
|
||||||
BUILD_TAGS: "build_qa"
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
trigeer-qa-installer:
|
trigger-qa-installer:
|
||||||
stage: build
|
stage: build
|
||||||
needs: ["lint"]
|
needs: ["lint"]
|
||||||
extends:
|
extends:
|
||||||
|
|||||||
@ -133,5 +133,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
* [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)
|
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||||
|
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE)
|
||||||
|
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)
|
||||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||||
<!-- END AUTOGEN -->
|
<!-- END AUTOGEN -->
|
||||||
|
|||||||
104
Changelog.md
104
Changelog.md
@ -3,6 +3,97 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Wakato Bridge 3.7.1
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Test(GODT-2740): Sending Plain text messages to internal recipient.
|
||||||
|
* Test(GODT-2892): Create fake log file.
|
||||||
|
* GODT-3122: Added test, changed interface for accessing display name.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Remove debug prints.
|
||||||
|
* GODT-2576: Forward and $Forward Flag Support.
|
||||||
|
* GODT-3053: Use smaller bridge window on small screens.
|
||||||
|
* GODT-3113: Only force UTF-8 charset for HTML part when needed.
|
||||||
|
* GODT-3113: Do not render HTML for attachment.
|
||||||
|
* GODT-3112: Replaced error message when bridge exists prematurely. Added a link to support form.
|
||||||
|
* GODT-2947: Remove 'blame it on the weather' error part from go-smtp.
|
||||||
|
* GODT-3010: Log MimeType parsing issue.
|
||||||
|
* GODT-3104: Added log entry for cert install status on startup on macOS.
|
||||||
|
* GODT-2277: Move Keychain helpers creation in main.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3054: Only delete drafts after message has been Sent.
|
||||||
|
* GODT-2576: Correctly handle Forwarded messages from Thunderbird.
|
||||||
|
* GODT-3122: Use display name as 'Email Account Name' in macOS profile.
|
||||||
|
* GODT-3125: Heartbeat crash on exit.
|
||||||
|
* GODT-2617: Validate user can send from the SMTP sender address.
|
||||||
|
* GODT-3123: Trigger bad event on empty EventID on existing accounts.
|
||||||
|
* GODT-3118: Do not reset EventID when migrating sync settings.
|
||||||
|
* GODT-3116: Panic on closed channel.
|
||||||
|
* GODT-1623: Throttle SMTP failed requests.
|
||||||
|
* GODT-3047: Fixed 'disk full' error message.
|
||||||
|
* GODT-3054: Delete draft create from reply.
|
||||||
|
* GODT-3048: WKD Policy behavior.
|
||||||
|
|
||||||
|
|
||||||
|
## Wakato Bridge 3.7.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Test(GODT-1224): Add testing around package creation.
|
||||||
|
* Add debug_assemble binary.
|
||||||
|
* Test(GODT-2723): Add importing a message with remote content.
|
||||||
|
* Test(GODT-2737): Sending HTML messages to internal.
|
||||||
|
* Test(GODT-3036): Keep inline attachment order on GPA Fake Server.
|
||||||
|
* GODT-3015: Add simple algorithm to deal with multiple attachment for bug report.
|
||||||
|
* Test: make message structure check more verbose.
|
||||||
|
* Test: Add test around account settings.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-3097: Warn about PGPInline encryption scheme which will be deprecated.
|
||||||
|
* Test: Support multiple users when waiting for sync event.
|
||||||
|
* Test: Update fake server with defautl draft content-type and test it.
|
||||||
|
* Test: be less aggressive while checking for message structure.
|
||||||
|
* GODT-2996: Set password fields to hidden when resetting the login form.
|
||||||
|
* GODT-2990: Change runner tags.
|
||||||
|
* GODT-2835: Bump GPA adding support for AsyncAttachments for BugReport +...
|
||||||
|
* GODT-2940: Allow 3 attempts for mailbox password.
|
||||||
|
* GODT-3095: Update GOpenPGP.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3106: Broken import route.
|
||||||
|
* GODT-3041: Fix Invalid Or Missing message signature during send.
|
||||||
|
* GODT-3087: Exclude attachment content-disposition part when determining...
|
||||||
|
* GODT-2887: Inline images with Apple Mail.
|
||||||
|
* GODT-3100: Fix issue where a fatal error that bubble up to cli.Run() is not written in the log file.
|
||||||
|
* GODT-3094: Clean up old update files on bridge startup.
|
||||||
|
* GODT-3012: Fix multipart request retries.
|
||||||
|
* GODT-2935: Do not allow parentID into drafts.
|
||||||
|
* GODT-2935: Correct error message when draft fails to create.
|
||||||
|
* GODT-2970: Correctly handle rename of Inbox.
|
||||||
|
* GODT-2969: Prevent duration corruption for config status event.
|
||||||
|
* Fixed type in QA installer CI job name.
|
||||||
|
* GODT-3019: Fix title of main window when no account is connected.
|
||||||
|
* GODT-3013: IMAP service getting "stuck".
|
||||||
|
* GODT-2966: Allow permissive parsing of MediaType parameters for import.
|
||||||
|
* GODT-2966: Add more test regarding quoted/unquoted filename in attachment.
|
||||||
|
* GODT-2490: Fix sync progress not being reset when toggling split mode.
|
||||||
|
* GODT-2515: Customized notification of unavailable keychain on macOS.
|
||||||
|
|
||||||
|
|
||||||
|
## Vasco da Gama Bridge 3.6.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3033: Unable to receive new mail.
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.4
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3033: Unable to receive new mail.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Vasco da Gama Bridge 3.6.0
|
## Vasco da Gama Bridge 3.6.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -21,6 +112,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2664: Trigger QA installer.
|
* GODT-2664: Trigger QA installer.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
* GODT-2992: Fix link in 'no account view' in main window after 2FA or TOTP are cancelled.
|
||||||
* GODT-2989: Allow to send bug report when no account connected.
|
* GODT-2989: Allow to send bug report when no account connected.
|
||||||
* GODT-2988: Fix setup wizard KB links.
|
* GODT-2988: Fix setup wizard KB links.
|
||||||
* GODT-2968: Use proper base64 encoded string even for bad password test.
|
* GODT-2968: Use proper base64 encoded string even for bad password test.
|
||||||
@ -32,6 +124,18 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2929: Message dedup with different text transfer encoding.
|
* GODT-2929: Message dedup with different text transfer encoding.
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.3
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-3004: Update gopenpgp and dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.2
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3003: Ensure IMAP State is reset after vault corruption.
|
||||||
|
* GODT-3001: Only create system labels during system label sync.
|
||||||
|
|
||||||
|
|
||||||
## Umshiang Bridge 3.5.1
|
## Umshiang Bridge 3.5.1
|
||||||
|
|
||||||
|
|||||||
3
Makefile
3
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.6.0+git
|
BRIDGE_APP_VERSION?=3.7.1+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -304,6 +304,7 @@ ApplyStageInput,BuildStageInput,BuildStageOutput,DownloadStageInput,DownloadStag
|
|||||||
StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \
|
StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \
|
||||||
> tmp
|
> tmp
|
||||||
mv tmp internal/services/syncservice/mocks_test.go
|
mv tmp internal/services/syncservice/mocks_test.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/gluon/connector IMAPStateWrite > internal/services/imapservice/mocks/mocks.go
|
||||||
|
|
||||||
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report
|
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -44,7 +43,5 @@ import (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
|
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
go.mod
24
go.mod
@ -5,10 +5,10 @@ go 1.20
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20230911134257-5eb2eeebbef5
|
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230925123025-331ad8e6d5ee
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||||
@ -43,17 +43,17 @@ require (
|
|||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
go.uber.org/goleak v1.2.1
|
go.uber.org/goleak v1.2.1
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.17.0
|
||||||
golang.org/x/sys v0.8.0
|
golang.org/x/sys v0.13.0
|
||||||
golang.org/x/text v0.9.0
|
golang.org/x/text v0.13.0
|
||||||
google.golang.org/grpc v1.53.0
|
google.golang.org/grpc v1.56.3
|
||||||
google.golang.org/protobuf v1.30.0
|
google.golang.org/protobuf v1.30.0
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 // indirect
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||||
@ -79,7 +79,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
@ -110,16 +110,18 @@ require (
|
|||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.14.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
golang.org/x/sync v0.2.0 // indirect
|
golang.org/x/sync v0.2.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
||||||
|
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
||||||
|
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605
|
||||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
|
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
|
||||||
)
|
)
|
||||||
|
|||||||
62
go.sum
62
go.sum
@ -15,6 +15,8 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA
|
|||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
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/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/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605 h1:54Fh3JS6s2Tjy6ZIRLtt1amZOqfYDcjErdye45z8fkQ=
|
||||||
|
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
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/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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
@ -23,24 +25,25 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20230911134257-5eb2eeebbef5 h1:O4BusNL870VgVVDSUX2Oaz8A/fNtJhakUKwx0YBIdn8=
|
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20230911134257-5eb2eeebbef5/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
|
||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230925123025-331ad8e6d5ee h1:CzFXOiflEZZqT3HQqj2I5AkIprRbc/c6/lToPdEKzxM=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc h1:GBRKoFAldApEMkMrsFN1ZxG0eG797w6LTv/dFMDcsqQ=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230925123025-331ad8e6d5ee/go.mod h1:Y3ea3i1UbqHz5vq43odmAAd6lmR4nx0ZIQ32tqMfxTY=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
|
||||||
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||||
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
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 h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||||
@ -64,6 +67,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
|||||||
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
||||||
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
@ -120,8 +124,6 @@ github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp
|
|||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+Xa0rBOqSlG6Qe9dKC/2vLhGAuZlWxTsc=
|
|
||||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
|
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
|
||||||
@ -155,8 +157,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
@ -178,8 +178,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@ -419,9 +419,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -464,14 +465,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -514,16 +516,22 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
@ -531,12 +539,16 @@ 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.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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -582,13 +594,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-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-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-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
||||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
|
||||||
"github.com/pkg/profile"
|
"github.com/pkg/profile"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -204,7 +205,7 @@ func run(c *cli.Context) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Restart the app if requested.
|
// Restart the app if requested.
|
||||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||||
// Handle crashes with various actions.
|
// Handle crashes with various actions.
|
||||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||||
migrationErr := migrateOldVersions()
|
migrationErr := migrateOldVersions()
|
||||||
@ -234,53 +235,56 @@ func run(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||||
// Unlock the encrypted vault.
|
// Look for available keychains
|
||||||
return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
return WithKeychainList(func(keychains *keychain.List) error {
|
||||||
if !v.Migrated() {
|
// Unlock the encrypted vault.
|
||||||
// Migrate old settings into the vault.
|
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||||
if err := migrateOldSettings(v); err != nil {
|
if !v.Migrated() {
|
||||||
logrus.WithError(err).Error("Failed to migrate old settings")
|
// Migrate old settings into the vault.
|
||||||
}
|
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, v); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to migrate old accounts")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The vault has been migrated.
|
|
||||||
if err := v.SetMigrated(); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to mark vault as migrated")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"lastVersion": v.GetLastVersion().String(),
|
|
||||||
"showAllMail": v.GetShowAllMail(),
|
|
||||||
"updateCh": v.GetUpdateChannel(),
|
|
||||||
"autoUpdate": v.GetAutoUpdate(),
|
|
||||||
"rollout": v.GetUpdateRollout(),
|
|
||||||
"DoH": v.GetProxyAllowed(),
|
|
||||||
}).Info("Vault loaded")
|
|
||||||
|
|
||||||
// Load the cookies from the vault.
|
|
||||||
return withCookieJar(v, func(cookieJar http.CookieJar) error {
|
|
||||||
// Create a new bridge instance.
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if corrupt {
|
// Migrate old accounts into the vault.
|
||||||
logrus.Warn("The vault is corrupt and has been wiped")
|
if err := migrateOldAccounts(locations, keychains, v); err != nil {
|
||||||
b.PushError(bridge.ErrVaultCorrupt)
|
logrus.WithError(err).Error("Failed to migrate old accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start telemetry heartbeat process
|
// The vault has been migrated.
|
||||||
b.StartHeartbeat(b)
|
if err := v.SetMigrated(); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to mark vault as migrated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run the frontend.
|
logrus.WithFields(logrus.Fields{
|
||||||
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
"lastVersion": v.GetLastVersion().String(),
|
||||||
|
"showAllMail": v.GetShowAllMail(),
|
||||||
|
"updateCh": v.GetUpdateChannel(),
|
||||||
|
"autoUpdate": v.GetAutoUpdate(),
|
||||||
|
"rollout": v.GetUpdateRollout(),
|
||||||
|
"DoH": v.GetProxyAllowed(),
|
||||||
|
}).Info("Vault loaded")
|
||||||
|
|
||||||
|
// Load the cookies from the vault.
|
||||||
|
return withCookieJar(v, func(cookieJar http.CookieJar) error {
|
||||||
|
// Create a new bridge instance.
|
||||||
|
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, keychains, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if corrupt {
|
||||||
|
logrus.Warn("The vault is corrupt and has been wiped")
|
||||||
|
b.PushError(bridge.ErrVaultCorrupt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old updates files
|
||||||
|
b.RemoveOldUpdates()
|
||||||
|
|
||||||
|
// Run the frontend.
|
||||||
|
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -290,6 +294,13 @@ func run(c *cli.Context) error {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// if an error occurs, it must be logged now because we're about to close the log file.
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's another instance already running, try to raise it and exit.
|
// If there's another instance already running, try to raise it and exit.
|
||||||
@ -470,6 +481,13 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
|||||||
return fn(persister)
|
return fn(persister)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithKeychainList init the list of usable keychains.
|
||||||
|
func WithKeychainList(fn func(*keychain.List) error) error {
|
||||||
|
logrus.Debug("Creating keychain list")
|
||||||
|
defer logrus.Debug("Keychain list stop")
|
||||||
|
return fn(keychain.NewList())
|
||||||
|
}
|
||||||
|
|
||||||
func setDeviceCookies(jar *cookies.Jar) error {
|
func setDeviceCookies(jar *cookies.Jar) error {
|
||||||
url, err := url.Parse(constants.APIHost)
|
url, err := url.Parse(constants.APIHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
@ -55,6 +56,7 @@ func withBridge(
|
|||||||
reporter *sentry.Reporter,
|
reporter *sentry.Reporter,
|
||||||
vault *vault.Vault,
|
vault *vault.Vault,
|
||||||
cookieJar http.CookieJar,
|
cookieJar http.CookieJar,
|
||||||
|
keychains *keychain.List,
|
||||||
fn func(*bridge.Bridge, <-chan events.Event) error,
|
fn func(*bridge.Bridge, <-chan events.Event) error,
|
||||||
) error {
|
) error {
|
||||||
logrus.Debug("Creating bridge")
|
logrus.Debug("Creating bridge")
|
||||||
@ -97,6 +99,7 @@ func withBridge(
|
|||||||
autostarter,
|
autostarter,
|
||||||
updater,
|
updater,
|
||||||
version,
|
version,
|
||||||
|
keychains,
|
||||||
|
|
||||||
// The API stuff.
|
// The API stuff.
|
||||||
constants.APIHost,
|
constants.APIHost,
|
||||||
@ -110,6 +113,7 @@ func withBridge(
|
|||||||
crashHandler,
|
crashHandler,
|
||||||
reporter,
|
reporter,
|
||||||
imap.DefaultEpochUIDValidityGenerator(),
|
imap.DefaultEpochUIDValidityGenerator(),
|
||||||
|
nil,
|
||||||
|
|
||||||
// The logging stuff.
|
// The logging stuff.
|
||||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||||
@ -155,7 +159,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return updater.NewUpdater(
|
return updater.NewUpdater(
|
||||||
updater.NewInstaller(versioner.New(updatesDir)),
|
versioner.New(updatesDir),
|
||||||
verifier,
|
verifier,
|
||||||
constants.UpdateName,
|
constants.UpdateName,
|
||||||
runtime.GOOS,
|
runtime.GOOS,
|
||||||
|
|||||||
@ -122,7 +122,7 @@ func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
|
|||||||
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
|
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List, v *vault.Vault) error {
|
||||||
logrus.Info("Migrating accounts")
|
logrus.Info("Migrating accounts")
|
||||||
|
|
||||||
settings, err := locations.ProvideSettingsPath()
|
settings, err := locations.ProvideSettingsPath()
|
||||||
@ -134,8 +134,7 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get helper: %w", err)
|
return fmt.Errorf("failed to get helper: %w", err)
|
||||||
}
|
}
|
||||||
|
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||||
keychain, err := keychain.NewKeychain(helper, "bridge")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create keychain: %w", err)
|
return fmt.Errorf("failed to create keychain: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,6 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -133,11 +132,9 @@ func TestKeychainMigration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUserMigration(t *testing.T) {
|
func TestUserMigration(t *testing.T) {
|
||||||
keychainHelper := keychain.NewTestHelper()
|
kcl := keychain.NewTestKeychainsList()
|
||||||
|
|
||||||
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
|
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
|
||||||
|
|
||||||
kc, err := keychain.NewKeychain("mock", "bridge")
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NoError(t, kc.Put("brokenID", "broken"))
|
require.NoError(t, kc.Put("brokenID", "broken"))
|
||||||
@ -178,7 +175,7 @@ func TestUserMigration(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, corrupt)
|
require.False(t, corrupt)
|
||||||
|
|
||||||
require.NoError(t, migrateOldAccounts(locations, v))
|
require.NoError(t, migrateOldAccounts(locations, kcl, v))
|
||||||
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
|
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
|
||||||
|
|
||||||
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
|
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"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/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
@ -29,12 +30,12 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
||||||
logrus.Debug("Creating vault")
|
logrus.Debug("Creating vault")
|
||||||
defer logrus.Debug("Vault stopped")
|
defer logrus.Debug("Vault stopped")
|
||||||
|
|
||||||
// Create the encVault.
|
// Create the encVault.
|
||||||
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
|
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not create vault: %w", err)
|
return fmt.Errorf("could not create vault: %w", err)
|
||||||
}
|
}
|
||||||
@ -44,12 +45,15 @@ func WithVault(locations *locations.Locations, panicHandler async.PanicHandler,
|
|||||||
"corrupt": corrupt,
|
"corrupt": corrupt,
|
||||||
}).Debug("Vault created")
|
}).Debug("Vault created")
|
||||||
|
|
||||||
|
cert, _ := encVault.GetBridgeTLSCert()
|
||||||
|
certs.NewInstaller().LogCertInstallStatus(cert)
|
||||||
|
|
||||||
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
||||||
|
|
||||||
return fn(encVault, insecure, corrupt)
|
return fn(encVault, insecure, corrupt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
|
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
|
||||||
vaultDir, err := locations.ProvideSettingsPath()
|
vaultDir, err := locations.ProvideSettingsPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||||
@ -62,7 +66,7 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
|
|||||||
insecure bool
|
insecure bool
|
||||||
)
|
)
|
||||||
|
|
||||||
if key, err := loadVaultKey(vaultDir); err != nil {
|
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
|
||||||
logrus.WithError(err).Error("Could not load/create vault key")
|
logrus.WithError(err).Error("Could not load/create vault key")
|
||||||
insecure = true
|
insecure = true
|
||||||
|
|
||||||
@ -85,13 +89,13 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
|
|||||||
return vault, insecure, corrupt, nil
|
return vault, insecure, corrupt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadVaultKey(vaultDir string) ([]byte, error) {
|
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
|
||||||
helper, err := vault.GetHelper(vaultDir)
|
helper, err := vault.GetHelper(vaultDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -74,7 +75,7 @@ type Bridge struct {
|
|||||||
installCh chan installJob
|
installCh chan installJob
|
||||||
|
|
||||||
// heartbeat is the telemetry heartbeat for metrics.
|
// heartbeat is the telemetry heartbeat for metrics.
|
||||||
heartbeat telemetry.Heartbeat
|
heartbeat *heartBeatState
|
||||||
|
|
||||||
// curVersion is the current version of the bridge,
|
// curVersion is the current version of the bridge,
|
||||||
// newVersion is the version that was installed by the updater.
|
// newVersion is the version that was installed by the updater.
|
||||||
@ -82,6 +83,9 @@ type Bridge struct {
|
|||||||
newVersion *semver.Version
|
newVersion *semver.Version
|
||||||
newVersionLock safe.RWMutex
|
newVersionLock safe.RWMutex
|
||||||
|
|
||||||
|
// keychains is the utils that own usable keychains found in the OS.
|
||||||
|
keychains *keychain.List
|
||||||
|
|
||||||
// focusService is used to raise the bridge window when needed.
|
// focusService is used to raise the bridge window when needed.
|
||||||
focusService *focus.Service
|
focusService *focus.Service
|
||||||
|
|
||||||
@ -124,9 +128,6 @@ type Bridge struct {
|
|||||||
// goUpdate triggers a check/install of updates.
|
// goUpdate triggers a check/install of updates.
|
||||||
goUpdate func()
|
goUpdate func()
|
||||||
|
|
||||||
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
|
||||||
goHeartbeat func()
|
|
||||||
|
|
||||||
serverManager *imapsmtpserver.Service
|
serverManager *imapsmtpserver.Service
|
||||||
syncService *syncservice.Service
|
syncService *syncservice.Service
|
||||||
}
|
}
|
||||||
@ -138,6 +139,7 @@ func New(
|
|||||||
autostarter Autostarter, // the autostarter to manage autostart settings
|
autostarter Autostarter, // the autostarter to manage autostart settings
|
||||||
updater Updater, // the updater to fetch and install updates
|
updater Updater, // the updater to fetch and install updates
|
||||||
curVersion *semver.Version, // the current version of the bridge
|
curVersion *semver.Version, // the current version of the bridge
|
||||||
|
keychains *keychain.List, // usable keychains
|
||||||
|
|
||||||
apiURL string, // the URL of the API to use
|
apiURL string, // the URL of the API to use
|
||||||
cookieJar http.CookieJar, // the cookie jar to use
|
cookieJar http.CookieJar, // the cookie jar to use
|
||||||
@ -148,6 +150,7 @@ func New(
|
|||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
uidValidityGenerator imap.UIDValidityGenerator,
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
|
heartBeatManager telemetry.HeartbeatManager,
|
||||||
|
|
||||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||||
logSMTP bool, // whether to log SMTP activity
|
logSMTP bool, // whether to log SMTP activity
|
||||||
@ -163,6 +166,7 @@ func New(
|
|||||||
|
|
||||||
// bridge is the bridge.
|
// bridge is the bridge.
|
||||||
bridge, err := newBridge(
|
bridge, err := newBridge(
|
||||||
|
context.Background(),
|
||||||
tasks,
|
tasks,
|
||||||
imapEventCh,
|
imapEventCh,
|
||||||
|
|
||||||
@ -171,6 +175,7 @@ func New(
|
|||||||
autostarter,
|
autostarter,
|
||||||
updater,
|
updater,
|
||||||
curVersion,
|
curVersion,
|
||||||
|
keychains,
|
||||||
panicHandler,
|
panicHandler,
|
||||||
reporter,
|
reporter,
|
||||||
|
|
||||||
@ -178,6 +183,7 @@ func New(
|
|||||||
identifier,
|
identifier,
|
||||||
proxyCtl,
|
proxyCtl,
|
||||||
uidValidityGenerator,
|
uidValidityGenerator,
|
||||||
|
heartBeatManager,
|
||||||
logIMAPClient, logIMAPServer, logSMTP,
|
logIMAPClient, logIMAPServer, logSMTP,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -196,6 +202,7 @@ func New(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newBridge(
|
func newBridge(
|
||||||
|
ctx context.Context,
|
||||||
tasks *async.Group,
|
tasks *async.Group,
|
||||||
imapEventCh chan imapEvents.Event,
|
imapEventCh chan imapEvents.Event,
|
||||||
|
|
||||||
@ -204,6 +211,7 @@ func newBridge(
|
|||||||
autostarter Autostarter,
|
autostarter Autostarter,
|
||||||
updater Updater,
|
updater Updater,
|
||||||
curVersion *semver.Version,
|
curVersion *semver.Version,
|
||||||
|
keychains *keychain.List,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
|
|
||||||
@ -211,6 +219,7 @@ func newBridge(
|
|||||||
identifier identifier.Identifier,
|
identifier identifier.Identifier,
|
||||||
proxyCtl ProxyController,
|
proxyCtl ProxyController,
|
||||||
uidValidityGenerator imap.UIDValidityGenerator,
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
|
heartbeatManager telemetry.HeartbeatManager,
|
||||||
|
|
||||||
logIMAPClient, logIMAPServer, logSMTP bool,
|
logIMAPClient, logIMAPServer, logSMTP bool,
|
||||||
) (*Bridge, error) {
|
) (*Bridge, error) {
|
||||||
@ -256,9 +265,13 @@ func newBridge(
|
|||||||
newVersion: curVersion,
|
newVersion: curVersion,
|
||||||
newVersionLock: safe.NewRWMutex(),
|
newVersionLock: safe.NewRWMutex(),
|
||||||
|
|
||||||
|
keychains: keychains,
|
||||||
|
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
reporter: reporter,
|
reporter: reporter,
|
||||||
|
|
||||||
|
heartbeat: newHeartBeatState(ctx, panicHandler),
|
||||||
|
|
||||||
focusService: focusService,
|
focusService: focusService,
|
||||||
autostarter: autostarter,
|
autostarter: autostarter,
|
||||||
locator: locator,
|
locator: locator,
|
||||||
@ -288,6 +301,12 @@ func newBridge(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if heartbeatManager == nil {
|
||||||
|
bridge.heartbeat.init(bridge, bridge)
|
||||||
|
} else {
|
||||||
|
bridge.heartbeat.init(bridge, heartbeatManager)
|
||||||
|
}
|
||||||
|
|
||||||
bridge.syncService.Run(bridge.tasks)
|
bridge.syncService.Run(bridge.tasks)
|
||||||
|
|
||||||
return bridge, nil
|
return bridge, nil
|
||||||
@ -417,6 +436,9 @@ func (bridge *Bridge) GetErrors() []error {
|
|||||||
func (bridge *Bridge) Close(ctx context.Context) {
|
func (bridge *Bridge) Close(ctx context.Context) {
|
||||||
logrus.Info("Closing bridge")
|
logrus.Info("Closing bridge")
|
||||||
|
|
||||||
|
// Stop heart beat before closing users.
|
||||||
|
bridge.heartbeat.stop()
|
||||||
|
|
||||||
// Close all users.
|
// Close all users.
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
@ -487,27 +509,15 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
|
|||||||
watcher.Close()
|
watcher.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusUp(ctx context.Context) {
|
func (bridge *Bridge) onStatusUp(_ context.Context) {
|
||||||
logrus.Info("Handling API status up")
|
logrus.Info("Handling API status up")
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
user.OnStatusUp(ctx)
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
|
|
||||||
bridge.goLoad()
|
bridge.goLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||||
logrus.Info("Handling API status down")
|
logrus.Info("Handling API status down")
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
user.OnStatusDown(ctx)
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
|
|
||||||
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@ -49,6 +49,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/tests"
|
"github.com/ProtonMail/proton-bridge/v3/tests"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
imapid "github.com/emersion/go-imap-id"
|
imapid "github.com/emersion/go-imap-id"
|
||||||
@ -585,7 +586,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
|
|||||||
require.NoError(t, os.RemoveAll(gluonDir))
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
// Bridge starts but can't find the gluon store dir; there should be no error.
|
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -950,6 +951,7 @@ func withBridgeNoMocks(
|
|||||||
mocks.Autostarter,
|
mocks.Autostarter,
|
||||||
mocks.Updater,
|
mocks.Updater,
|
||||||
v2_3_0,
|
v2_3_0,
|
||||||
|
keychain.NewTestKeychainsList(),
|
||||||
|
|
||||||
// The API stuff.
|
// The API stuff.
|
||||||
apiURL,
|
apiURL,
|
||||||
@ -961,6 +963,7 @@ func withBridgeNoMocks(
|
|||||||
mocks.CrashHandler,
|
mocks.CrashHandler,
|
||||||
mocks.Reporter,
|
mocks.Reporter,
|
||||||
testUIDValidityGenerator,
|
testUIDValidityGenerator,
|
||||||
|
mocks.Heartbeat,
|
||||||
|
|
||||||
// The logging stuff.
|
// The logging stuff.
|
||||||
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
||||||
@ -970,9 +973,6 @@ func withBridgeNoMocks(
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, bridge.GetErrors())
|
require.Empty(t, bridge.GetErrors())
|
||||||
|
|
||||||
// Start the Heartbeat process.
|
|
||||||
bridge.StartHeartbeat(mocks.Heartbeat)
|
|
||||||
|
|
||||||
// Wait for bridge to finish loading users.
|
// Wait for bridge to finish loading users.
|
||||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
@ -33,63 +34,133 @@ const (
|
|||||||
DefaultMaxSessionCountForBugReport = 10
|
DefaultMaxSessionCountForBugReport = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
|
type ReportBugReq struct {
|
||||||
var account = username
|
OSType string
|
||||||
|
OSVersion string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
EmailClient string
|
||||||
|
IncludeLogs bool
|
||||||
|
}
|
||||||
|
|
||||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
|
||||||
account = info.Username
|
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
|
||||||
|
report.Username = info.Username
|
||||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||||
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
||||||
account = user.Username()
|
report.Username = user.Username()
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachment []proton.ReportBugAttachment
|
var attachments []proton.ReportBugAttachment
|
||||||
|
if report.IncludeLogs {
|
||||||
if attachLogs {
|
logs, err := bridge.CollectLogs()
|
||||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
attachments = append(attachments, logs)
|
||||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
attachment = append(attachment, proton.ReportBugAttachment{
|
|
||||||
Name: "logs.zip",
|
|
||||||
Filename: "logs.zip",
|
|
||||||
MIMEType: "application/zip",
|
|
||||||
Body: body,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
safe.Lock(func() {
|
var firstAtt proton.ReportBugAttachment
|
||||||
|
if len(attachments) > 0 && report.IncludeLogs {
|
||||||
|
firstAtt = attachments[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentType := proton.AttachmentTypeSync
|
||||||
|
if len(attachments) > 1 {
|
||||||
|
attachmentType = proton.AttachmentTypeAsync
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := bridge.createTicket(ctx, report, attachmentType, firstAtt)
|
||||||
|
if err != nil || token == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugSent()
|
user.ReportBugSent()
|
||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
// if we have a token we can append more attachment to the bugReport
|
||||||
OS: osType,
|
for i, att := range attachments {
|
||||||
OSVersion: osVersion,
|
if i == 0 && report.IncludeLogs {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := bridge.appendComment(ctx, token, att)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
Title: "[Bridge] Bug - " + title,
|
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
|
||||||
Description: description,
|
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
Client: client,
|
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return proton.ReportBugAttachment{
|
||||||
|
Name: "logs.zip",
|
||||||
|
Filename: "logs.zip",
|
||||||
|
MIMEType: "application/zip",
|
||||||
|
Body: body,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) createTicket(ctx context.Context, report *ReportBugReq,
|
||||||
|
asyncAttach proton.AttachmentType, att proton.ReportBugAttachment) (string, error) {
|
||||||
|
var attachments []proton.ReportBugAttachment
|
||||||
|
attachments = append(attachments, att)
|
||||||
|
res, err := bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||||
|
OS: report.OSType,
|
||||||
|
OSVersion: report.OSVersion,
|
||||||
|
|
||||||
|
Title: "[Bridge] Bug - " + report.Title,
|
||||||
|
Description: report.Description,
|
||||||
|
|
||||||
|
Client: report.EmailClient,
|
||||||
ClientType: proton.ClientTypeEmail,
|
ClientType: proton.ClientTypeEmail,
|
||||||
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
||||||
|
|
||||||
Username: account,
|
Username: report.Username,
|
||||||
Email: email,
|
Email: report.Email,
|
||||||
}, attachment...)
|
|
||||||
|
AsyncAttachments: asyncAttach,
|
||||||
|
}, attachments...)
|
||||||
|
|
||||||
|
if err != nil || asyncAttach != proton.AttachmentTypeAsync {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if asyncAttach == proton.AttachmentTypeAsync && res.Token == nil {
|
||||||
|
return "", errors.New("no token returns for AsyncAttachments")
|
||||||
|
}
|
||||||
|
|
||||||
|
return *res.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) appendComment(ctx context.Context, token string, att proton.ReportBugAttachment) error {
|
||||||
|
var attachments []proton.ReportBugAttachment
|
||||||
|
attachments = append(attachments, att)
|
||||||
|
return bridge.api.ReportBugAttachement(ctx, proton.ReportBugAttachmentReq{
|
||||||
|
Product: proton.ClientTypeEmail,
|
||||||
|
Body: "Comment adding attachment: " + att.Filename,
|
||||||
|
Token: token,
|
||||||
|
}, attachments...)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBugClicked() {
|
func (bridge *Bridge) ReportBugClicked() {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugClicked()
|
user.ReportBugClicked()
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ func (bridge *Bridge) ReportBugClicked() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) AutoconfigUsed(client string) {
|
func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.AutoconfigUsed(client)
|
user.AutoconfigUsed(client)
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.KBArticleOpened(article)
|
user.KBArticleOpened(article)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
|
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
|
||||||
@ -30,8 +31,8 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigureAppleMail configures apple mail for the given userID and address.
|
// ConfigureAppleMail configures Apple Mail for the given userID and address.
|
||||||
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
|
// If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL.
|
||||||
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
|
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"userID": userID,
|
"userID": userID,
|
||||||
@ -44,16 +45,28 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
|
|||||||
return ErrNoSuchUser
|
return ErrNoSuchUser
|
||||||
}
|
}
|
||||||
|
|
||||||
if address == "" {
|
emails := user.Emails()
|
||||||
address = user.Emails()[0]
|
displayNames := user.DisplayNames()
|
||||||
|
if (len(emails) == 0) || (len(displayNames) == 0) {
|
||||||
|
return errors.New("could not retrieve user address info")
|
||||||
}
|
}
|
||||||
|
|
||||||
username := address
|
if address == "" {
|
||||||
addresses := address
|
address = emails[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var username, displayName, addresses string
|
||||||
if user.GetAddressMode() == vault.CombinedMode {
|
if user.GetAddressMode() == vault.CombinedMode {
|
||||||
username = user.Emails()[0]
|
username = address
|
||||||
addresses = strings.Join(user.Emails(), ",")
|
displayName = displayNames[username]
|
||||||
|
addresses = strings.Join(emails, ",")
|
||||||
|
} else {
|
||||||
|
username = address
|
||||||
|
addresses = address
|
||||||
|
displayName = displayNames[address]
|
||||||
|
if len(displayName) == 0 {
|
||||||
|
displayName = address
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
||||||
@ -69,6 +82,7 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
|
|||||||
bridge.vault.GetIMAPSSL(),
|
bridge.vault.GetIMAPSSL(),
|
||||||
bridge.vault.GetSMTPSSL(),
|
bridge.vault.GetSMTPSSL(),
|
||||||
username,
|
username,
|
||||||
|
displayName,
|
||||||
addresses,
|
addresses,
|
||||||
user.BridgePass(),
|
user.BridgePass(),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,18 +20,100 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const HeartbeatCheckInterval = time.Hour
|
const HeartbeatCheckInterval = time.Hour
|
||||||
|
|
||||||
|
type heartBeatState struct {
|
||||||
|
task *async.Group
|
||||||
|
telemetry.Heartbeat
|
||||||
|
taskLock sync.Mutex
|
||||||
|
taskStarted bool
|
||||||
|
taskInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeartBeatState(ctx context.Context, panicHandler async.PanicHandler) *heartBeatState {
|
||||||
|
return &heartBeatState{
|
||||||
|
task: async.NewGroup(ctx, panicHandler),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager) {
|
||||||
|
h.Heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), bridge.keychains.GetDefaultHelper())
|
||||||
|
h.taskInterval = manager.GetHeartbeatPeriodicInterval()
|
||||||
|
h.SetRollout(bridge.GetUpdateRollout())
|
||||||
|
h.SetAutoStart(bridge.GetAutostart())
|
||||||
|
h.SetAutoUpdate(bridge.GetAutoUpdate())
|
||||||
|
h.SetBeta(bridge.GetUpdateChannel())
|
||||||
|
h.SetDoh(bridge.GetProxyAllowed())
|
||||||
|
h.SetShowAllMail(bridge.GetShowAllMail())
|
||||||
|
h.SetIMAPConnectionMode(bridge.GetIMAPSSL())
|
||||||
|
h.SetSMTPConnectionMode(bridge.GetSMTPSSL())
|
||||||
|
h.SetIMAPPort(bridge.GetIMAPPort())
|
||||||
|
h.SetSMTPPort(bridge.GetSMTPPort())
|
||||||
|
h.SetCacheLocation(bridge.GetGluonCacheDir())
|
||||||
|
if val, err := bridge.GetKeychainApp(); err != nil {
|
||||||
|
h.SetKeyChainPref(val)
|
||||||
|
} else {
|
||||||
|
h.SetKeyChainPref(bridge.keychains.GetDefaultHelper())
|
||||||
|
}
|
||||||
|
h.SetPrevVersion(bridge.GetLastVersion().String())
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
|
var splitMode = false
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
if user.GetAddressMode() == vault.SplitMode {
|
||||||
|
splitMode = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var nbAccount = len(bridge.users)
|
||||||
|
h.SetNbAccount(nbAccount)
|
||||||
|
h.SetSplitMode(splitMode)
|
||||||
|
|
||||||
|
// Do not try to send if there is no user yet.
|
||||||
|
if nbAccount > 0 {
|
||||||
|
defer h.start()
|
||||||
|
}
|
||||||
|
}, bridge.usersLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *heartBeatState) start() {
|
||||||
|
h.taskLock.Lock()
|
||||||
|
defer h.taskLock.Unlock()
|
||||||
|
if h.taskStarted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.taskStarted = true
|
||||||
|
|
||||||
|
h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) {
|
||||||
|
logrus.Debug("Checking for heartbeat")
|
||||||
|
|
||||||
|
h.TrySending(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *heartBeatState) stop() {
|
||||||
|
h.taskLock.Lock()
|
||||||
|
defer h.taskLock.Unlock()
|
||||||
|
if !h.taskStarted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.task.CancelAndWait()
|
||||||
|
h.taskStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
|
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
|
||||||
var flag = true
|
var flag = true
|
||||||
if bridge.GetTelemetryDisabled() {
|
if bridge.GetTelemetryDisabled() {
|
||||||
@ -80,49 +162,6 @@ func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
|
|||||||
return bridge.vault.SetLastHeartbeatSent(timestamp)
|
return bridge.vault.SetLastHeartbeatSent(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
|
func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration {
|
||||||
bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper)
|
return HeartbeatCheckInterval
|
||||||
|
|
||||||
// Check for heartbeat when triggered.
|
|
||||||
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
|
|
||||||
logrus.Debug("Checking for heartbeat")
|
|
||||||
|
|
||||||
bridge.heartbeat.TrySending(ctx)
|
|
||||||
})
|
|
||||||
|
|
||||||
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
|
|
||||||
bridge.heartbeat.SetAutoStart(bridge.GetAutostart())
|
|
||||||
bridge.heartbeat.SetAutoUpdate(bridge.GetAutoUpdate())
|
|
||||||
bridge.heartbeat.SetBeta(bridge.GetUpdateChannel())
|
|
||||||
bridge.heartbeat.SetDoh(bridge.GetProxyAllowed())
|
|
||||||
bridge.heartbeat.SetShowAllMail(bridge.GetShowAllMail())
|
|
||||||
bridge.heartbeat.SetIMAPConnectionMode(bridge.GetIMAPSSL())
|
|
||||||
bridge.heartbeat.SetSMTPConnectionMode(bridge.GetSMTPSSL())
|
|
||||||
bridge.heartbeat.SetIMAPPort(bridge.GetIMAPPort())
|
|
||||||
bridge.heartbeat.SetSMTPPort(bridge.GetSMTPPort())
|
|
||||||
bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir())
|
|
||||||
if val, err := bridge.GetKeychainApp(); err != nil {
|
|
||||||
bridge.heartbeat.SetKeyChainPref(val)
|
|
||||||
} else {
|
|
||||||
bridge.heartbeat.SetKeyChainPref(keychain.DefaultHelper)
|
|
||||||
}
|
|
||||||
bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String())
|
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
var splitMode = false
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
if user.GetAddressMode() == vault.SplitMode {
|
|
||||||
splitMode = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var nbAccount = len(bridge.users)
|
|
||||||
bridge.heartbeat.SetNbAccount(nbAccount)
|
|
||||||
bridge.heartbeat.SetSplitMode(splitMode)
|
|
||||||
|
|
||||||
// Do not try to send if there is no user yet.
|
|
||||||
if nbAccount > 0 {
|
|
||||||
defer bridge.goHeartbeat()
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
internal/bridge/keychain.go
Normal file
24
internal/bridge/keychain.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import "golang.org/x/exp/maps"
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetHelpersNames() []string {
|
||||||
|
return maps.Keys(bridge.keychains.GetHelpers())
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||||
@ -51,6 +52,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
|||||||
|
|
||||||
// this is called at start of heartbeat process.
|
// this is called at start of heartbeat process.
|
||||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||||
|
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
|
||||||
|
|
||||||
return mocks
|
return mocks
|
||||||
}
|
}
|
||||||
@ -154,3 +156,7 @@ func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Down
|
|||||||
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -36,6 +36,20 @@ func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
|
|||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHeartbeatPeriodicInterval mocks base method.
|
||||||
|
func (m *MockHeartbeatManager) GetHeartbeatPeriodicInterval() time.Duration {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetHeartbeatPeriodicInterval")
|
||||||
|
ret0, _ := ret[0].(time.Duration)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeartbeatPeriodicInterval indicates an expected call of GetHeartbeatPeriodicInterval.
|
||||||
|
func (mr *MockHeartbeatManagerMockRecorder) GetHeartbeatPeriodicInterval() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeartbeatPeriodicInterval", reflect.TypeOf((*MockHeartbeatManager)(nil).GetHeartbeatPeriodicInterval))
|
||||||
|
}
|
||||||
|
|
||||||
// GetLastHeartbeatSent mocks base method.
|
// GetLastHeartbeatSent mocks base method.
|
||||||
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
|
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
@ -336,6 +337,9 @@ func TestBridge_SendInvite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
||||||
|
// NOTE: Prior to GODT-2887, these tests had inline images, however after the implementation to support
|
||||||
|
// inline images new parts are injected to reference inline images without content-id set. The images
|
||||||
|
// in this test have been changed to regular attachments to keep the original checks in place.
|
||||||
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
||||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
Subject: A new message
|
Subject: A new message
|
||||||
@ -343,7 +347,7 @@ Date: Mon, 13 Mar 2023 16:06:16 +0100
|
|||||||
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
Content-Disposition: inline;
|
Content-Disposition: attachment;
|
||||||
filename=Cat_August_2010-4.jpeg
|
filename=Cat_August_2010-4.jpeg
|
||||||
Content-Type: image/jpeg;
|
Content-Type: image/jpeg;
|
||||||
name="Cat_August_2010-4.jpeg"
|
name="Cat_August_2010-4.jpeg"
|
||||||
@ -360,7 +364,7 @@ Subject: A new message Part2
|
|||||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
Content-Disposition: inline;
|
Content-Disposition: attachment;
|
||||||
filename=Cat_August_2010-4.jpeg
|
filename=Cat_August_2010-4.jpeg
|
||||||
Content-Type: image/jpeg;
|
Content-Type: image/jpeg;
|
||||||
name="Cat_August_2010-4.jpeg"
|
name="Cat_August_2010-4.jpeg"
|
||||||
@ -520,3 +524,234 @@ SGVsbG8gd29ybGQK
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendInlineImage(t *testing.T) {
|
||||||
|
const messageInlineImageOnly = `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 messageInlineImageWithHTML = `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-Type: text/html;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--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 messageInlineImageWithText = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part3
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/plain;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--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 messageInlineImageFollowedByText = `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-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/plain;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--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) {
|
||||||
|
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||||
|
defer smtpWaiter.Done()
|
||||||
|
|
||||||
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
messages := []string{
|
||||||
|
messageInlineImageOnly,
|
||||||
|
messageInlineImageWithHTML,
|
||||||
|
messageInlineImageWithText,
|
||||||
|
messageInlineImageFollowedByText,
|
||||||
|
}
|
||||||
|
|
||||||
|
smtpWaiter.Wait()
|
||||||
|
|
||||||
|
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 := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
||||||
|
defer senderIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// Connect the recipient IMAP client.
|
||||||
|
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
||||||
|
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if len(messages) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// messages may not be in order
|
||||||
|
for _, message := range messages {
|
||||||
|
require.Equal(t, 1, len(message.BodyStructure.Parts))
|
||||||
|
require.Equal(t, "multipart", message.BodyStructure.MIMEType)
|
||||||
|
require.Equal(t, "mixed", message.BodyStructure.MIMESubType)
|
||||||
|
require.Equal(t, "multipart", message.BodyStructure.Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "related", message.BodyStructure.Parts[0].MIMESubType)
|
||||||
|
require.Len(t, message.BodyStructure.Parts[0].Parts, 2)
|
||||||
|
require.Equal(t, "text", message.BodyStructure.Parts[0].Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "html", message.BodyStructure.Parts[0].Parts[0].MIMESubType)
|
||||||
|
require.Equal(t, "image", message.BodyStructure.Parts[0].Parts[1].MIMEType)
|
||||||
|
require.Equal(t, "jpeg", message.BodyStructure.Parts[0].Parts[1].MIMESubType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendAddressDisabled(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
recipientUserID, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
senderUserID, addrID, err := s.CreateUser("sender", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||||
|
defer smtpWaiter.Done()
|
||||||
|
|
||||||
|
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
smtpWaiter.Wait()
|
||||||
|
|
||||||
|
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 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}))
|
||||||
|
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
string(senderInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
err = client.SendMail(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
[]string{recipientInfo.Addresses[0]},
|
||||||
|
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
|
||||||
|
)
|
||||||
|
|
||||||
|
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
|
||||||
|
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -261,9 +261,12 @@ func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// If telemetry is re-enabled locally, try to send the heartbeat.
|
// If telemetry is re-enabled locally, try to send the heartbeat.
|
||||||
if !isDisabled {
|
if isDisabled {
|
||||||
defer bridge.goHeartbeat()
|
bridge.heartbeat.stop()
|
||||||
|
} else {
|
||||||
|
bridge.heartbeat.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
"github.com/bradenaw/juniper/iterator"
|
"github.com/bradenaw/juniper/iterator"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
@ -579,6 +580,67 @@ func TestBridge_MessageCreateDuringSync(t *testing.T) {
|
|||||||
}, server.WithTLS(false))
|
}, server.WithTLS(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(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)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for sync to finish
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
settingsPath, err := locator.ProvideSettingsPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
|
||||||
|
// Check sync state is complete
|
||||||
|
{
|
||||||
|
state, err := imapservice.NewSyncState(syncStatePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, syncStatus.IsComplete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// corrupt the vault
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
|
||||||
|
|
||||||
|
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check sync state is reset.
|
||||||
|
{
|
||||||
|
state, err := imapservice.NewSyncState(syncStatePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, syncStatus.IsComplete())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
||||||
m := proton.New(
|
m := proton.New(
|
||||||
proton.WithHostURL(s.GetHostURL()),
|
proton.WithHostURL(s.GetHostURL()),
|
||||||
|
|||||||
@ -53,4 +53,5 @@ type Autostarter interface {
|
|||||||
type Updater interface {
|
type Updater interface {
|
||||||
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
||||||
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
||||||
|
RemoveOldUpdates() error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,3 +139,9 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
}
|
}
|
||||||
}, bridge.newVersionLock)
|
}, bridge.newVersionLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) RemoveOldUpdates() {
|
||||||
|
if err := bridge.updater.RemoveOldUpdates(); err != nil {
|
||||||
|
logrus.WithError(err).Error("Remove old updates fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -46,6 +46,8 @@ const (
|
|||||||
Connected
|
Connected
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
// UserID is the user's API ID.
|
// UserID is the user's API ID.
|
||||||
UserID string
|
UserID string
|
||||||
@ -66,10 +68,10 @@ type UserInfo struct {
|
|||||||
BridgePass []byte
|
BridgePass []byte
|
||||||
|
|
||||||
// UsedSpace is the amount of space used by the user.
|
// UsedSpace is the amount of space used by the user.
|
||||||
UsedSpace int
|
UsedSpace uint64
|
||||||
|
|
||||||
// MaxSpace is the total amount of space available to the user.
|
// MaxSpace is the total amount of space available to the user.
|
||||||
MaxSpace int
|
MaxSpace uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||||
@ -157,11 +159,15 @@ func (bridge *Bridge) LoginUser(
|
|||||||
func() (string, error) {
|
func() (string, error) {
|
||||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
||||||
},
|
},
|
||||||
func() error {
|
|
||||||
return client.AuthDelete(ctx)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Failure to unlock will allow retries, so we do not delete auth.
|
||||||
|
if !errors.Is(err, ErrFailedToUnlock) {
|
||||||
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
logrus.WithError(deleteErr).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
return "", fmt.Errorf("failed to login user: %w", err)
|
return "", fmt.Errorf("failed to login user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,7 +223,16 @@ func (bridge *Bridge) LoginFull(
|
|||||||
keyPass = password
|
keyPass = password
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.LoginUser(ctx, client, auth, keyPass)
|
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
|
||||||
|
if err != nil {
|
||||||
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutUser logs out the given user.
|
// LogoutUser logs out the given user.
|
||||||
@ -314,7 +329,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
|||||||
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
||||||
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
||||||
|
|
||||||
return safe.LockRet(func() error {
|
return safe.RLockRet(func() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
user, ok := bridge.users[userID]
|
user, ok := bridge.users[userID]
|
||||||
@ -374,9 +389,9 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
||||||
return "", fmt.Errorf("failed to unlock user keys: %w", err)
|
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
|
||||||
} else if userKR.CountDecryptionEntities() == 0 {
|
} else if userKR.CountDecryptionEntities() == 0 {
|
||||||
return "", fmt.Errorf("failed to unlock user keys")
|
return "", ErrFailedToUnlock
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
||||||
@ -479,7 +494,7 @@ func (bridge *Bridge) addUser(
|
|||||||
return fmt.Errorf("failed to add vault user: %w", err)
|
return fmt.Errorf("failed to add vault user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
|
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
|
||||||
if _, ok := err.(*resty.ResponseError); ok || isLogin {
|
if _, ok := err.(*resty.ResponseError); ok || isLogin {
|
||||||
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
|
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
|
||||||
|
|
||||||
@ -514,6 +529,7 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
client *proton.Client,
|
client *proton.Client,
|
||||||
apiUser proton.User,
|
apiUser proton.User,
|
||||||
vault *vault.User,
|
vault *vault.User,
|
||||||
|
isNew bool,
|
||||||
) error {
|
) error {
|
||||||
statsPath, err := bridge.locator.ProvideStatsPath()
|
statsPath, err := bridge.locator.ProvideStatsPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -541,6 +557,7 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
&bridgeEventSubscription{b: bridge},
|
&bridgeEventSubscription{b: bridge},
|
||||||
bridge.syncService,
|
bridge.syncService,
|
||||||
syncSettingsPath,
|
syncSettingsPath,
|
||||||
|
isNew,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create user: %w", err)
|
return fmt.Errorf("failed to create user: %w", err)
|
||||||
@ -577,7 +594,7 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
// As we need at least one user to send heartbeat, try to send it.
|
// As we need at least one user to send heartbeat, try to send it.
|
||||||
defer bridge.goHeartbeat()
|
bridge.heartbeat.start()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||||
"user_id": user.ID(),
|
"user_id": user.ID(),
|
||||||
"old_event_id": event.OldEventID,
|
"old_event_id": event.OldEventID,
|
||||||
|
|||||||
@ -356,6 +356,10 @@ func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func osSupportCertInstall() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted.
|
// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted.
|
||||||
// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain.
|
// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain.
|
||||||
func installCert(certPEM []byte) error {
|
func installCert(certPEM []byte) error {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
func TestCertInKeychain(t *testing.T) {
|
func TestCertInKeychain(t *testing.T) {
|
||||||
// no trust settings change is performed, so this test will not trigger an OS security prompt.
|
// no trust settings change is performed, so this test will not trigger an OS security prompt.
|
||||||
certPEM := generatePEMCertificate(t)
|
certPEM := generatePEMCertificate(t)
|
||||||
|
require.True(t, osSupportCertInstall())
|
||||||
require.False(t, isCertInKeychain(certPEM))
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
require.NoError(t, addCertToKeychain(certPEM))
|
require.NoError(t, addCertToKeychain(certPEM))
|
||||||
require.True(t, isCertInKeychain(certPEM))
|
require.True(t, isCertInKeychain(certPEM))
|
||||||
|
|||||||
@ -17,6 +17,10 @@
|
|||||||
|
|
||||||
package certs
|
package certs
|
||||||
|
|
||||||
|
func osSupportCertInstall() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func installCert([]byte) error {
|
func installCert([]byte) error {
|
||||||
return nil // Linux doesn't have a root cert store.
|
return nil // Linux doesn't have a root cert store.
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,10 @@
|
|||||||
|
|
||||||
package certs
|
package certs
|
||||||
|
|
||||||
|
func osSupportCertInstall() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func installCert([]byte) error {
|
func installCert([]byte) error {
|
||||||
return nil // NOTE(GODT-986): Install certs to root cert store?
|
return nil // NOTE(GODT-986): Install certs to root cert store?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,10 @@ func NewInstaller() *Installer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (installer *Installer) OSSupportCertInstall() bool {
|
||||||
|
return osSupportCertInstall()
|
||||||
|
}
|
||||||
|
|
||||||
func (installer *Installer) InstallCert(certPEM []byte) error {
|
func (installer *Installer) InstallCert(certPEM []byte) error {
|
||||||
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
|
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
|
||||||
|
|
||||||
@ -64,3 +68,15 @@ func (installer *Installer) UninstallCert(certPEM []byte) error {
|
|||||||
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
|
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
|
||||||
return isCertInstalled(certPEM)
|
return isCertInstalled(certPEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogCertInstallStatus reports the current status of the certificate installation in the log.
|
||||||
|
// If certificate installation is not supported on the platform, this function does nothing.
|
||||||
|
func (installer *Installer) LogCertInstallStatus(certPEM []byte) {
|
||||||
|
if installer.OSSupportCertInstall() {
|
||||||
|
if installer.IsCertInstalled(certPEM) {
|
||||||
|
installer.log.Info("The Bridge TLS certificate is installed in the OS keychain")
|
||||||
|
} else {
|
||||||
|
installer.log.Info("The Bridge TLS certificate is not installed in the OS keychain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -39,10 +39,10 @@ func (c *AppleMail) Configure(
|
|||||||
hostname string,
|
hostname string,
|
||||||
imapPort, smtpPort int,
|
imapPort, smtpPort int,
|
||||||
imapSSL, smtpSSL bool,
|
imapSSL, smtpSSL bool,
|
||||||
username, addresses string,
|
username, displayName, addresses string,
|
||||||
password []byte,
|
password []byte,
|
||||||
) error {
|
) error {
|
||||||
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, addresses, password)
|
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, displayName, addresses, password)
|
||||||
|
|
||||||
confPath, err := saveConfigTemporarily(mc)
|
confPath, err := saveConfigTemporarily(mc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -66,13 +66,13 @@ func prepareMobileConfig(
|
|||||||
hostname string,
|
hostname string,
|
||||||
imapPort, smtpPort int,
|
imapPort, smtpPort int,
|
||||||
imapSSL, smtpSSL bool,
|
imapSSL, smtpSSL bool,
|
||||||
username, addresses string,
|
username, displayName, addresses string,
|
||||||
password []byte,
|
password []byte,
|
||||||
) *mobileconfig.Config {
|
) *mobileconfig.Config {
|
||||||
return &mobileconfig.Config{
|
return &mobileconfig.Config{
|
||||||
DisplayName: username,
|
DisplayName: username,
|
||||||
EmailAddress: addresses,
|
EmailAddress: addresses,
|
||||||
AccountName: username,
|
AccountName: displayName,
|
||||||
AccountDescription: username,
|
AccountDescription: username,
|
||||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||||
IMAP: &mobileconfig.IMAP{
|
IMAP: &mobileconfig.IMAP{
|
||||||
|
|||||||
@ -95,6 +95,13 @@ func (status *ConfigurationStatus) IsPending() bool {
|
|||||||
return !status.Data.DataV1.PendingSince.IsZero()
|
return !status.Data.DataV1.PendingSince.IsZero()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (status *ConfigurationStatus) isPendingSinceMin() int {
|
||||||
|
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (status *ConfigurationStatus) IsFromFailure() bool {
|
func (status *ConfigurationStatus) IsFromFailure() bool {
|
||||||
status.DataLock.RLock()
|
status.DataLock.RLock()
|
||||||
defer status.DataLock.RUnlock()
|
defer status.DataLock.RUnlock()
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigAbortValues struct {
|
type ConfigAbortValues struct {
|
||||||
@ -41,17 +40,20 @@ type ConfigAbortData struct {
|
|||||||
|
|
||||||
type ConfigAbortBuilder struct{}
|
type ConfigAbortBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
|
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigAbortData{
|
return ConfigAbortData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_abort",
|
Event: "bridge_config_abort",
|
||||||
Values: ConfigSuccessValues{
|
Values: ConfigSuccessValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigAbortBuilder{}
|
var builder = configstatus.ConfigAbortBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigAbortBuilder{}
|
var builder = configstatus.ConfigAbortBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
|
|||||||
@ -33,13 +33,16 @@ type ConfigProgressData struct {
|
|||||||
|
|
||||||
type ConfigProgressBuilder struct{}
|
type ConfigProgressBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
|
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigProgressData{
|
return ConfigProgressData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_progress",
|
Event: "bridge_config_progress",
|
||||||
Values: ConfigProgressValues{
|
Values: ConfigProgressValues{
|
||||||
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
|
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
|
||||||
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
|
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigProgressBuilder{}
|
var builder = configstatus.ConfigProgressBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigProgressBuilder{}
|
var builder = configstatus.ConfigProgressBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigRecoveryValues struct {
|
type ConfigRecoveryValues struct {
|
||||||
@ -43,19 +42,22 @@ type ConfigRecoveryData struct {
|
|||||||
|
|
||||||
type ConfigRecoveryBuilder struct{}
|
type ConfigRecoveryBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
|
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigRecoveryData{
|
return ConfigRecoveryData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_recovery",
|
Event: "bridge_config_recovery",
|
||||||
Values: ConfigRecoveryValues{
|
Values: ConfigRecoveryValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigRecoveryDimensions{
|
Dimensions: ConfigRecoveryDimensions{
|
||||||
Autoconf: data.DataV1.Autoconf,
|
Autoconf: config.Data.DataV1.Autoconf,
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
FailureDetails: data.DataV1.FailureDetails,
|
FailureDetails: config.Data.DataV1.FailureDetails,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigSuccessValues struct {
|
type ConfigSuccessValues struct {
|
||||||
@ -42,18 +41,21 @@ type ConfigSuccessData struct {
|
|||||||
|
|
||||||
type ConfigSuccessBuilder struct{}
|
type ConfigSuccessBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
|
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigSuccessData{
|
return ConfigSuccessData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_success",
|
Event: "bridge_config_success",
|
||||||
Values: ConfigSuccessValues{
|
Values: ConfigSuccessValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
Autoconf: data.DataV1.Autoconf,
|
Autoconf: config.Data.DataV1.Autoconf,
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigSuccessBuilder{}
|
var builder = configstatus.ConfigSuccessBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigSuccessBuilder{}
|
var builder = configstatus.ConfigSuccessBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
|
|||||||
@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
|
|||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
|
|
||||||
UsedSpace int
|
UsedSpace uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (event UsedSpaceChanged) String() string {
|
func (event UsedSpaceChanged) String() string {
|
||||||
|
|||||||
@ -415,7 +415,11 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||||
QMessageBox::critical(nullptr, "Error", e.qwhat());
|
QString message = e.qwhat();
|
||||||
|
if (e.showSupportLink()) {
|
||||||
|
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
|
||||||
|
}
|
||||||
|
QMessageBox::critical(nullptr, "Error", message);
|
||||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -368,7 +368,7 @@ Item {
|
|||||||
currentIndex: hasAccount() ? 1 : 0
|
currentIndex: hasAccount() ? 1 : 0
|
||||||
NoAccountView {
|
NoAccountView {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
onLinkClicked: function() {
|
onStartSetup: {
|
||||||
root.showLogin("")
|
root.showLogin("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,8 +69,8 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
colorScheme: ProtonStyle.currentStyle
|
colorScheme: ProtonStyle.currentStyle
|
||||||
height: ProtonStyle.window_default_height
|
height: screen.height < ProtonStyle.window_default_height + 100 ? ProtonStyle.window_minimum_height : ProtonStyle.window_default_height
|
||||||
minimumHeight:ProtonStyle.window_minimum_height
|
minimumHeight: ProtonStyle.window_minimum_height
|
||||||
minimumWidth: ProtonStyle.window_minimum_width
|
minimumWidth: ProtonStyle.window_minimum_width
|
||||||
visible: true
|
visible: true
|
||||||
width: ProtonStyle.window_default_width
|
width: ProtonStyle.window_default_width
|
||||||
|
|||||||
@ -23,7 +23,7 @@ Rectangle {
|
|||||||
|
|
||||||
color: root.colorScheme.background_norm
|
color: root.colorScheme.background_norm
|
||||||
|
|
||||||
signal linkClicked()
|
signal startSetup()
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -38,8 +38,10 @@ Rectangle {
|
|||||||
wizard: setupWizard
|
wizard: setupWizard
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
showOnboarding();
|
showNoAccount();
|
||||||
link1.setCallback(root.linkClicked, "Start setup", false)
|
}
|
||||||
|
onStartSetup: {
|
||||||
|
root.startSetup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Image {
|
Image {
|
||||||
|
|||||||
@ -380,7 +380,7 @@ QtObject {
|
|||||||
}
|
}
|
||||||
property Notification diskFull: Notification {
|
property Notification diskFull: Notification {
|
||||||
brief: title
|
brief: title
|
||||||
description: qsTr("Quit Bridge and free disk space or disable the local cache (not recommended).")
|
description: qsTr("Quit Bridge and free disk space or move the local cache to another disk.")
|
||||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
title: qsTr("Your disk is almost full")
|
title: qsTr("Your disk is almost full")
|
||||||
@ -728,10 +728,12 @@ QtObject {
|
|||||||
}
|
}
|
||||||
property Notification noKeychain: Notification {
|
property Notification noKeychain: Notification {
|
||||||
brief: title
|
brief: title
|
||||||
description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
description: Backend.goos === "darwin" ?
|
||||||
|
qsTr("Bridge is not able to access your keychain. Please make sure your keychain is not locked and restart the application.") :
|
||||||
|
qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
||||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
title: qsTr("No keychain available")
|
title: Backend.goos === "darwin" ? qsTr("Cannot access keychain") : qsTr("No keychain available")
|
||||||
type: Notification.NotificationType.Danger
|
type: Notification.NotificationType.Danger
|
||||||
|
|
||||||
action: [
|
action: [
|
||||||
|
|||||||
@ -114,6 +114,9 @@ FocusScope {
|
|||||||
function getText(start, end) {
|
function getText(start, end) {
|
||||||
control.getText(start, end);
|
control.getText(start, end);
|
||||||
}
|
}
|
||||||
|
function hidePassword() {
|
||||||
|
eyeButton.checked = false;
|
||||||
|
}
|
||||||
function insert(position, text) {
|
function insert(position, text) {
|
||||||
control.insert(position, text);
|
control.insert(position, text);
|
||||||
}
|
}
|
||||||
@ -147,6 +150,9 @@ FocusScope {
|
|||||||
function selectWord() {
|
function selectWord() {
|
||||||
control.selectWord();
|
control.selectWord();
|
||||||
}
|
}
|
||||||
|
function showPassword() {
|
||||||
|
eyeButton.checked = true;
|
||||||
|
}
|
||||||
function undo() {
|
function undo() {
|
||||||
control.undo();
|
control.undo();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,14 +18,21 @@ import QtQuick.Controls
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property string addAccountTitle: qsTr("Add a Proton Mail account")
|
||||||
|
readonly property string welcomeDescription: qsTr("Bridge is the gateway between your Proton account and your email client. It runs in the background and encrypts and decrypts your messages seamlessly. ");
|
||||||
|
readonly property string welcomeTitle: qsTr("Welcome to\nProton Mail Bridge")
|
||||||
|
readonly property string welcomeImage: "/qml/icons/img-welcome.svg"
|
||||||
|
readonly property int welcomeImageHeight: 148;
|
||||||
|
readonly property int welcomeImageWidth: 265;
|
||||||
|
|
||||||
property int iconHeight
|
property int iconHeight
|
||||||
property string iconSource
|
property string iconSource
|
||||||
property int iconWidth
|
property int iconWidth
|
||||||
property var wizard
|
property var wizard
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var _colorScheme: wizard ? wizard.colorScheme : colorScheme
|
property var _colorScheme: wizard ? wizard.colorScheme : colorScheme
|
||||||
property var link1: linkLabel1
|
|
||||||
property var link2: linkLabel2
|
signal startSetup()
|
||||||
|
|
||||||
function showAppleMailAutoconfigCertificateInstall() {
|
function showAppleMailAutoconfigCertificateInstall() {
|
||||||
showAppleMailAutoconfigCommon();
|
showAppleMailAutoconfigCommon();
|
||||||
@ -65,26 +72,27 @@ Item {
|
|||||||
function showLoginMailboxPassword() {
|
function showLoginMailboxPassword() {
|
||||||
showOnboarding();
|
showOnboarding();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showNoAccount() {
|
||||||
|
titleLabel.text = welcomeTitle;
|
||||||
|
descriptionLabel.text = welcomeDescription;
|
||||||
|
linkLabel1.setCallback(startSetup, "Start setup", false);
|
||||||
|
linkLabel2.clear();
|
||||||
|
root.iconSource = welcomeImage;
|
||||||
|
root.iconHeight = welcomeImageHeight;
|
||||||
|
root.iconWidth = welcomeImageWidth;
|
||||||
|
}
|
||||||
|
|
||||||
function showOnboarding() {
|
function showOnboarding() {
|
||||||
titleLabel.text = (Backend.users.count === 0) ? qsTr("Welcome to\nProton Mail Bridge") : qsTr("Add a Proton Mail account");
|
titleLabel.text = (Backend.users.count === 0) ? welcomeTitle : addAccountTitle;
|
||||||
descriptionLabel.text = qsTr("Bridge is the gateway between your Proton account and your email client. It runs in the background and encrypts and decrypts your messages seamlessly. ");
|
descriptionLabel.text = welcomeDescription
|
||||||
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
|
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
|
||||||
linkLabel2.clear();
|
linkLabel2.clear();
|
||||||
root.iconSource = "/qml/icons/img-welcome.svg";
|
root.iconSource = welcomeImage;
|
||||||
root.iconHeight = 148;
|
root.iconHeight = welcomeImageHeight;
|
||||||
root.iconWidth = 265;
|
root.iconWidth = welcomeImageWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onLogin2FARequested() {
|
|
||||||
showLogin2FA();
|
|
||||||
}
|
|
||||||
function onLogin2PasswordRequested() {
|
|
||||||
showLoginMailboxPassword();
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Backend
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|||||||
@ -44,6 +44,8 @@ FocusScope {
|
|||||||
} else {
|
} else {
|
||||||
passwordTextField.forceActiveFocus();
|
passwordTextField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
passwordTextField.hidePassword();
|
||||||
|
secondPasswordTextField.hidePassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
|
|||||||
@ -186,6 +186,17 @@ Item {
|
|||||||
|
|
||||||
target: clientConfigAppleMail
|
target: clientConfigAppleMail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onLogin2FARequested() {
|
||||||
|
leftContent.showLogin2FA();
|
||||||
|
}
|
||||||
|
function onLogin2PasswordRequested() {
|
||||||
|
leftContent.showLoginMailboxPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Image {
|
Image {
|
||||||
id: mailLogoWithWordmark
|
id: mailLogoWithWordmark
|
||||||
|
|||||||
@ -26,14 +26,16 @@ namespace bridgepp {
|
|||||||
/// \param[in] what A description of the exception.
|
/// \param[in] what A description of the exception.
|
||||||
/// \param[in] details The optional details for the exception.
|
/// \param[in] details The optional details for the exception.
|
||||||
/// \param[in] function The name of the calling function.
|
/// \param[in] function The name of the calling function.
|
||||||
|
/// \param[in] showSupportLink Should a link to the support web form be included in GUI message.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment) noexcept
|
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment, bool showSupportLink) noexcept
|
||||||
: std::exception()
|
: std::exception()
|
||||||
, qwhat_(std::move(qwhat))
|
, qwhat_(std::move(qwhat))
|
||||||
, what_(qwhat_.toLocal8Bit())
|
, what_(qwhat_.toLocal8Bit())
|
||||||
, details_(std::move(details))
|
, details_(std::move(details))
|
||||||
, function_(std::move(function))
|
, function_(std::move(function))
|
||||||
, attachment_(std::move(attachment)) {
|
, attachment_(std::move(attachment))
|
||||||
|
, showSupportLink_(showSupportLink) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -46,7 +48,8 @@ Exception::Exception(Exception const &ref) noexcept
|
|||||||
, what_(ref.what_)
|
, what_(ref.what_)
|
||||||
, details_(ref.details_)
|
, details_(ref.details_)
|
||||||
, function_(ref.function_)
|
, function_(ref.function_)
|
||||||
, attachment_(ref.attachment_) {
|
, attachment_(ref.attachment_)
|
||||||
|
, showSupportLink_(ref.showSupportLink_) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +62,8 @@ Exception::Exception(Exception &&ref) noexcept
|
|||||||
, what_(ref.what_)
|
, what_(ref.what_)
|
||||||
, details_(ref.details_)
|
, details_(ref.details_)
|
||||||
, function_(ref.function_)
|
, function_(ref.function_)
|
||||||
, attachment_(ref.attachment_) {
|
, attachment_(ref.attachment_)
|
||||||
|
, showSupportLink_(ref.showSupportLink_) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -118,4 +122,12 @@ QString Exception::detailedWhat() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return true iff A link to the support page should shown in the GUI message box.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool Exception::showSupportLink() const {
|
||||||
|
return showSupportLink_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace bridgepp
|
} // namespace bridgepp
|
||||||
|
|||||||
@ -33,7 +33,7 @@ namespace bridgepp {
|
|||||||
class Exception : public std::exception {
|
class Exception : public std::exception {
|
||||||
public: // member functions
|
public: // member functions
|
||||||
explicit Exception(QString qwhat = QString(), QString details = QString(), QString function = QString(),
|
explicit Exception(QString qwhat = QString(), QString details = QString(), QString function = QString(),
|
||||||
QByteArray attachment = QByteArray()) noexcept; ///< Constructor
|
QByteArray attachment = QByteArray(), bool showSupportLink = false) noexcept; ///< Constructor
|
||||||
Exception(Exception const &ref) noexcept; ///< copy constructor
|
Exception(Exception const &ref) noexcept; ///< copy constructor
|
||||||
Exception(Exception &&ref) noexcept; ///< copy constructor
|
Exception(Exception &&ref) noexcept; ///< copy constructor
|
||||||
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
|
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
|
||||||
@ -45,6 +45,7 @@ public: // member functions
|
|||||||
QString function() const noexcept; ///< Return the function that threw the exception.
|
QString function() const noexcept; ///< Return the function that threw the exception.
|
||||||
QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
|
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).
|
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).
|
||||||
|
bool showSupportLink() const; ///< Return the value for the 'Show support link' option.
|
||||||
|
|
||||||
public: // static data members
|
public: // static data members
|
||||||
static qsizetype const attachmentMaxLength {25 * 1024}; ///< The maximum length text attachment sent in Sentry reports, in bytes.
|
static qsizetype const attachmentMaxLength {25 * 1024}; ///< The maximum length text attachment sent in Sentry reports, in bytes.
|
||||||
@ -55,6 +56,7 @@ private: // data members
|
|||||||
QString const details_; ///< The optional details for the exception.
|
QString const details_; ///< The optional details for the exception.
|
||||||
QString const function_; ///< The name of the function that created the exception.
|
QString const function_; ///< The name of the function that created the exception.
|
||||||
QByteArray const attachment_; ///< The attachment to add to the exception.
|
QByteArray const attachment_; ///< The attachment to add to the exception.
|
||||||
|
bool const showSupportLink_; ///< Should the GUI feedback include a link to support.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -72,8 +72,8 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const & sessionID, Q
|
|||||||
bool found = false;
|
bool found = false;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (serverProcess && serverProcess->getStatus().ended) {
|
if (serverProcess && serverProcess->getStatus().ended) {
|
||||||
throw Exception("Bridge application exited before providing a gRPC service configuration file.", QString(), __FUNCTION__,
|
throw Exception("Bridge failed to start.", "Bridge application exited before providing a gRPC service configuration file", __FUNCTION__,
|
||||||
tailOfLatestBridgeLog(sessionID));
|
tailOfLatestBridgeLog(sessionID), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
|
|||||||
@ -309,6 +309,8 @@ void User::setIsSyncing(bool syncing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSyncing_ = syncing;
|
isSyncing_ = syncing;
|
||||||
|
syncProgress_ = 0;
|
||||||
|
|
||||||
emit isSyncingChanged(syncing);
|
emit isSyncingChanged(syncing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -193,7 +193,7 @@ func NewUserBadEvent(userID string, errorMessage string) *StreamEvent {
|
|||||||
return userEvent(&UserEvent{Event: &UserEvent_UserBadEvent{UserBadEvent: &UserBadEvent{UserID: userID, ErrorMessage: errorMessage}}})
|
return userEvent(&UserEvent{Event: &UserEvent_UserBadEvent{UserBadEvent: &UserBadEvent{UserID: userID, ErrorMessage: errorMessage}}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUsedBytesChangedEvent(userID string, usedBytes int) *StreamEvent {
|
func NewUsedBytesChangedEvent(userID string, usedBytes uint64) *StreamEvent {
|
||||||
return userEvent(&UserEvent{Event: &UserEvent_UsedBytesChangedEvent{UsedBytesChangedEvent: &UsedBytesChangedEvent{UserID: userID, UsedBytes: int64(usedBytes)}}})
|
return userEvent(&UserEvent{Event: &UserEvent_UsedBytesChangedEvent{UsedBytesChangedEvent: &UsedBytesChangedEvent{UserID: userID, UsedBytes: int64(usedBytes)}}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,8 +54,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
serverConfigFileName = "grpcServerConfig.json"
|
serverConfigFileName = "grpcServerConfig.json"
|
||||||
serverTokenMetadataKey = "server-token"
|
serverTokenMetadataKey = "server-token"
|
||||||
|
twoPasswordsMaxAttemptCount = 3 // The number of attempts allowed for the mailbox password.
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service is the RPC service struct.
|
// Service is the RPC service struct.
|
||||||
@ -82,9 +83,10 @@ type Service struct { // nolint:structcheck
|
|||||||
target updater.VersionInfo
|
target updater.VersionInfo
|
||||||
targetLock safe.RWMutex
|
targetLock safe.RWMutex
|
||||||
|
|
||||||
authClient *proton.Client
|
authClient *proton.Client
|
||||||
auth proton.Auth
|
auth proton.Auth
|
||||||
password []byte
|
password []byte
|
||||||
|
twoPasswordAttemptCount int
|
||||||
|
|
||||||
log *logrus.Entry
|
log *logrus.Entry
|
||||||
initializing sync.WaitGroup
|
initializing sync.WaitGroup
|
||||||
@ -338,6 +340,11 @@ func (s *Service) watchEvents() {
|
|||||||
case events.SyncFinished:
|
case events.SyncFinished:
|
||||||
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
|
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
|
||||||
|
|
||||||
|
case events.SyncFailed:
|
||||||
|
if errors.Is(event.Error, context.Canceled) {
|
||||||
|
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
|
||||||
|
}
|
||||||
|
|
||||||
case events.SyncProgress:
|
case events.SyncProgress:
|
||||||
_ = s.SendEvent(NewSyncProgressEvent(event.UserID, event.Progress, event.Elapsed.Milliseconds(), event.Remaining.Milliseconds()))
|
_ = s.SendEvent(NewSyncProgressEvent(event.UserID, event.Progress, event.Elapsed.Milliseconds(), event.Remaining.Milliseconds()))
|
||||||
|
|
||||||
@ -408,7 +415,12 @@ func (s *Service) loginClean() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) finishLogin() {
|
func (s *Service) finishLogin() {
|
||||||
defer s.loginClean()
|
performCleanup := true
|
||||||
|
defer func() {
|
||||||
|
if performCleanup {
|
||||||
|
s.loginClean()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
|
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
|
||||||
|
|
||||||
@ -426,10 +438,24 @@ func (s *Service) finishLogin() {
|
|||||||
eventCh, done := s.bridge.GetEvents(events.UserLoggedIn{})
|
eventCh, done := s.bridge.GetEvents(events.UserLoggedIn{})
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
userID, err := s.bridge.LoginUser(context.Background(), s.authClient, s.auth, s.password)
|
ctx := context.Background()
|
||||||
|
userID, err := s.bridge.LoginUser(ctx, s.authClient, s.auth, s.password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Errorf("Finish login failed")
|
s.log.WithError(err).Errorf("Finish login failed")
|
||||||
_ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, err.Error()))
|
s.twoPasswordAttemptCount++
|
||||||
|
errType := LoginErrorType_TWO_PASSWORDS_ABORT
|
||||||
|
if errors.Is(err, bridge.ErrFailedToUnlock) {
|
||||||
|
if s.twoPasswordAttemptCount < twoPasswordsMaxAttemptCount {
|
||||||
|
performCleanup = false
|
||||||
|
errType = LoginErrorType_TWO_PASSWORDS_ERROR
|
||||||
|
} else {
|
||||||
|
if deleteErr := s.authClient.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
s.log.WithError(deleteErr).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = s.SendEvent(NewLoginError(errType, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,10 +33,8 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/ports"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/ports"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/runtime/protoimpl"
|
"google.golang.org/protobuf/runtime/protoimpl"
|
||||||
@ -339,18 +337,17 @@ func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*empty
|
|||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
defer func() { _ = s.SendEvent(NewReportBugFinishedEvent()) }()
|
defer func() { _ = s.SendEvent(NewReportBugFinishedEvent()) }()
|
||||||
|
reportReq := bridge.ReportBugReq{
|
||||||
if err := s.bridge.ReportBug(
|
OSType: report.OsType,
|
||||||
context.Background(),
|
OSVersion: report.OsVersion,
|
||||||
report.OsType,
|
Title: report.Title,
|
||||||
report.OsVersion,
|
Description: report.Description,
|
||||||
report.Title,
|
Username: report.Address,
|
||||||
report.Description,
|
Email: report.Address,
|
||||||
report.Address,
|
EmailClient: report.EmailClient,
|
||||||
report.Address,
|
IncludeLogs: report.IncludeLogs,
|
||||||
report.EmailClient,
|
}
|
||||||
report.IncludeLogs,
|
if err := s.bridge.ReportBug(context.Background(), &reportReq); err != nil {
|
||||||
); err != nil {
|
|
||||||
s.log.WithError(err).Error("Failed to report bug")
|
s.log.WithError(err).Error("Failed to report bug")
|
||||||
_ = s.SendEvent(NewReportBugErrorEvent())
|
_ = s.SendEvent(NewReportBugErrorEvent())
|
||||||
return
|
return
|
||||||
@ -384,6 +381,7 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty,
|
|||||||
go func() {
|
go func() {
|
||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
|
s.twoPasswordAttemptCount = 0
|
||||||
password, err := base64Decode(login.Password)
|
password, err := base64Decode(login.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Error("Cannot decode password")
|
s.log.WithError(err).Error("Cannot decode password")
|
||||||
@ -712,7 +710,7 @@ func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*w
|
|||||||
func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
|
func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
|
||||||
s.log.Debug("AvailableKeychains")
|
s.log.Debug("AvailableKeychains")
|
||||||
|
|
||||||
return &AvailableKeychainsResponse{Keychains: maps.Keys(keychain.Helpers)}, nil
|
return &AvailableKeychainsResponse{Keychains: s.bridge.GetHelpersNames()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
|||||||
@ -48,4 +48,6 @@ type APIClient interface {
|
|||||||
DeleteMessage(ctx context.Context, messageIDs ...string) error
|
DeleteMessage(ctx context.Context, messageIDs ...string) error
|
||||||
MarkMessagesRead(ctx context.Context, messageIDs ...string) error
|
MarkMessagesRead(ctx context.Context, messageIDs ...string) error
|
||||||
MarkMessagesUnread(ctx context.Context, messageIDs ...string) error
|
MarkMessagesUnread(ctx context.Context, messageIDs ...string) error
|
||||||
|
MarkMessagesForwarded(ctx context.Context, messageIDs ...string) error
|
||||||
|
MarkMessagesUnForwarded(ctx context.Context, messageIDs ...string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,7 @@ type Connector struct {
|
|||||||
log *logrus.Entry
|
log *logrus.Entry
|
||||||
|
|
||||||
sharedCache *SharedCache
|
sharedCache *SharedCache
|
||||||
|
syncState *SyncState
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnector(
|
func NewConnector(
|
||||||
@ -75,6 +76,7 @@ func NewConnector(
|
|||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
telemetry Telemetry,
|
telemetry Telemetry,
|
||||||
showAllMail bool,
|
showAllMail bool,
|
||||||
|
syncState *SyncState,
|
||||||
) *Connector {
|
) *Connector {
|
||||||
userID := identityState.UserID()
|
userID := identityState.UserID()
|
||||||
|
|
||||||
@ -82,9 +84,9 @@ func NewConnector(
|
|||||||
identityState: identityState,
|
identityState: identityState,
|
||||||
addrID: addrID,
|
addrID: addrID,
|
||||||
showAllMail: b32(showAllMail),
|
showAllMail: b32(showAllMail),
|
||||||
flags: defaultFlags,
|
flags: defaultMailboxFlags(),
|
||||||
permFlags: defaultPermanentFlags,
|
permFlags: defaultMailboxPermanentFlags(),
|
||||||
attrs: defaultAttributes,
|
attrs: defaultMailboxAttributes(),
|
||||||
|
|
||||||
client: apiClient,
|
client: apiClient,
|
||||||
telemetry: telemetry,
|
telemetry: telemetry,
|
||||||
@ -106,6 +108,7 @@ func NewConnector(
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
sharedCache: NewSharedCached(),
|
sharedCache: NewSharedCached(),
|
||||||
|
syncState: syncState,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,9 +117,47 @@ func (s *Connector) StateClose() {
|
|||||||
s.updateCh.CloseAndDiscardQueued()
|
s.updateCh.CloseAndDiscardQueued()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Connector) Init(_ context.Context, cache connector.IMAPState) error {
|
func (s *Connector) Init(ctx context.Context, cache connector.IMAPState) error {
|
||||||
s.sharedCache.Set(cache)
|
s.sharedCache.Set(cache)
|
||||||
return nil
|
|
||||||
|
return cache.Write(ctx, func(ctx context.Context, write connector.IMAPStateWrite) error {
|
||||||
|
rd := s.labels.Read()
|
||||||
|
defer rd.Close()
|
||||||
|
|
||||||
|
mboxes, err := write.GetMailboxesWithoutAttrib(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to fix bug when a vault got corrupted, but the sync state did not get reset leading to
|
||||||
|
// all labels being written to the root level. If we detect this happened, reset the sync state.
|
||||||
|
{
|
||||||
|
applied, err := fixGODT3003Labels(ctx, s.log, mboxes, rd, write)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if applied {
|
||||||
|
s.log.Debug("Patched folders/labels after GODT-3003 incident, resetting sync state.")
|
||||||
|
if err := s.syncState.ClearSyncStatus(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retroactively apply the forwarded flags to existing mailboxes so that the IMAP clients can recognize
|
||||||
|
// that they can store these flags now.
|
||||||
|
if err := write.AddFlagsToAllMailboxes(ctx, imap.ForwardFlagList...); err != nil {
|
||||||
|
return fmt.Errorf("failed to add \\Forward flag to all mailboxes:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add forwarded flag as perm flags to all mailboxes.
|
||||||
|
if err := write.AddPermFlagsToAllMailboxes(ctx, imap.ForwardFlagList...); err != nil {
|
||||||
|
return fmt.Errorf("failed to add \\Forward permanent flag to all mailboxes:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Connector) Authorize(ctx context.Context, username string, password []byte) bool {
|
func (s *Connector) Authorize(ctx context.Context, username string, password []byte) bool {
|
||||||
@ -458,6 +499,14 @@ func (s *Connector) MarkMessagesFlagged(ctx context.Context, _ connector.IMAPSta
|
|||||||
return s.client.UnlabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), proton.StarredLabel)
|
return s.client.UnlabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), proton.StarredLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Connector) MarkMessagesForwarded(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, flagged bool) error {
|
||||||
|
if flagged {
|
||||||
|
return s.client.MarkMessagesForwarded(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.client.MarkMessagesUnForwarded(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Connector) GetUpdates() <-chan imap.Update {
|
func (s *Connector) GetUpdates() <-chan imap.Update {
|
||||||
return s.updateCh.GetChannel()
|
return s.updateCh.GetChannel()
|
||||||
}
|
}
|
||||||
@ -472,12 +521,6 @@ func (s *Connector) ShowAllMail(v bool) {
|
|||||||
atomic.StoreUint32(&s.showAllMail, b32(v))
|
atomic.StoreUint32(&s.showAllMail, b32(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
defaultFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted) // nolint:gochecknoglobals
|
|
||||||
defaultPermanentFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted) // nolint:gochecknoglobals
|
|
||||||
defaultAttributes = imap.NewFlagSet() // nolint:gochecknoglobals
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
folderPrefix = "Folders"
|
folderPrefix = "Folders"
|
||||||
labelPrefix = "Labels"
|
labelPrefix = "Labels"
|
||||||
@ -745,3 +788,56 @@ func (s *Connector) createDraft(ctx context.Context, literal []byte, addrKR *cry
|
|||||||
func (s *Connector) publishUpdate(_ context.Context, update imap.Update) {
|
func (s *Connector) publishUpdate(_ context.Context, update imap.Update) {
|
||||||
s.updateCh.Enqueue(update)
|
s.updateCh.Enqueue(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fixGODT3003Labels(
|
||||||
|
ctx context.Context,
|
||||||
|
log *logrus.Entry,
|
||||||
|
mboxes []imap.MailboxNoAttrib,
|
||||||
|
rd labelsRead,
|
||||||
|
write connector.IMAPStateWrite,
|
||||||
|
) (bool, error) {
|
||||||
|
var applied bool
|
||||||
|
for _, mbox := range mboxes {
|
||||||
|
lbl, ok := rd.GetLabel(string(mbox.ID))
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if lbl.Type == proton.LabelTypeFolder {
|
||||||
|
if mbox.Name[0] != folderPrefix {
|
||||||
|
log.WithField("labelID", mbox.ID.ShortID()).Debug("Found folder without prefix, patching")
|
||||||
|
if err := write.PatchMailboxHierarchyWithoutTransforms(ctx, mbox.ID, xslices.Insert(mbox.Name, 0, folderPrefix)); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to update mailbox name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
applied = true
|
||||||
|
}
|
||||||
|
} else if lbl.Type == proton.LabelTypeLabel {
|
||||||
|
if mbox.Name[0] != labelPrefix {
|
||||||
|
log.WithField("labelID", mbox.ID.ShortID()).Debug("Found label without prefix, patching")
|
||||||
|
if err := write.PatchMailboxHierarchyWithoutTransforms(ctx, mbox.ID, xslices.Insert(mbox.Name, 0, labelPrefix)); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to update mailbox name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
applied = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applied, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultMailboxFlags() imap.FlagSet {
|
||||||
|
f := imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted)
|
||||||
|
f.AddToSelf(imap.ForwardFlagList...)
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultMailboxPermanentFlags() imap.FlagSet {
|
||||||
|
return defaultMailboxFlags()
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultMailboxAttributes() imap.FlagSet {
|
||||||
|
return imap.NewFlagSet()
|
||||||
|
}
|
||||||
|
|||||||
205
internal/services/imapservice/connector_test.go
Normal file
205
internal/services/imapservice/connector_test.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
// 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 imapservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/mocks"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFixGODT3003Labels(t *testing.T) {
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
log := logrus.WithField("test", "test")
|
||||||
|
|
||||||
|
sharedLabels := newRWLabels()
|
||||||
|
wr := sharedLabels.Write()
|
||||||
|
wr.SetLabel("foo", proton.Label{
|
||||||
|
ID: "foo",
|
||||||
|
ParentID: "bar",
|
||||||
|
Name: "Foo",
|
||||||
|
Path: []string{"bar", "Foo"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeFolder,
|
||||||
|
})
|
||||||
|
|
||||||
|
wr.SetLabel("0", proton.Label{
|
||||||
|
ID: "0",
|
||||||
|
ParentID: "",
|
||||||
|
Name: "Inbox",
|
||||||
|
Path: []string{"Inbox"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeSystem,
|
||||||
|
})
|
||||||
|
|
||||||
|
wr.SetLabel("bar", proton.Label{
|
||||||
|
ID: "bar",
|
||||||
|
ParentID: "",
|
||||||
|
Name: "boo",
|
||||||
|
Path: []string{"bar"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeFolder,
|
||||||
|
})
|
||||||
|
|
||||||
|
wr.SetLabel("my_label", proton.Label{
|
||||||
|
ID: "my_label",
|
||||||
|
ParentID: "",
|
||||||
|
Name: "MyLabel",
|
||||||
|
Path: []string{"MyLabel"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
})
|
||||||
|
|
||||||
|
wr.SetLabel("my_label2", proton.Label{
|
||||||
|
ID: "my_label2",
|
||||||
|
ParentID: "",
|
||||||
|
Name: "MyLabel2",
|
||||||
|
Path: []string{labelPrefix, "MyLabel2"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
})
|
||||||
|
wr.Close()
|
||||||
|
|
||||||
|
mboxs := []imap.MailboxNoAttrib{
|
||||||
|
{
|
||||||
|
ID: "0",
|
||||||
|
Name: []string{"Inbox"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "bar",
|
||||||
|
Name: []string{"bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Name: []string{"bar", "Foo"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "my_label",
|
||||||
|
Name: []string{"MyLabel"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "my_label2",
|
||||||
|
Name: []string{labelPrefix, "MyLabel2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := sharedLabels.Read()
|
||||||
|
defer rd.Close()
|
||||||
|
|
||||||
|
imapState := mocks.NewMockIMAPStateWrite(mockCtrl)
|
||||||
|
|
||||||
|
imapState.EXPECT().PatchMailboxHierarchyWithoutTransforms(gomock.Any(), gomock.Eq(imap.MailboxID("bar")), gomock.Eq([]string{folderPrefix, "bar"}))
|
||||||
|
imapState.EXPECT().PatchMailboxHierarchyWithoutTransforms(gomock.Any(), gomock.Eq(imap.MailboxID("foo")), gomock.Eq([]string{folderPrefix, "bar", "Foo"}))
|
||||||
|
imapState.EXPECT().PatchMailboxHierarchyWithoutTransforms(gomock.Any(), gomock.Eq(imap.MailboxID("my_label")), gomock.Eq([]string{labelPrefix, "MyLabel"}))
|
||||||
|
|
||||||
|
applied, err := fixGODT3003Labels(context.Background(), log, mboxs, rd, imapState)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, applied)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixGODT3003Labels_Noop(t *testing.T) {
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
log := logrus.WithField("test", "test")
|
||||||
|
|
||||||
|
sharedLabels := newRWLabels()
|
||||||
|
wr := sharedLabels.Write()
|
||||||
|
wr.SetLabel("foo", proton.Label{
|
||||||
|
ID: "foo",
|
||||||
|
ParentID: "bar",
|
||||||
|
Name: "Foo",
|
||||||
|
Path: []string{folderPrefix, "bar", "Foo"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeFolder,
|
||||||
|
})
|
||||||
|
|
||||||
|
wr.SetLabel("0", proton.Label{
|
||||||
|
ID: "0",
|
||||||
|
ParentID: "",
|
||||||
|
Name: "Inbox",
|
||||||
|
Path: []string{"Inbox"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeSystem,
|
||||||
|
})
|
||||||
|
|
||||||
|
wr.SetLabel("bar", proton.Label{
|
||||||
|
ID: "bar",
|
||||||
|
ParentID: "",
|
||||||
|
Name: "bar",
|
||||||
|
Path: []string{folderPrefix, "bar"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeFolder,
|
||||||
|
})
|
||||||
|
|
||||||
|
wr.SetLabel("my_label", proton.Label{
|
||||||
|
ID: "my_label",
|
||||||
|
ParentID: "",
|
||||||
|
Name: "MyLabel",
|
||||||
|
Path: []string{labelPrefix, "MyLabel"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
})
|
||||||
|
|
||||||
|
wr.SetLabel("my_label2", proton.Label{
|
||||||
|
ID: "my_label2",
|
||||||
|
ParentID: "",
|
||||||
|
Name: "MyLabel2",
|
||||||
|
Path: []string{labelPrefix, "MyLabel2"},
|
||||||
|
Color: "",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
})
|
||||||
|
wr.Close()
|
||||||
|
|
||||||
|
mboxs := []imap.MailboxNoAttrib{
|
||||||
|
{
|
||||||
|
ID: "0",
|
||||||
|
Name: []string{"Inbox"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "bar",
|
||||||
|
Name: []string{folderPrefix, "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Name: []string{folderPrefix, "bar", "Foo"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "my_label",
|
||||||
|
Name: []string{labelPrefix, "MyLabel"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "my_label2",
|
||||||
|
Name: []string{labelPrefix, "MyLabel2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := sharedLabels.Read()
|
||||||
|
defer rd.Close()
|
||||||
|
|
||||||
|
imapState := mocks.NewMockIMAPStateWrite(mockCtrl)
|
||||||
|
applied, err := fixGODT3003Labels(context.Background(), log, mboxs, rd, imapState)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, applied)
|
||||||
|
}
|
||||||
@ -68,6 +68,10 @@ func BuildFlagSetFromMessageMetadata(message proton.MessageMetadata) imap.FlagSe
|
|||||||
flags.AddToSelf(imap.FlagAnswered)
|
flags.AddToSelf(imap.FlagAnswered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if message.IsForwarded {
|
||||||
|
flags.AddToSelf(imap.ForwardFlagList...)
|
||||||
|
}
|
||||||
|
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,8 +32,8 @@ func newSystemMailboxCreatedUpdate(labelID imap.MailboxID, labelName string) *im
|
|||||||
}
|
}
|
||||||
|
|
||||||
attrs := imap.NewFlagSet(imap.AttrNoInferiors)
|
attrs := imap.NewFlagSet(imap.AttrNoInferiors)
|
||||||
permanentFlags := defaultPermanentFlags
|
permanentFlags := defaultMailboxPermanentFlags()
|
||||||
flags := defaultFlags
|
flags := defaultMailboxFlags()
|
||||||
|
|
||||||
switch labelID {
|
switch labelID {
|
||||||
case proton.TrashLabel:
|
case proton.TrashLabel:
|
||||||
@ -86,8 +86,8 @@ func newPlaceHolderMailboxCreatedUpdate(labelName string) *imap.MailboxCreated {
|
|||||||
return imap.NewMailboxCreated(imap.Mailbox{
|
return imap.NewMailboxCreated(imap.Mailbox{
|
||||||
ID: imap.MailboxID(labelName),
|
ID: imap.MailboxID(labelName),
|
||||||
Name: []string{labelName},
|
Name: []string{labelName},
|
||||||
Flags: defaultFlags,
|
Flags: defaultMailboxFlags(),
|
||||||
PermanentFlags: defaultPermanentFlags,
|
PermanentFlags: defaultMailboxPermanentFlags(),
|
||||||
Attributes: imap.NewFlagSet(imap.AttrNoSelect),
|
Attributes: imap.NewFlagSet(imap.AttrNoSelect),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -96,8 +96,8 @@ func newMailboxCreatedUpdate(labelID imap.MailboxID, labelName []string) *imap.M
|
|||||||
return imap.NewMailboxCreated(imap.Mailbox{
|
return imap.NewMailboxCreated(imap.Mailbox{
|
||||||
ID: labelID,
|
ID: labelID,
|
||||||
Name: labelName,
|
Name: labelName,
|
||||||
Flags: defaultFlags,
|
Flags: defaultMailboxFlags(),
|
||||||
PermanentFlags: defaultPermanentFlags,
|
PermanentFlags: defaultMailboxPermanentFlags(),
|
||||||
Attributes: imap.NewFlagSet(),
|
Attributes: imap.NewFlagSet(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
176
internal/services/imapservice/mocks/mocks.go
Normal file
176
internal/services/imapservice/mocks/mocks.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/ProtonMail/gluon/connector (interfaces: IMAPStateWrite)
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
imap "github.com/ProtonMail/gluon/imap"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockIMAPStateWrite is a mock of IMAPStateWrite interface.
|
||||||
|
type MockIMAPStateWrite struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockIMAPStateWriteMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockIMAPStateWriteMockRecorder is the mock recorder for MockIMAPStateWrite.
|
||||||
|
type MockIMAPStateWriteMockRecorder struct {
|
||||||
|
mock *MockIMAPStateWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockIMAPStateWrite creates a new mock instance.
|
||||||
|
func NewMockIMAPStateWrite(ctrl *gomock.Controller) *MockIMAPStateWrite {
|
||||||
|
mock := &MockIMAPStateWrite{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockIMAPStateWriteMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockIMAPStateWrite) EXPECT() *MockIMAPStateWriteMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlagsToAllMailboxes mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) AddFlagsToAllMailboxes(arg0 context.Context, arg1 ...string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
varargs := []interface{}{arg0}
|
||||||
|
for _, a := range arg1 {
|
||||||
|
varargs = append(varargs, a)
|
||||||
|
}
|
||||||
|
ret := m.ctrl.Call(m, "AddFlagsToAllMailboxes", varargs...)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlagsToAllMailboxes indicates an expected call of AddFlagsToAllMailboxes.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) AddFlagsToAllMailboxes(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
varargs := append([]interface{}{arg0}, arg1...)
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFlagsToAllMailboxes", reflect.TypeOf((*MockIMAPStateWrite)(nil).AddFlagsToAllMailboxes), varargs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPermFlagsToAllMailboxes mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) AddPermFlagsToAllMailboxes(arg0 context.Context, arg1 ...string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
varargs := []interface{}{arg0}
|
||||||
|
for _, a := range arg1 {
|
||||||
|
varargs = append(varargs, a)
|
||||||
|
}
|
||||||
|
ret := m.ctrl.Call(m, "AddPermFlagsToAllMailboxes", varargs...)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPermFlagsToAllMailboxes indicates an expected call of AddPermFlagsToAllMailboxes.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) AddPermFlagsToAllMailboxes(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
varargs := append([]interface{}{arg0}, arg1...)
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPermFlagsToAllMailboxes", reflect.TypeOf((*MockIMAPStateWrite)(nil).AddPermFlagsToAllMailboxes), varargs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMailbox mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) CreateMailbox(arg0 context.Context, arg1 imap.Mailbox) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "CreateMailbox", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMailbox indicates an expected call of CreateMailbox.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) CreateMailbox(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMailbox", reflect.TypeOf((*MockIMAPStateWrite)(nil).CreateMailbox), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMailboxCount mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) GetMailboxCount(arg0 context.Context) (int, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetMailboxCount", arg0)
|
||||||
|
ret0, _ := ret[0].(int)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMailboxCount indicates an expected call of GetMailboxCount.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) GetMailboxCount(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailboxCount", reflect.TypeOf((*MockIMAPStateWrite)(nil).GetMailboxCount), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMailboxesWithoutAttrib mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) GetMailboxesWithoutAttrib(arg0 context.Context) ([]imap.MailboxNoAttrib, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetMailboxesWithoutAttrib", arg0)
|
||||||
|
ret0, _ := ret[0].([]imap.MailboxNoAttrib)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMailboxesWithoutAttrib indicates an expected call of GetMailboxesWithoutAttrib.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) GetMailboxesWithoutAttrib(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailboxesWithoutAttrib", reflect.TypeOf((*MockIMAPStateWrite)(nil).GetMailboxesWithoutAttrib), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSettings mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) GetSettings(arg0 context.Context) (string, bool, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetSettings", arg0)
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
ret1, _ := ret[1].(bool)
|
||||||
|
ret2, _ := ret[2].(error)
|
||||||
|
return ret0, ret1, ret2
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSettings indicates an expected call of GetSettings.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) GetSettings(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSettings", reflect.TypeOf((*MockIMAPStateWrite)(nil).GetSettings), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchMailboxHierarchyWithoutTransforms mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) PatchMailboxHierarchyWithoutTransforms(arg0 context.Context, arg1 imap.MailboxID, arg2 []string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "PatchMailboxHierarchyWithoutTransforms", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchMailboxHierarchyWithoutTransforms indicates an expected call of PatchMailboxHierarchyWithoutTransforms.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) PatchMailboxHierarchyWithoutTransforms(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchMailboxHierarchyWithoutTransforms", reflect.TypeOf((*MockIMAPStateWrite)(nil).PatchMailboxHierarchyWithoutTransforms), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSettings mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) StoreSettings(arg0 context.Context, arg1 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "StoreSettings", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSettings indicates an expected call of StoreSettings.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) StoreSettings(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreSettings", reflect.TypeOf((*MockIMAPStateWrite)(nil).StoreSettings), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMessageFlags mocks base method.
|
||||||
|
func (m *MockIMAPStateWrite) UpdateMessageFlags(arg0 context.Context, arg1 imap.MessageID, arg2 imap.FlagSet) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdateMessageFlags", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMessageFlags indicates an expected call of UpdateMessageFlags.
|
||||||
|
func (mr *MockIMAPStateWriteMockRecorder) UpdateMessageFlags(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMessageFlags", reflect.TypeOf((*MockIMAPStateWrite)(nil).UpdateMessageFlags), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
@ -94,7 +95,7 @@ type Service struct {
|
|||||||
|
|
||||||
syncConfigPath string
|
syncConfigPath string
|
||||||
lastHandledEventID string
|
lastHandledEventID string
|
||||||
isSyncing bool
|
isSyncing atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -151,14 +152,14 @@ func NewService(
|
|||||||
connectors: make(map[string]*Connector),
|
connectors: make(map[string]*Connector),
|
||||||
maxSyncMemory: maxSyncMemory,
|
maxSyncMemory: maxSyncMemory,
|
||||||
|
|
||||||
eventWatcher: subscription.Add(events.IMAPServerCreated{}),
|
eventWatcher: subscription.Add(events.IMAPServerCreated{}, events.ConnStatusUp{}, events.ConnStatusDown{}),
|
||||||
eventSubscription: subscription,
|
eventSubscription: subscription,
|
||||||
showAllMail: showAllMail,
|
showAllMail: showAllMail,
|
||||||
|
|
||||||
syncUpdateApplier: syncUpdateApplier,
|
syncUpdateApplier: syncUpdateApplier,
|
||||||
syncMessageBuilder: syncMessageBuilder,
|
syncMessageBuilder: syncMessageBuilder,
|
||||||
syncReporter: syncReporter,
|
syncReporter: syncReporter,
|
||||||
syncConfigPath: getSyncConfigPath(syncConfigDir, identityState.User.ID),
|
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,18 +218,6 @@ func (s *Service) Resync(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CancelSync(ctx context.Context) error {
|
|
||||||
_, err := s.cpc.Send(ctx, &cancelSyncReq{})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ResumeSync(ctx context.Context) error {
|
|
||||||
_, err := s.cpc.Send(ctx, &resumeSyncReq{})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) OnBadEvent(ctx context.Context) error {
|
func (s *Service) OnBadEvent(ctx context.Context) error {
|
||||||
_, err := s.cpc.Send(ctx, &onBadEventReq{})
|
_, err := s.cpc.Send(ctx, &onBadEventReq{})
|
||||||
|
|
||||||
@ -341,6 +330,7 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
}
|
}
|
||||||
switch r := req.Value().(type) {
|
switch r := req.Value().(type) {
|
||||||
case *setAddressModeReq:
|
case *setAddressModeReq:
|
||||||
|
s.log.Debug("Set Address Mode Request")
|
||||||
err := s.setAddressMode(ctx, r.mode)
|
err := s.setAddressMode(ctx, r.mode)
|
||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
|
|
||||||
@ -350,38 +340,33 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
s.log.Info("Resync reply sent, handling as refresh event")
|
s.log.Info("Resync reply sent, handling as refresh event")
|
||||||
|
|
||||||
case *cancelSyncReq:
|
|
||||||
s.log.Info("Cancelling sync")
|
|
||||||
s.syncHandler.Cancel()
|
|
||||||
req.Reply(ctx, nil, nil)
|
|
||||||
|
|
||||||
case *resumeSyncReq:
|
|
||||||
s.log.Info("Resuming sync")
|
|
||||||
// Cancel previous run, if any, just in case.
|
|
||||||
s.cancelSync()
|
|
||||||
s.startSyncing()
|
|
||||||
req.Reply(ctx, nil, nil)
|
|
||||||
case *getLabelsReq:
|
case *getLabelsReq:
|
||||||
|
s.log.Debug("Get labels Request")
|
||||||
labels := s.labels.GetLabelMap()
|
labels := s.labels.GetLabelMap()
|
||||||
req.Reply(ctx, labels, nil)
|
req.Reply(ctx, labels, nil)
|
||||||
|
|
||||||
case *onBadEventReq:
|
case *onBadEventReq:
|
||||||
|
s.log.Debug("Bad Event Request")
|
||||||
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *onBadEventResyncReq:
|
case *onBadEventResyncReq:
|
||||||
|
s.log.Debug("Bad Event Resync Request")
|
||||||
err := s.addConnectorsToServer(ctx, s.connectors)
|
err := s.addConnectorsToServer(ctx, s.connectors)
|
||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *onLogoutReq:
|
case *onLogoutReq:
|
||||||
|
s.log.Debug("Logout Request")
|
||||||
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *showAllMailReq:
|
case *showAllMailReq:
|
||||||
|
s.log.Debug("Show all mail request")
|
||||||
req.Reply(ctx, nil, nil)
|
req.Reply(ctx, nil, nil)
|
||||||
s.setShowAllMail(r.v)
|
s.setShowAllMail(r.v)
|
||||||
|
|
||||||
case *getSyncFailedMessagesReq:
|
case *getSyncFailedMessagesReq:
|
||||||
|
s.log.Debug("Get sync failed messages Request")
|
||||||
status, err := s.syncStateProvider.GetSyncStatus(ctx)
|
status, err := s.syncStateProvider.GetSyncStatus(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.Reply(ctx, nil, fmt.Errorf("failed to get sync status: %w", err))
|
req.Reply(ctx, nil, fmt.Errorf("failed to get sync status: %w", err))
|
||||||
@ -405,23 +390,33 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start a goroutine to wait on event reset as it is possible that the sync received message
|
||||||
|
// was processed during an event publish. This in turn will block the imap service, since the
|
||||||
|
// event service is unable to reply to the request until the events have been processed.
|
||||||
s.log.Info("Sync complete, starting API event stream")
|
s.log.Info("Sync complete, starting API event stream")
|
||||||
if err := s.eventProvider.RewindEventID(ctx, s.lastHandledEventID); err != nil {
|
go func() {
|
||||||
if errors.Is(err, context.Canceled) {
|
// If context cancelled do not do anything
|
||||||
continue
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.WithError(err).Error("Failed to rewind event service")
|
if err := s.eventProvider.RewindEventID(ctx, s.lastHandledEventID); err != nil {
|
||||||
s.eventPublisher.PublishEvent(ctx, events.UserBadEvent{
|
if errors.Is(err, context.Canceled) {
|
||||||
UserID: s.identityState.UserID(),
|
return
|
||||||
OldEventID: "",
|
}
|
||||||
NewEventID: "",
|
|
||||||
EventInfo: "",
|
|
||||||
Error: fmt.Errorf("failed to rewind event loop: %w", err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
s.isSyncing = false
|
s.log.WithError(err).Error("Failed to rewind event service")
|
||||||
|
s.eventPublisher.PublishEvent(ctx, events.UserBadEvent{
|
||||||
|
UserID: s.identityState.UserID(),
|
||||||
|
OldEventID: "",
|
||||||
|
NewEventID: "",
|
||||||
|
EventInfo: "",
|
||||||
|
Error: fmt.Errorf("failed to rewind event loop: %w", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
s.isSyncing.Store(false)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
case request, ok := <-s.syncUpdateApplier.requestCh:
|
case request, ok := <-s.syncUpdateApplier.requestCh:
|
||||||
@ -443,7 +438,7 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
e.Consume(func(event proton.Event) error {
|
e.Consume(func(event proton.Event) error {
|
||||||
if s.isSyncing {
|
if s.isSyncing.Load() {
|
||||||
if err := syncEventHandler.OnEvent(ctx, event); err != nil {
|
if err := syncEventHandler.OnEvent(ctx, event); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -470,10 +465,21 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := e.(events.IMAPServerCreated); ok {
|
switch e.(type) {
|
||||||
|
case events.IMAPServerCreated:
|
||||||
|
s.log.Debug("On IMAPServerCreated")
|
||||||
if err := s.addConnectorsToServer(ctx, s.connectors); err != nil {
|
if err := s.addConnectorsToServer(ctx, s.connectors); err != nil {
|
||||||
s.log.WithError(err).Error("Failed to add connector to server after created")
|
s.log.WithError(err).Error("Failed to add connector to server after created")
|
||||||
}
|
}
|
||||||
|
case events.ConnStatusUp:
|
||||||
|
s.log.Info("Connection Restored Resuming Sync (if any)")
|
||||||
|
// Cancel previous run, if any, just in case.
|
||||||
|
s.cancelSync()
|
||||||
|
s.startSyncing()
|
||||||
|
|
||||||
|
case events.ConnStatusDown:
|
||||||
|
s.log.Info("Connection Lost cancelling sync")
|
||||||
|
s.cancelSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,6 +504,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
|
|||||||
s.panicHandler,
|
s.panicHandler,
|
||||||
s.telemetry,
|
s.telemetry,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
|
s.syncStateProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
return connectors, nil
|
return connectors, nil
|
||||||
@ -514,6 +521,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
|
|||||||
s.panicHandler,
|
s.panicHandler,
|
||||||
s.telemetry,
|
s.telemetry,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
|
s.syncStateProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -613,21 +621,17 @@ func (s *Service) setShowAllMail(v bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) startSyncing() {
|
func (s *Service) startSyncing() {
|
||||||
s.isSyncing = true
|
s.isSyncing.Store(true)
|
||||||
s.syncHandler.Execute(s.syncReporter, s.labels.GetLabelMap(), s.syncUpdateApplier, s.syncMessageBuilder, syncservice.DefaultRetryCoolDown)
|
s.syncHandler.Execute(s.syncReporter, s.labels.GetLabelMap(), s.syncUpdateApplier, s.syncMessageBuilder, syncservice.DefaultRetryCoolDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) cancelSync() {
|
func (s *Service) cancelSync() {
|
||||||
s.syncHandler.CancelAndWait()
|
s.syncHandler.CancelAndWait()
|
||||||
s.isSyncing = false
|
s.isSyncing.Store(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
type resyncReq struct{}
|
type resyncReq struct{}
|
||||||
|
|
||||||
type cancelSyncReq struct{}
|
|
||||||
|
|
||||||
type resumeSyncReq struct{}
|
|
||||||
|
|
||||||
type getLabelsReq struct{}
|
type getLabelsReq struct{}
|
||||||
|
|
||||||
type onBadEventReq struct{}
|
type onBadEventReq struct{}
|
||||||
@ -644,6 +648,6 @@ type setAddressModeReq struct {
|
|||||||
|
|
||||||
type getSyncFailedMessagesReq struct{}
|
type getSyncFailedMessagesReq struct{}
|
||||||
|
|
||||||
func getSyncConfigPath(path string, userID string) string {
|
func GetSyncConfigPath(path string, userID string) string {
|
||||||
return filepath.Join(path, fmt.Sprintf("sync-%v", userID))
|
return filepath.Join(path, fmt.Sprintf("sync-%v", userID))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,6 +128,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
|
|||||||
s.panicHandler,
|
s.panicHandler,
|
||||||
s.telemetry,
|
s.telemetry,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
|
s.syncStateProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := s.serverManager.AddIMAPUser(ctx, connector, connector.addrID, s.gluonIDProvider, s.syncStateProvider); err != nil {
|
if err := s.serverManager.AddIMAPUser(ctx, connector, connector.addrID, s.gluonIDProvider, s.syncStateProvider); err != nil {
|
||||||
|
|||||||
@ -220,7 +220,7 @@ func (s *SyncState) loadUnsafe() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteSyncState(configDir, userID string) error {
|
func DeleteSyncState(configDir, userID string) error {
|
||||||
path := getSyncConfigPath(configDir, userID)
|
path := GetSyncConfigPath(configDir, userID)
|
||||||
|
|
||||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
@ -234,7 +234,7 @@ func MigrateVaultSettings(
|
|||||||
hasLabels, hasMessages bool,
|
hasLabels, hasMessages bool,
|
||||||
failedMessageIDs []string,
|
failedMessageIDs []string,
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
filePath := getSyncConfigPath(configDir, userID)
|
filePath := GetSyncConfigPath(configDir, userID)
|
||||||
|
|
||||||
_, err := os.ReadFile(filePath) //nolint:gosec
|
_, err := os.ReadFile(filePath) //nolint:gosec
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import (
|
|||||||
|
|
||||||
func TestMigrateSyncSettings_AlreadyExists(t *testing.T) {
|
func TestMigrateSyncSettings_AlreadyExists(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := getSyncConfigPath(tmpDir, "test")
|
testFile := GetSyncConfigPath(tmpDir, "test")
|
||||||
|
|
||||||
expected, err := generateTestState(testFile)
|
expected, err := generateTestState(testFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -53,7 +53,7 @@ func TestMigrateSyncSettings_DoesNotExist(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, migrated)
|
require.True(t, migrated)
|
||||||
|
|
||||||
state, err := NewSyncState(getSyncConfigPath(tmpDir, "test"))
|
state, err := NewSyncState(GetSyncConfigPath(tmpDir, "test"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
status, err := state.GetSyncStatus(context.Background())
|
status, err := state.GetSyncStatus(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@ -119,6 +119,10 @@ func (s *SyncUpdateApplier) SyncSystemLabelsOnly(ctx context.Context, labels map
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if label.Type != proton.LabelTypeSystem {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for _, c := range connectors {
|
for _, c := range connectors {
|
||||||
update := newSystemMailboxCreatedUpdate(imap.MailboxID(label.ID), label.Name)
|
update := newSystemMailboxCreatedUpdate(imap.MailboxID(label.ID), label.Name)
|
||||||
updates = append(updates, update)
|
updates = append(updates, update)
|
||||||
|
|||||||
@ -390,6 +390,11 @@ func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
|
|||||||
} else {
|
} else {
|
||||||
log.Info("Creating new IMAP user")
|
log.Info("Creating new IMAP user")
|
||||||
|
|
||||||
|
// GODT-3003: Ensure previous IMAP sync state is cleared if we run into code path after vault corruption.
|
||||||
|
if err := syncStateProvider.ClearSyncStatus(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to reset sync status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
gluonID, err := sm.imapServer.AddUser(ctx, connector, idProvider.GluonKey())
|
gluonID, err := sm.imapServer.AddUser(ctx, connector, idProvider.GluonKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
|||||||
@ -21,16 +21,21 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Accounts struct {
|
type Accounts struct {
|
||||||
accountsLock sync.RWMutex
|
accountsLock sync.RWMutex
|
||||||
accounts map[string]*Service
|
accounts map[string]*smtpAccountState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxFailedCommands = 3
|
||||||
|
const defaultErrTimeout = 20 * time.Second
|
||||||
|
const successiveErrInterval = time.Second
|
||||||
|
|
||||||
func NewAccounts() *Accounts {
|
func NewAccounts() *Accounts {
|
||||||
return &Accounts{
|
return &Accounts{
|
||||||
accounts: make(map[string]*Service),
|
accounts: make(map[string]*smtpAccountState),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +43,10 @@ func (s *Accounts) AddAccount(account *Service) {
|
|||||||
s.accountsLock.Lock()
|
s.accountsLock.Lock()
|
||||||
defer s.accountsLock.Unlock()
|
defer s.accountsLock.Unlock()
|
||||||
|
|
||||||
s.accounts[account.UserID()] = account
|
s.accounts[account.UserID()] = &smtpAccountState{
|
||||||
|
service: account,
|
||||||
|
errTimeout: defaultErrTimeout,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Accounts) RemoveAccount(account *Service) {
|
func (s *Accounts) RemoveAccount(account *Service) {
|
||||||
@ -52,18 +60,18 @@ func (s *Accounts) CheckAuth(user string, password []byte) (string, string, erro
|
|||||||
s.accountsLock.RLock()
|
s.accountsLock.RLock()
|
||||||
defer s.accountsLock.RUnlock()
|
defer s.accountsLock.RUnlock()
|
||||||
|
|
||||||
for id, service := range s.accounts {
|
for id, account := range s.accounts {
|
||||||
addrID, err := service.checkAuth(context.Background(), user, password)
|
addrID, err := account.service.checkAuth(context.Background(), user, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
service.telemetry.ReportSMTPAuthSuccess(context.Background())
|
account.service.telemetry.ReportSMTPAuthSuccess(context.Background())
|
||||||
return id, addrID, nil
|
return id, addrID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, service := range s.accounts {
|
for _, service := range s.accounts {
|
||||||
service.telemetry.ReportSMTPAuthFailed(user)
|
service.service.telemetry.ReportSMTPAuthFailed(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", "", ErrNoSuchUser
|
return "", "", ErrNoSuchUser
|
||||||
@ -77,10 +85,57 @@ func (s *Accounts) SendMail(ctx context.Context, userID, addrID, from string, to
|
|||||||
s.accountsLock.RLock()
|
s.accountsLock.RLock()
|
||||||
defer s.accountsLock.RUnlock()
|
defer s.accountsLock.RUnlock()
|
||||||
|
|
||||||
service, ok := s.accounts[userID]
|
requestTime := time.Now()
|
||||||
|
|
||||||
|
account, ok := s.accounts[userID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrNoSuchUser
|
return ErrNoSuchUser
|
||||||
}
|
}
|
||||||
|
|
||||||
return service.SendMail(ctx, addrID, from, to, r)
|
if err := account.canMakeRequest(requestTime); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := account.service.SendMail(ctx, addrID, from, to, r)
|
||||||
|
account.handleSMTPErr(requestTime, err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type smtpAccountState struct {
|
||||||
|
service *Service
|
||||||
|
errTimeout time.Duration
|
||||||
|
|
||||||
|
errLock sync.Mutex
|
||||||
|
errCounter int
|
||||||
|
lastRequest time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *smtpAccountState) canMakeRequest(requestTime time.Time) error {
|
||||||
|
s.errLock.Lock()
|
||||||
|
defer s.errLock.Unlock()
|
||||||
|
|
||||||
|
if s.errCounter >= maxFailedCommands {
|
||||||
|
if requestTime.Sub(s.lastRequest) >= s.errTimeout {
|
||||||
|
s.errCounter = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrTooManyErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *smtpAccountState) handleSMTPErr(requestTime time.Time, err error) {
|
||||||
|
s.errLock.Lock()
|
||||||
|
defer s.errLock.Unlock()
|
||||||
|
|
||||||
|
if err == nil || requestTime.Sub(s.lastRequest) > successiveErrInterval {
|
||||||
|
s.errCounter = 0
|
||||||
|
} else {
|
||||||
|
s.errCounter++
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastRequest = requestTime
|
||||||
}
|
}
|
||||||
|
|||||||
46
internal/services/smtp/accounts_test.go
Normal file
46
internal/services/smtp/accounts_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccountTimeout(t *testing.T) {
|
||||||
|
account := smtpAccountState{errTimeout: 5 * time.Second}
|
||||||
|
err := errors.New("fail")
|
||||||
|
|
||||||
|
for i := 0; i <= maxFailedCommands; i++ {
|
||||||
|
requestTime := time.Now()
|
||||||
|
assert.Nil(t, account.canMakeRequest(requestTime))
|
||||||
|
account.handleSMTPErr(requestTime, err)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
requestTime := time.Now()
|
||||||
|
assert.ErrorIs(t, account.canMakeRequest(requestTime), ErrTooManyErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Eventually(t, func() bool {
|
||||||
|
requestTime := time.Now()
|
||||||
|
return account.canMakeRequest(requestTime) == nil
|
||||||
|
}, 10*time.Second, time.Second)
|
||||||
|
}
|
||||||
@ -17,8 +17,24 @@
|
|||||||
|
|
||||||
package smtp
|
package smtp
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
var ErrInvalidRecipient = errors.New("invalid recipient")
|
var ErrInvalidRecipient = errors.New("invalid recipient")
|
||||||
var ErrInvalidReturnPath = errors.New("invalid return path")
|
var ErrInvalidReturnPath = errors.New("invalid return path")
|
||||||
var ErrNoSuchUser = errors.New("no such user")
|
var ErrNoSuchUser = errors.New("no such user")
|
||||||
|
var ErrTooManyErrors = errors.New("too many failed requests, please try again later")
|
||||||
|
|
||||||
|
type ErrCanNotSendOnAddress struct {
|
||||||
|
address string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrCanNotSendOnAddress(address string) *ErrCanNotSendOnAddress {
|
||||||
|
return &ErrCanNotSendOnAddress{address: address}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrCanNotSendOnAddress) Error() string {
|
||||||
|
return fmt.Sprintf("can't send on address: %v", e.address)
|
||||||
|
}
|
||||||
|
|||||||
@ -95,6 +95,16 @@ func (s *Service) smtpSendMail(ctx context.Context, authID string, from string,
|
|||||||
// If the message contains a sender, use it instead of the one from the return path.
|
// If the message contains a sender, use it instead of the one from the return path.
|
||||||
if sender, ok := getMessageSender(parser); ok {
|
if sender, ok := getMessageSender(parser); ok {
|
||||||
from = sender
|
from = sender
|
||||||
|
fromAddr, err = s.identityState.GetAddr(from)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Errorf("Failed to get identity for from address %v", sender)
|
||||||
|
return ErrInvalidReturnPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fromAddr.Send {
|
||||||
|
s.log.Errorf("Can't send emails on address: %v", fromAddr.Email)
|
||||||
|
return &ErrCanNotSendOnAddress{address: fromAddr.Email}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the user's mail settings.
|
// Load the user's mail settings.
|
||||||
@ -181,7 +191,7 @@ func (s *Service) sendWithKey(
|
|||||||
if message.InReplyTo != "" {
|
if message.InReplyTo != "" {
|
||||||
references = append(references, message.InReplyTo)
|
references = append(references, message.InReplyTo)
|
||||||
}
|
}
|
||||||
parentID, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
|
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := s.reporter.ReportMessageWithContext("Failed to get parent ID", reporter.Context{
|
if err := s.reporter.ReportMessageWithContext("Failed to get parent ID", reporter.Context{
|
||||||
"error": err,
|
"error": err,
|
||||||
@ -207,7 +217,7 @@ func (s *Service) sendWithKey(
|
|||||||
return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType)
|
return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType)
|
||||||
}
|
}
|
||||||
|
|
||||||
draft, err := s.createDraft(ctx, addrKR, emails, from, to, parentID, message.InReplyTo, proton.DraftTemplate{
|
draft, err := s.createDraft(ctx, addrKR, emails, from, to, parentID, message.InReplyTo, message.XForward, proton.DraftTemplate{
|
||||||
Subject: message.Subject,
|
Subject: message.Subject,
|
||||||
Body: decBody,
|
Body: decBody,
|
||||||
MIMEType: message.MIMEType,
|
MIMEType: message.MIMEType,
|
||||||
@ -220,7 +230,7 @@ func (s *Service) sendWithKey(
|
|||||||
ExternalID: message.ExternalID,
|
ExternalID: message.ExternalID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return proton.Message{}, fmt.Errorf("failed to create attachments: %w", err)
|
return proton.Message{}, fmt.Errorf("failed to create draft: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
attKeys, err := s.createAttachments(ctx, s.client, addrKR, draft.ID, message.Attachments)
|
attKeys, err := s.createAttachments(ctx, s.client, addrKR, draft.ID, message.Attachments)
|
||||||
@ -243,6 +253,13 @@ func (s *Service) sendWithKey(
|
|||||||
return proton.Message{}, fmt.Errorf("failed to send draft: %w", err)
|
return proton.Message{}, fmt.Errorf("failed to send draft: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only delete the drafts, if any, after message was successfully sent.
|
||||||
|
if len(draftsToDelete) != 0 {
|
||||||
|
if err := s.client.DeleteMessage(ctx, draftsToDelete...); err != nil {
|
||||||
|
s.log.WithField("ids", draftsToDelete).WithError(err).Errorf("Failed to delete requested messages from Drafts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,11 +269,12 @@ func getParentID(
|
|||||||
authAddrID string,
|
authAddrID string,
|
||||||
addrMode usertypes.AddressMode,
|
addrMode usertypes.AddressMode,
|
||||||
references []string,
|
references []string,
|
||||||
) (string, error) {
|
) (string, []string, error) {
|
||||||
var (
|
var (
|
||||||
parentID string
|
parentID string
|
||||||
internal []string
|
internal []string
|
||||||
external []string
|
external []string
|
||||||
|
draftsToDelete []string
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collect all the internal and external references of the message.
|
// Collect all the internal and external references of the message.
|
||||||
@ -281,14 +299,18 @@ func getParentID(
|
|||||||
AddressID: addrID,
|
AddressID: addrID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get message metadata: %w", err)
|
return "", nil, fmt.Errorf("failed to get message metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, metadata := range metadata {
|
for _, metadata := range metadata {
|
||||||
if !metadata.IsDraft() {
|
if !metadata.IsDraft() {
|
||||||
parentID = metadata.ID
|
parentID = metadata.ID
|
||||||
} else if err := client.DeleteMessage(ctx, metadata.ID); err != nil {
|
} else {
|
||||||
return "", fmt.Errorf("failed to delete message: %w", err)
|
// We need to record this ID to delete later after the message has been sent successfully. This is
|
||||||
|
// required for Apple Mail to correctly delete a draft when a draft is created in Apple Mail, then
|
||||||
|
// edited on the web, edited again in Apple Mail and then Send from Apple Mail. If we don't
|
||||||
|
// delete the referenced draft it is never deleted from the drafts folder.
|
||||||
|
draftsToDelete = append(draftsToDelete, metadata.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,13 +331,17 @@ func getParentID(
|
|||||||
AddressID: addrID,
|
AddressID: addrID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get message metadata: %w", err)
|
return "", nil, fmt.Errorf("failed to get message metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch len(metadata) {
|
switch len(metadata) {
|
||||||
case 1:
|
case 1:
|
||||||
// found exactly one parent
|
// found exactly one parent
|
||||||
parentID = metadata[0].ID
|
// We can only reference messages that have been sent or received. If this message is a draft
|
||||||
|
// it needs to be ignored.
|
||||||
|
if metadata[0].Flags.Has(proton.MessageFlagSent) || metadata[0].Flags.Has(proton.MessageFlagReceived) {
|
||||||
|
parentID = metadata[0].ID
|
||||||
|
}
|
||||||
case 0:
|
case 0:
|
||||||
// found no parents
|
// found no parents
|
||||||
default:
|
default:
|
||||||
@ -330,7 +356,7 @@ func getParentID(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parentID, nil
|
return parentID, draftsToDelete, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) createDraft(
|
func (s *Service) createDraft(
|
||||||
@ -341,6 +367,7 @@ func (s *Service) createDraft(
|
|||||||
to []string,
|
to []string,
|
||||||
parentID string,
|
parentID string,
|
||||||
replyToID string,
|
replyToID string,
|
||||||
|
xForwardID string,
|
||||||
template proton.DraftTemplate,
|
template proton.DraftTemplate,
|
||||||
) (proton.Message, error) {
|
) (proton.Message, error) {
|
||||||
// Check sender: set the sender if it's missing.
|
// Check sender: set the sender if it's missing.
|
||||||
@ -376,7 +403,12 @@ func (s *Service) createDraft(
|
|||||||
var action proton.CreateDraftAction
|
var action proton.CreateDraftAction
|
||||||
|
|
||||||
if len(replyToID) > 0 {
|
if len(replyToID) > 0 {
|
||||||
action = proton.ReplyAction
|
// Thunderbird fills both ReplyTo and adds an X-Forwarded-Message-Id header when forwarding.
|
||||||
|
if replyToID == xForwardID {
|
||||||
|
action = proton.ForwardAction
|
||||||
|
} else {
|
||||||
|
action = proton.ReplyAction
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
action = proton.ForwardAction
|
action = proton.ForwardAction
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,11 +18,14 @@
|
|||||||
package smtp
|
package smtp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
@ -43,6 +46,9 @@ func createSendReq(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if recs := recipients.scheme(proton.InternalScheme, proton.ClearScheme, proton.PGPInlineScheme); len(recs) > 0 {
|
if recs := recipients.scheme(proton.InternalScheme, proton.ClearScheme, proton.PGPInlineScheme); len(recs) > 0 {
|
||||||
|
if recs := recipients.scheme(proton.PGPInlineScheme); len(recs) > 0 {
|
||||||
|
logrus.WithFields(logrus.Fields{"service": "smtp", "settings": "recipient"}).Warn("PGPInline scheme used. Planed to be deprecated.")
|
||||||
|
}
|
||||||
if recs := recs.content(rfc822.TextHTML); len(recs) > 0 {
|
if recs := recs.content(rfc822.TextHTML); len(recs) > 0 {
|
||||||
if err := req.AddTextPackage(kr, string(richBody), rfc822.TextHTML, recs, attKeys); err != nil {
|
if err := req.AddTextPackage(kr, string(richBody), rfc822.TextHTML, recs, attKeys); err != nil {
|
||||||
return proton.SendDraftReq{}, err
|
return proton.SendDraftReq{}, err
|
||||||
@ -54,6 +60,10 @@ func createSendReq(
|
|||||||
return proton.SendDraftReq{}, err
|
return proton.SendDraftReq{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if recs := recs.content(rfc822.MultipartMixed); len(recs) > 0 {
|
||||||
|
return proton.SendDraftReq{}, fmt.Errorf("invalid MIME type for MIME package: %s", rfc822.MultipartMixed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
|
|||||||
1271
internal/services/smtp/smtp_packages_test.go
Normal file
1271
internal/services/smtp/smtp_packages_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -33,13 +34,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type contactSettings struct {
|
type contactSettings struct {
|
||||||
Email string
|
Email string
|
||||||
Keys []string
|
Keys []string
|
||||||
Scheme string
|
Scheme string
|
||||||
Sign bool
|
Sign bool
|
||||||
SignIsSet bool
|
SignIsSet bool
|
||||||
Encrypt bool
|
Encrypt bool
|
||||||
MIMEType rfc822.MIMEType
|
EncryptUntrusted bool
|
||||||
|
MIMEType rfc822.MIMEType
|
||||||
}
|
}
|
||||||
|
|
||||||
// newContactSettings converts the API settings into our local settings.
|
// newContactSettings converts the API settings into our local settings.
|
||||||
@ -60,6 +62,12 @@ func newContactSettings(settings proton.ContactSettings) *contactSettings {
|
|||||||
metadata.Encrypt = *settings.Encrypt
|
metadata.Encrypt = *settings.Encrypt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.EncryptUntrusted != nil {
|
||||||
|
metadata.EncryptUntrusted = *settings.EncryptUntrusted
|
||||||
|
} else {
|
||||||
|
metadata.EncryptUntrusted = true
|
||||||
|
}
|
||||||
|
|
||||||
if settings.Scheme != nil {
|
if settings.Scheme != nil {
|
||||||
switch *settings.Scheme { // nolint:exhaustive
|
switch *settings.Scheme { // nolint:exhaustive
|
||||||
case proton.PGPMIMEScheme:
|
case proton.PGPMIMEScheme:
|
||||||
@ -425,9 +433,12 @@ func (b *sendPrefsBuilder) setExternalPGPSettingsWithWKDKeys(
|
|||||||
return errors.New("an API key is necessary but wasn't provided")
|
return errors.New("an API key is necessary but wasn't provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
// We always encrypt and sign external mail if WKD keys are present.
|
b.withEncrypt(vCardData.EncryptUntrusted)
|
||||||
b.withEncrypt(true)
|
if vCardData.EncryptUntrusted {
|
||||||
b.withSign(true)
|
b.withSign(true)
|
||||||
|
} else if vCardData.SignIsSet {
|
||||||
|
b.withSign(vCardData.Sign)
|
||||||
|
}
|
||||||
|
|
||||||
// If the contact has a specific Scheme preference, we set it (otherwise we
|
// If the contact has a specific Scheme preference, we set it (otherwise we
|
||||||
// leave it unset to allow it to be filled in with the default value later).
|
// leave it unset to allow it to be filled in with the default value later).
|
||||||
@ -547,6 +558,7 @@ func (b *sendPrefsBuilder) setEncryptionPreferences(mailSettings proton.MailSett
|
|||||||
// Otherwise keep the defined value.
|
// Otherwise keep the defined value.
|
||||||
switch mailSettings.PGPScheme {
|
switch mailSettings.PGPScheme {
|
||||||
case proton.PGPInlineScheme:
|
case proton.PGPInlineScheme:
|
||||||
|
logrus.WithFields(logrus.Fields{"service": "smtp", "settings": "account"}).Warn("PGPInline scheme used. Planed to be deprecated.")
|
||||||
b.withSchemeDefault(pgpInline)
|
b.withSchemeDefault(pgpInline)
|
||||||
case proton.PGPMIMEScheme:
|
case proton.PGPMIMEScheme:
|
||||||
b.withSchemeDefault(pgpMIME)
|
b.withSchemeDefault(pgpMIME)
|
||||||
|
|||||||
@ -110,7 +110,22 @@ func TestPreferencesBuilder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "wkd-external",
|
name: "wkd-external",
|
||||||
|
|
||||||
contactMeta: &contactSettings{},
|
contactMeta: &contactSettings{EncryptUntrusted: true},
|
||||||
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
|
isInternal: false,
|
||||||
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
||||||
|
|
||||||
|
wantEncrypt: true,
|
||||||
|
wantSign: proton.DetachedSignature,
|
||||||
|
wantScheme: proton.PGPMIMEScheme,
|
||||||
|
wantMIMEType: "multipart/mixed",
|
||||||
|
wantPublicKey: testPublicKey,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "wkd-external",
|
||||||
|
|
||||||
|
contactMeta: &contactSettings{EncryptUntrusted: true},
|
||||||
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
isInternal: false,
|
isInternal: false,
|
||||||
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
||||||
@ -125,7 +140,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "wkd-external with contact-specific email format",
|
name: "wkd-external with contact-specific email format",
|
||||||
|
|
||||||
contactMeta: &contactSettings{MIMEType: "text/plain"},
|
contactMeta: &contactSettings{MIMEType: "text/plain", EncryptUntrusted: true},
|
||||||
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
isInternal: false,
|
isInternal: false,
|
||||||
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
||||||
@ -140,7 +155,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "wkd-external with global pgp-inline scheme",
|
name: "wkd-external with global pgp-inline scheme",
|
||||||
|
|
||||||
contactMeta: &contactSettings{},
|
contactMeta: &contactSettings{EncryptUntrusted: true},
|
||||||
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
isInternal: false,
|
isInternal: false,
|
||||||
mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"},
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"},
|
||||||
@ -155,7 +170,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "wkd-external with contact-specific pgp-inline scheme overriding global pgp-mime setting",
|
name: "wkd-external with contact-specific pgp-inline scheme overriding global pgp-mime setting",
|
||||||
|
|
||||||
contactMeta: &contactSettings{Scheme: pgpInline},
|
contactMeta: &contactSettings{Scheme: pgpInline, EncryptUntrusted: true},
|
||||||
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
isInternal: false,
|
isInternal: false,
|
||||||
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
||||||
@ -170,7 +185,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "wkd-external with contact-specific pgp-mime scheme overriding global pgp-inline setting",
|
name: "wkd-external with contact-specific pgp-mime scheme overriding global pgp-inline setting",
|
||||||
|
|
||||||
contactMeta: &contactSettings{Scheme: pgpMIME},
|
contactMeta: &contactSettings{Scheme: pgpMIME, EncryptUntrusted: true},
|
||||||
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
isInternal: false,
|
isInternal: false,
|
||||||
mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"},
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"},
|
||||||
@ -185,7 +200,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "wkd-external with additional pinned contact public key",
|
name: "wkd-external with additional pinned contact public key",
|
||||||
|
|
||||||
contactMeta: &contactSettings{Keys: []string{testContactKey}},
|
contactMeta: &contactSettings{Keys: []string{testContactKey}, EncryptUntrusted: true},
|
||||||
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
isInternal: false,
|
isInternal: false,
|
||||||
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
||||||
@ -201,7 +216,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
|||||||
// NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation.
|
// NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation.
|
||||||
name: "wkd-external with additional conflicting contact public key",
|
name: "wkd-external with additional conflicting contact public key",
|
||||||
|
|
||||||
contactMeta: &contactSettings{Keys: []string{testOtherContactKey}},
|
contactMeta: &contactSettings{Keys: []string{testOtherContactKey}, EncryptUntrusted: true},
|
||||||
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
isInternal: false,
|
isInternal: false,
|
||||||
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
||||||
@ -213,6 +228,51 @@ func TestPreferencesBuilder(t *testing.T) {
|
|||||||
wantPublicKey: testPublicKey,
|
wantPublicKey: testPublicKey,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "wkd-external-with-encrypt-and-sign-disabled",
|
||||||
|
|
||||||
|
contactMeta: &contactSettings{EncryptUntrusted: false},
|
||||||
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
|
isInternal: false,
|
||||||
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
||||||
|
|
||||||
|
wantEncrypt: false,
|
||||||
|
wantSign: proton.NoSignature,
|
||||||
|
wantScheme: proton.ClearScheme,
|
||||||
|
wantMIMEType: "text/html",
|
||||||
|
wantPublicKey: testPublicKey,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "wkd-external-with-encrypt-and-sign-disabled-plain-text",
|
||||||
|
|
||||||
|
contactMeta: &contactSettings{EncryptUntrusted: false},
|
||||||
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
|
isInternal: false,
|
||||||
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/plain"},
|
||||||
|
|
||||||
|
wantEncrypt: false,
|
||||||
|
wantSign: proton.NoSignature,
|
||||||
|
wantScheme: proton.ClearScheme,
|
||||||
|
wantMIMEType: "text/plain",
|
||||||
|
wantPublicKey: testPublicKey,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "wkd-external-with-encrypt-disabled-sign-enabled",
|
||||||
|
|
||||||
|
contactMeta: &contactSettings{EncryptUntrusted: false, Sign: true, SignIsSet: true},
|
||||||
|
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
|
||||||
|
isInternal: false,
|
||||||
|
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
|
||||||
|
|
||||||
|
wantEncrypt: false,
|
||||||
|
wantSign: proton.DetachedSignature,
|
||||||
|
wantScheme: proton.ClearMIMEScheme,
|
||||||
|
wantMIMEType: "multipart/mixed",
|
||||||
|
wantPublicKey: testPublicKey,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "external",
|
name: "external",
|
||||||
|
|
||||||
|
|||||||
@ -58,6 +58,10 @@ func (s Status) IsComplete() bool {
|
|||||||
return s.HasLabels && s.HasMessages
|
return s.HasLabels && s.HasMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Status) InProgress() bool {
|
||||||
|
return s.HasLabels || s.HasMessageCount
|
||||||
|
}
|
||||||
|
|
||||||
// Regulator is an abstraction for the sync service, since it regulates the number of concurrent sync activities.
|
// Regulator is an abstraction for the sync service, since it regulates the number of concurrent sync activities.
|
||||||
type Regulator interface {
|
type Regulator interface {
|
||||||
Sync(ctx context.Context, stage *Job)
|
Sync(ctx context.Context, stage *Job)
|
||||||
|
|||||||
@ -296,7 +296,7 @@ func (m *MockUserUsedSpaceEventHandler) EXPECT() *MockUserUsedSpaceEventHandlerM
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleUsedSpaceEvent mocks base method.
|
// HandleUsedSpaceEvent mocks base method.
|
||||||
func (m *MockUserUsedSpaceEventHandler) HandleUsedSpaceEvent(arg0 context.Context, arg1 int) error {
|
func (m *MockUserUsedSpaceEventHandler) HandleUsedSpaceEvent(arg0 context.Context, arg1 int64) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "HandleUsedSpaceEvent", arg0, arg1)
|
ret := m.ctrl.Call(m, "HandleUsedSpaceEvent", arg0, arg1)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
@ -67,6 +68,8 @@ type Service struct {
|
|||||||
|
|
||||||
eventPollWaiters []*EventPollWaiter
|
eventPollWaiters []*EventPollWaiter
|
||||||
eventPollWaitersLock sync.Mutex
|
eventPollWaitersLock sync.Mutex
|
||||||
|
eventSubscription events.Subscription
|
||||||
|
eventWatcher *watcher.Watcher[events.Event]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -78,6 +81,7 @@ func NewService(
|
|||||||
jitter time.Duration,
|
jitter time.Duration,
|
||||||
eventTimeout time.Duration,
|
eventTimeout time.Duration,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
|
eventSubscription events.Subscription,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
cpc: cpc.NewCPC(),
|
cpc: cpc.NewCPC(),
|
||||||
@ -88,11 +92,13 @@ func NewService(
|
|||||||
"service": "user-events",
|
"service": "user-events",
|
||||||
"user": userID,
|
"user": userID,
|
||||||
}),
|
}),
|
||||||
eventPublisher: eventPublisher,
|
eventPublisher: eventPublisher,
|
||||||
timer: proton.NewTicker(pollPeriod, jitter, panicHandler),
|
timer: proton.NewTicker(pollPeriod, jitter, panicHandler),
|
||||||
paused: 1,
|
paused: 1,
|
||||||
eventTimeout: eventTimeout,
|
eventTimeout: eventTimeout,
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
|
eventSubscription: eventSubscription,
|
||||||
|
eventWatcher: eventSubscription.Add(events.ConnStatusDown{}, events.ConnStatusUp{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,6 +230,19 @@ func (s *Service) run(ctx context.Context, lastEventID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
case e, ok := <-s.eventWatcher.GetChannel():
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.(type) {
|
||||||
|
case events.ConnStatusDown:
|
||||||
|
s.log.Info("Connection Lost, pausing")
|
||||||
|
s.Pause()
|
||||||
|
case events.ConnStatusUp:
|
||||||
|
s.log.Info("Connection Restored, resuming")
|
||||||
|
s.Resume()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply any pending subscription changes.
|
// Apply any pending subscription changes.
|
||||||
@ -295,6 +314,11 @@ func (s *Service) run(ctx context.Context, lastEventID string) {
|
|||||||
|
|
||||||
// Close should be called after the service has been cancelled to clean up any remaining pending operations.
|
// Close should be called after the service has been cancelled to clean up any remaining pending operations.
|
||||||
func (s *Service) Close() {
|
func (s *Service) Close() {
|
||||||
|
if s.eventSubscription != nil {
|
||||||
|
s.eventSubscription.Remove(s.eventWatcher)
|
||||||
|
s.eventSubscription = nil
|
||||||
|
}
|
||||||
|
|
||||||
s.pendingSubscriptionsLock.Lock()
|
s.pendingSubscriptionsLock.Lock()
|
||||||
defer s.pendingSubscriptionsLock.Unlock()
|
defer s.pendingSubscriptionsLock.Unlock()
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,7 @@ func TestServiceHandleEventError_SubscriberEventUnwrapping(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
@ -85,6 +86,7 @@ func TestServiceHandleEventError_BadEventPutsServiceOnPause(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
service.Resume()
|
service.Resume()
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
@ -118,6 +120,7 @@ func TestServiceHandleEventError_BadEventFromPublishTimeout(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
@ -148,6 +151,7 @@ func TestServiceHandleEventError_NoBadEventCheck(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
@ -173,6 +177,7 @@ func TestServiceHandleEventError_JsonUnmarshalEventProducesUncategorizedErrorEve
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -67,6 +68,7 @@ func TestServiceHandleEvent_CheckEventCategoriesHandledInOrder(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
10*time.Second,
|
10*time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
@ -84,7 +86,7 @@ func TestServiceHandleEvent_CheckEventCategoriesHandledInOrder(t *testing.T) {
|
|||||||
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{Refresh: proton.RefreshMail}))
|
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{Refresh: proton.RefreshMail}))
|
||||||
|
|
||||||
// Simulate Regular event.
|
// Simulate Regular event.
|
||||||
usedSpace := 20
|
usedSpace := int64(20)
|
||||||
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{
|
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{
|
||||||
User: new(proton.User),
|
User: new(proton.User),
|
||||||
Addresses: []proton.AddressEvent{
|
Addresses: []proton.AddressEvent{
|
||||||
@ -127,6 +129,7 @@ func TestServiceHandleEvent_CheckEventFailureCausesError(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
@ -164,6 +167,7 @@ func TestServiceHandleEvent_CheckEventFailureCausesErrorParallel(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
|
|||||||
@ -75,6 +75,7 @@ func TestService_EventIDLoadStore(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := service.Start(context.Background(), group)
|
_, err := service.Start(context.Background(), group)
|
||||||
@ -130,6 +131,7 @@ func TestService_RetryEventOnNonCatastrophicFailure(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
service.Subscribe(NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber}))
|
service.Subscribe(NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber}))
|
||||||
|
|
||||||
@ -179,6 +181,7 @@ func TestService_OnBadEventServiceIsPaused(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event publisher expectations.
|
// Event publisher expectations.
|
||||||
@ -245,6 +248,7 @@ func TestService_UnsubscribeDuringEventHandlingDoesNotCauseDeadlock(t *testing.T
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber})
|
subscription := NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber})
|
||||||
@ -304,6 +308,7 @@ func TestService_UnsubscribeBeforeHandlingEventIsNotConsideredError(t *testing.T
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewEventSubscriber("Foo")
|
subscription := NewEventSubscriber("Foo")
|
||||||
@ -363,6 +368,7 @@ func TestService_WaitOnEventPublishAfterPause(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscriber.EXPECT().HandleMessageEvents(gomock.Any(), gomock.Eq(messageEvents)).Times(1).DoAndReturn(func(_ context.Context, _ []proton.MessageEvent) error {
|
subscriber.EXPECT().HandleMessageEvents(gomock.Any(), gomock.Eq(messageEvents)).Times(1).DoAndReturn(func(_ context.Context, _ []proton.MessageEvent) error {
|
||||||
@ -435,6 +441,7 @@ func TestService_EventRewind(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := service.Start(context.Background(), group)
|
_, err := service.Start(context.Background(), group)
|
||||||
|
|||||||
@ -98,7 +98,7 @@ type UserEventHandler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserUsedSpaceEventHandler interface {
|
type UserUsedSpaceEventHandler interface {
|
||||||
HandleUsedSpaceEvent(ctx context.Context, newSpace int) error
|
HandleUsedSpaceEvent(ctx context.Context, newSpace int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSettingsHandler interface {
|
type UserSettingsHandler interface {
|
||||||
|
|||||||
@ -102,13 +102,13 @@ func (s *Service) CheckAuth(ctx context.Context, email string, password []byte)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HandleUsedSpaceEvent(ctx context.Context, newSpace int) error {
|
func (s *Service) HandleUsedSpaceEvent(ctx context.Context, newSpace int64) error {
|
||||||
s.log.Info("Handling User Space Changed event")
|
s.log.Info("Handling User Space Changed event")
|
||||||
|
|
||||||
if s.identity.OnUserSpaceChanged(newSpace) {
|
if s.identity.OnUserSpaceChanged(uint64(newSpace)) {
|
||||||
s.eventPublisher.PublishEvent(ctx, events.UsedSpaceChanged{
|
s.eventPublisher.PublishEvent(ctx, events.UsedSpaceChanged{
|
||||||
UserID: s.identity.User.ID,
|
UserID: s.identity.User.ID,
|
||||||
UsedSpace: newSpace,
|
UsedSpace: uint64(newSpace),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ func TestService_OnUserSpaceChanged(t *testing.T) {
|
|||||||
|
|
||||||
// New value, event should be published.
|
// New value, event should be published.
|
||||||
require.NoError(t, service.HandleUsedSpaceEvent(context.Background(), 1024))
|
require.NoError(t, service.HandleUsedSpaceEvent(context.Background(), 1024))
|
||||||
require.Equal(t, 1024, service.identity.User.UsedSpace)
|
require.Equal(t, uint64(1024), service.identity.User.UsedSpace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_OnRefreshEvent(t *testing.T) {
|
func TestService_OnRefreshEvent(t *testing.T) {
|
||||||
|
|||||||
@ -119,7 +119,7 @@ func (s *State) OnRefreshEvent(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) OnUserSpaceChanged(value int) bool {
|
func (s *State) OnUserSpaceChanged(value uint64) bool {
|
||||||
if s.User.UsedSpace == value {
|
if s.User.UsedSpace == value {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,20 @@ func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
|
|||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHeartbeatPeriodicInterval mocks base method.
|
||||||
|
func (m *MockHeartbeatManager) GetHeartbeatPeriodicInterval() time.Duration {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetHeartbeatPeriodicInterval")
|
||||||
|
ret0, _ := ret[0].(time.Duration)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeartbeatPeriodicInterval indicates an expected call of GetHeartbeatPeriodicInterval.
|
||||||
|
func (mr *MockHeartbeatManagerMockRecorder) GetHeartbeatPeriodicInterval() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeartbeatPeriodicInterval", reflect.TypeOf((*MockHeartbeatManager)(nil).GetHeartbeatPeriodicInterval))
|
||||||
|
}
|
||||||
|
|
||||||
// GetLastHeartbeatSent mocks base method.
|
// GetLastHeartbeatSent mocks base method.
|
||||||
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
|
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@ -42,6 +42,7 @@ type HeartbeatManager interface {
|
|||||||
SendHeartbeat(ctx context.Context, heartbeat *HeartbeatData) bool
|
SendHeartbeat(ctx context.Context, heartbeat *HeartbeatData) bool
|
||||||
GetLastHeartbeatSent() time.Time
|
GetLastHeartbeatSent() time.Time
|
||||||
SetLastHeartbeatSent(time.Time) error
|
SetLastHeartbeatSent(time.Time) error
|
||||||
|
GetHeartbeatPeriodicInterval() time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeartbeatValues struct {
|
type HeartbeatValues struct {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -46,15 +47,17 @@ type Installer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Updater struct {
|
type Updater struct {
|
||||||
|
versioner *versioner.Versioner
|
||||||
installer Installer
|
installer Installer
|
||||||
verifier *crypto.KeyRing
|
verifier *crypto.KeyRing
|
||||||
product string
|
product string
|
||||||
platform string
|
platform string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUpdater(installer Installer, verifier *crypto.KeyRing, product, platform string) *Updater {
|
func NewUpdater(ver *versioner.Versioner, verifier *crypto.KeyRing, product, platform string) *Updater {
|
||||||
return &Updater{
|
return &Updater{
|
||||||
installer: installer,
|
versioner: ver,
|
||||||
|
installer: NewInstaller(ver),
|
||||||
verifier: verifier,
|
verifier: verifier,
|
||||||
product: product,
|
product: product,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
@ -109,6 +112,10 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Updater) RemoveOldUpdates() error {
|
||||||
|
return u.versioner.RemoveOldVersions()
|
||||||
|
}
|
||||||
|
|
||||||
// getVersionFileURL returns the URL of the version file.
|
// getVersionFileURL returns the URL of the version file.
|
||||||
// For example:
|
// For example:
|
||||||
// - https://protonmail.com/download/bridge/version_linux.json
|
// - https://protonmail.com/download/bridge/version_linux.json
|
||||||
|
|||||||
@ -38,7 +38,7 @@ func (user *User) SendConfigStatusSuccess(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builder configstatus.ConfigSuccessBuilder
|
var builder configstatus.ConfigSuccessBuilder
|
||||||
success := builder.New(user.configStatus.Data)
|
success := builder.New(user.configStatus)
|
||||||
data, err := json.Marshal(success)
|
data, err := json.Marshal(success)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := user.reporter.ReportMessageWithContext("Cannot parse config_success data.", reporter.Context{
|
if err := user.reporter.ReportMessageWithContext("Cannot parse config_success data.", reporter.Context{
|
||||||
@ -69,7 +69,7 @@ func (user *User) SendConfigStatusAbort(ctx context.Context, withTelemetry bool)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var builder configstatus.ConfigAbortBuilder
|
var builder configstatus.ConfigAbortBuilder
|
||||||
abort := builder.New(user.configStatus.Data)
|
abort := builder.New(user.configStatus)
|
||||||
data, err := json.Marshal(abort)
|
data, err := json.Marshal(abort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := user.reporter.ReportMessageWithContext("Cannot parse config_abort data.", reporter.Context{
|
if err := user.reporter.ReportMessageWithContext("Cannot parse config_abort data.", reporter.Context{
|
||||||
@ -98,7 +98,7 @@ func (user *User) SendConfigStatusRecovery(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builder configstatus.ConfigRecoveryBuilder
|
var builder configstatus.ConfigRecoveryBuilder
|
||||||
success := builder.New(user.configStatus.Data)
|
success := builder.New(user.configStatus)
|
||||||
data, err := json.Marshal(success)
|
data, err := json.Marshal(success)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := user.reporter.ReportMessageWithContext("Cannot parse config_recovery data.", reporter.Context{
|
if err := user.reporter.ReportMessageWithContext("Cannot parse config_recovery data.", reporter.Context{
|
||||||
@ -125,7 +125,7 @@ func (user *User) SendConfigStatusProgress(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var builder configstatus.ConfigProgressBuilder
|
var builder configstatus.ConfigProgressBuilder
|
||||||
progress := builder.New(user.configStatus.Data)
|
progress := builder.New(user.configStatus)
|
||||||
if progress.Values.NbDay == 0 {
|
if progress.Values.NbDay == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
@ -37,6 +38,7 @@ import (
|
|||||||
imapservice "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
imapservice "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
bmessage "github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
||||||
"github.com/bradenaw/juniper/xmaps"
|
"github.com/bradenaw/juniper/xmaps"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/emersion/go-message"
|
"github.com/emersion/go-message"
|
||||||
@ -224,6 +226,55 @@ func (user *User) DebugDownloadMessages(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TryBuildDebugMessage(path string) error {
|
||||||
|
meta, err := loadDebugMetadata(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, bodyDecrypted, err := loadDebugBody(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var da []bmessage.DecryptedAttachment
|
||||||
|
if len(meta.Attachments) != 0 {
|
||||||
|
d, err := loadAttachments(path, &meta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
da = d
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedMessage := bmessage.DecryptedMessage{
|
||||||
|
Msg: proton.Message{
|
||||||
|
MessageMetadata: meta.MessageMetadata,
|
||||||
|
Header: meta.Header,
|
||||||
|
ParsedHeaders: meta.ParsedHeaders,
|
||||||
|
Body: "",
|
||||||
|
MIMEType: meta.MIMEType,
|
||||||
|
Attachments: nil,
|
||||||
|
},
|
||||||
|
Body: bytes.Buffer{},
|
||||||
|
BodyErr: nil,
|
||||||
|
Attachments: da,
|
||||||
|
}
|
||||||
|
|
||||||
|
if bodyDecrypted {
|
||||||
|
decryptedMessage.Body.Write(body)
|
||||||
|
} else {
|
||||||
|
decryptedMessage.Msg.Body = string(body)
|
||||||
|
decryptedMessage.BodyErr = fmt.Errorf("body did not decrypt")
|
||||||
|
}
|
||||||
|
|
||||||
|
var rfc822Message bytes.Buffer
|
||||||
|
if err := bmessage.BuildRFC822Into(nil, &decryptedMessage, defaultMessageJobOpts(), &rfc822Message); err != nil {
|
||||||
|
return fmt.Errorf("failed to build message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getBodyName(path string) string {
|
func getBodyName(path string) string {
|
||||||
return filepath.Join(path, "body.txt")
|
return filepath.Join(path, "body.txt")
|
||||||
}
|
}
|
||||||
@ -297,16 +348,16 @@ func decodeSimpleMessage(outPath string, kr *crypto.KeyRing, msg proton.Message)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeMetadata(outPath string, msg proton.Message) error {
|
type DebugMetadata struct {
|
||||||
type CustomMetadata struct {
|
proton.MessageMetadata
|
||||||
proton.MessageMetadata
|
Header string
|
||||||
Header string
|
ParsedHeaders proton.Headers
|
||||||
ParsedHeaders proton.Headers
|
MIMEType rfc822.MIMEType
|
||||||
MIMEType rfc822.MIMEType
|
Attachments []proton.Attachment
|
||||||
Attachments []proton.Attachment
|
}
|
||||||
}
|
|
||||||
|
|
||||||
metadata := CustomMetadata{
|
func writeMetadata(outPath string, msg proton.Message) error {
|
||||||
|
metadata := DebugMetadata{
|
||||||
MessageMetadata: msg.MessageMetadata,
|
MessageMetadata: msg.MessageMetadata,
|
||||||
Header: msg.Header,
|
Header: msg.Header,
|
||||||
ParsedHeaders: msg.ParsedHeaders,
|
ParsedHeaders: msg.ParsedHeaders,
|
||||||
@ -433,3 +484,78 @@ func writeCustomAttachmentPart(
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadDebugMetadata(dir string) (DebugMetadata, error) {
|
||||||
|
metadataPath := getMetadataPath(dir)
|
||||||
|
b, err := os.ReadFile(metadataPath) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
return DebugMetadata{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var m DebugMetadata
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &m); err != nil {
|
||||||
|
return DebugMetadata{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDebugBody(dir string) ([]byte, bool, error) {
|
||||||
|
if b, err := os.ReadFile(getBodyName(dir)); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return b, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b, err := os.ReadFile(getBodyNameFailed(dir)); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return b, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false, fmt.Errorf("body is either pgp message, which we can't handle or is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAttachments(dir string, meta *DebugMetadata) ([]bmessage.DecryptedAttachment, error) {
|
||||||
|
attDecrypted := make([]bmessage.DecryptedAttachment, 0, len(meta.Attachments))
|
||||||
|
|
||||||
|
for _, a := range meta.Attachments {
|
||||||
|
data, err := os.ReadFile(getAttachmentPathSuccess(dir, a.ID, a.Name))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, fmt.Errorf("attachment (%v,%v) must have failed to decrypt, we can't do anything since we need the user's keyring", a.ID, a.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to load attachment (%v,%v): %w", a.ID, a.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
da := bmessage.DecryptedAttachment{
|
||||||
|
Packet: nil,
|
||||||
|
Encrypted: nil,
|
||||||
|
Data: bytes.Buffer{},
|
||||||
|
Err: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
da.Data.Write(data)
|
||||||
|
|
||||||
|
attDecrypted = append(attDecrypted, da)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attDecrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultMessageJobOpts() bmessage.JobOptions {
|
||||||
|
return bmessage.JobOptions{
|
||||||
|
IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead.
|
||||||
|
SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate.
|
||||||
|
AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id.
|
||||||
|
AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id.
|
||||||
|
AddMessageDate: true, // Whether to include message time as X-Pm-Date.
|
||||||
|
AddMessageIDReference: true, // Whether to include the MessageID in References.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func migrateSyncStatusFromVault(encVault *vault.User, syncConfigDir string, user
|
|||||||
}
|
}
|
||||||
|
|
||||||
if migrated {
|
if migrated {
|
||||||
if err := encVault.ClearSyncStatus(); err != nil {
|
if err := encVault.ClearSyncStatusWithoutEventID(); err != nil {
|
||||||
return fmt.Errorf("failed to clear sync settings from vault: %w", err)
|
return fmt.Errorf("failed to clear sync settings from vault: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,6 +105,7 @@ func New(
|
|||||||
eventSubscription events.Subscription,
|
eventSubscription events.Subscription,
|
||||||
syncService syncservice.Regulator,
|
syncService syncservice.Regulator,
|
||||||
syncConfigDir string,
|
syncConfigDir string,
|
||||||
|
isNew bool,
|
||||||
) (*User, error) {
|
) (*User, error) {
|
||||||
user, err := newImpl(
|
user, err := newImpl(
|
||||||
ctx,
|
ctx,
|
||||||
@ -122,6 +123,7 @@ func New(
|
|||||||
eventSubscription,
|
eventSubscription,
|
||||||
syncService,
|
syncService,
|
||||||
syncConfigDir,
|
syncConfigDir,
|
||||||
|
isNew,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Cleanup any pending resources on error
|
// Cleanup any pending resources on error
|
||||||
@ -152,6 +154,7 @@ func newImpl(
|
|||||||
eventSubscription events.Subscription,
|
eventSubscription events.Subscription,
|
||||||
syncService syncservice.Regulator,
|
syncService syncservice.Regulator,
|
||||||
syncConfigDir string,
|
syncConfigDir string,
|
||||||
|
isNew bool,
|
||||||
) (*User, error) {
|
) (*User, error) {
|
||||||
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
||||||
|
|
||||||
@ -223,6 +226,7 @@ func newImpl(
|
|||||||
EventJitter,
|
EventJitter,
|
||||||
5*time.Minute,
|
5*time.Minute,
|
||||||
crashHandler,
|
crashHandler,
|
||||||
|
eventSubscription,
|
||||||
)
|
)
|
||||||
|
|
||||||
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode())
|
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode())
|
||||||
@ -294,6 +298,14 @@ func newImpl(
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If it's not a fresh user check the eventID and evaluate whether it is valid. If it's a new user, we don't
|
||||||
|
// need to perform this check.
|
||||||
|
if !isNew {
|
||||||
|
if err := checkIrrecoverableEventID(ctx, encVault.EventID(), apiUser.ID, syncConfigDir, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start Event Service
|
// Start Event Service
|
||||||
lastEventID, err := user.eventService.Start(ctx, user.serviceGroup)
|
lastEventID, err := user.eventService.Start(ctx, user.serviceGroup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -367,24 +379,28 @@ func (user *User) Match(query string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emails returns all the user's active email addresses.
|
// DisplayNames returns a map of the email addresses and their associated display names.
|
||||||
// It returns them in sorted order; the user's primary address is first.
|
func (user *User) DisplayNames() map[string]string {
|
||||||
func (user *User) Emails() []string {
|
addresses := user.protonAddresses()
|
||||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
if addresses == nil {
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
apiAddrs, err := user.identityService.GetAddresses(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
addresses := xslices.Filter(maps.Values(apiAddrs), func(addr proton.Address) bool {
|
result := make(map[string]string)
|
||||||
return addr.Status == proton.AddressStatusEnabled && addr.Type != proton.AddressTypeExternal
|
for _, address := range addresses {
|
||||||
})
|
result[address.Email] = address.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
slices.SortFunc(addresses, func(a, b proton.Address) bool {
|
return result
|
||||||
return a.Order < b.Order
|
}
|
||||||
})
|
|
||||||
|
// Emails returns all the user's active email addresses.
|
||||||
|
// It returns them in sorted order; the user's primary address is first.
|
||||||
|
func (user *User) Emails() []string {
|
||||||
|
addresses := user.protonAddresses()
|
||||||
|
if addresses == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return xslices.Map(addresses, func(addr proton.Address) string {
|
return xslices.Map(addresses, func(addr proton.Address) string {
|
||||||
return addr.Email
|
return addr.Email
|
||||||
@ -515,7 +531,7 @@ func (user *User) BridgePass() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UsedSpace returns the total space used by the user on the API.
|
// UsedSpace returns the total space used by the user on the API.
|
||||||
func (user *User) UsedSpace() int {
|
func (user *User) UsedSpace() uint64 {
|
||||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -528,7 +544,7 @@ func (user *User) UsedSpace() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MaxSpace returns the amount of space the user can use on the API.
|
// MaxSpace returns the amount of space the user can use on the API.
|
||||||
func (user *User) MaxSpace() int {
|
func (user *User) MaxSpace() uint64 {
|
||||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -554,27 +570,6 @@ func (user *User) CheckAuth(email string, password []byte) (string, error) {
|
|||||||
return user.identityService.CheckAuth(ctx, email, password)
|
return user.identityService.CheckAuth(ctx, email, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnStatusUp is called when the connection goes up.
|
|
||||||
func (user *User) OnStatusUp(ctx context.Context) {
|
|
||||||
user.log.Info("Connection is up")
|
|
||||||
|
|
||||||
user.eventService.Resume()
|
|
||||||
|
|
||||||
if err := user.imapService.ResumeSync(ctx); err != nil {
|
|
||||||
user.log.WithError(err).Error("Failed to resume sync")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnStatusDown is called when the connection goes down.
|
|
||||||
func (user *User) OnStatusDown(ctx context.Context) {
|
|
||||||
user.log.Info("Connection is down")
|
|
||||||
|
|
||||||
user.eventService.Pause()
|
|
||||||
if err := user.imapService.CancelSync(ctx); err != nil {
|
|
||||||
user.log.WithError(err).Error("Failed to cancel sync")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout logs the user out from the API.
|
// Logout logs the user out from the API.
|
||||||
func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
||||||
user.log.WithField("withAPI", withAPI).Info("Logging out user")
|
user.log.WithField("withAPI", withAPI).Info("Logging out user")
|
||||||
@ -702,3 +697,23 @@ func (user *User) PauseEventLoopWithWaiter() *userevents.EventPollWaiter {
|
|||||||
func (user *User) ResumeEventLoop() {
|
func (user *User) ResumeEventLoop() {
|
||||||
user.eventService.Resume()
|
user.eventService.Resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) protonAddresses() []proton.Address {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
apiAddrs, err := user.identityService.GetAddresses(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses := xslices.Filter(maps.Values(apiAddrs), func(addr proton.Address) bool {
|
||||||
|
return addr.Status == proton.AddressStatusEnabled && addr.Type != proton.AddressTypeExternal
|
||||||
|
})
|
||||||
|
|
||||||
|
slices.SortFunc(addresses, func(a, b proton.Address) bool {
|
||||||
|
return a.Order < b.Order
|
||||||
|
})
|
||||||
|
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
|||||||
68
internal/user/user_check.go
Normal file
68
internal/user/user_check.go
Normal file
@ -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/>.
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkIrrecoverableEventID(
|
||||||
|
ctx context.Context,
|
||||||
|
lastEventID,
|
||||||
|
userID,
|
||||||
|
syncConfigDir string,
|
||||||
|
publisher events.EventPublisher,
|
||||||
|
) error {
|
||||||
|
// If we detect that the event ID stored in the vault got reset, the user is not a new account and
|
||||||
|
// we have started or finished syncing: this is an irrecoverable state and we should produce a bad event.
|
||||||
|
if lastEventID != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
syncConfigPath := imapservice.GetSyncConfigPath(syncConfigDir, userID)
|
||||||
|
|
||||||
|
syncState, err := imapservice.NewSyncState(syncConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read imap sync state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncStatus, err := syncState.GetSyncStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to imap sync status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if syncStatus.IsComplete() || syncStatus.InProgress() {
|
||||||
|
publisher.PublishEvent(ctx, newEmptyEventIDBadEvent(userID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEmptyEventIDBadEvent(userID string) events.UserBadEvent {
|
||||||
|
return events.UserBadEvent{
|
||||||
|
UserID: userID,
|
||||||
|
OldEventID: "",
|
||||||
|
NewEventID: "",
|
||||||
|
EventInfo: "EventID missing from vault",
|
||||||
|
Error: fmt.Errorf("eventID in vault is empty, when it shouldn't be"),
|
||||||
|
}
|
||||||
|
}
|
||||||
104
internal/user/user_check_test.go
Normal file
104
internal/user/user_check_test.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// 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 user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckIrrecoverableEventID_EventIDIsEmptyButNoSyncStarted(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
userID := "foo"
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
publisher := mocks.NewMockEventPublisher(mockCtrl)
|
||||||
|
|
||||||
|
require.NoError(t, checkIrrecoverableEventID(context.Background(), "", userID, tmpDir, publisher))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckIrrecoverableEventID_EventIDIsNotEmptyButNoSyncStarted(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
userID := "foo"
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
publisher := mocks.NewMockEventPublisher(mockCtrl)
|
||||||
|
|
||||||
|
require.NoError(t, checkIrrecoverableEventID(context.Background(), "ffoofo", userID, tmpDir, publisher))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckIrrecoverableEventID_EventIDIsEmptyButSyncStarted(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
userID := "foo"
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
publisher := mocks.NewMockEventPublisher(mockCtrl)
|
||||||
|
|
||||||
|
publisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(newEmptyEventIDBadEvent(userID)))
|
||||||
|
|
||||||
|
require.NoError(t, genSyncState(context.Background(), userID, tmpDir, false))
|
||||||
|
require.NoError(t, checkIrrecoverableEventID(context.Background(), "", userID, tmpDir, publisher))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckIrrecoverableEventID_EventIDIsEmptyButSyncFinished(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
userID := "foo"
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
publisher := mocks.NewMockEventPublisher(mockCtrl)
|
||||||
|
|
||||||
|
publisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(newEmptyEventIDBadEvent(userID)))
|
||||||
|
|
||||||
|
require.NoError(t, genSyncState(context.Background(), userID, tmpDir, true))
|
||||||
|
require.NoError(t, checkIrrecoverableEventID(context.Background(), "", userID, tmpDir, publisher))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckIrrecoverableEventID_EventIDIsNotEmptyButSyncFinished(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
userID := "foo"
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
publisher := mocks.NewMockEventPublisher(mockCtrl)
|
||||||
|
|
||||||
|
require.NoError(t, genSyncState(context.Background(), userID, tmpDir, true))
|
||||||
|
require.NoError(t, checkIrrecoverableEventID(context.Background(), "some event", userID, tmpDir, publisher))
|
||||||
|
}
|
||||||
|
|
||||||
|
func genSyncState(ctx context.Context, userID, dir string, finished bool) error {
|
||||||
|
s, err := imapservice.NewSyncState(imapservice.GetSyncConfigPath(dir, userID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
if err := s.SetHasLabels(ctx, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.SetHasMessages(ctx, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.SetMessageCount(ctx, 10); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := s.SetHasLabels(ctx, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -58,8 +59,12 @@ func TestUser_Info(t *testing.T) {
|
|||||||
// User's name should be correct.
|
// User's name should be correct.
|
||||||
require.Equal(t, "username", user.Name())
|
require.Equal(t, "username", user.Name())
|
||||||
|
|
||||||
// User's email should be correct.
|
// User's emails should be correct and their associated display names should be correct
|
||||||
require.ElementsMatch(t, []string{"username@" + s.GetDomain(), "alias@pm.me"}, user.Emails())
|
require.ElementsMatch(t, []string{"username@" + s.GetDomain(), "alias@pm.me"}, user.Emails())
|
||||||
|
require.True(t, reflect.DeepEqual(map[string]string{
|
||||||
|
"username@" + s.GetDomain(): "username" + " (Display Name)",
|
||||||
|
"alias@pm.me": "alias@pm.me (Display Name)",
|
||||||
|
}, user.DisplayNames()))
|
||||||
|
|
||||||
// By default, user should be in combined mode.
|
// By default, user should be in combined mode.
|
||||||
require.Equal(t, vault.CombinedMode, user.GetAddressMode())
|
require.Equal(t, vault.CombinedMode, user.GetAddressMode())
|
||||||
@ -98,12 +103,14 @@ func withAPI(_ testing.TB, ctx context.Context, fn func(context.Context, *server
|
|||||||
func withAccount(tb testing.TB, s *server.Server, username, password string, aliases []string, fn func(string, []string)) { //nolint:unparam
|
func withAccount(tb testing.TB, s *server.Server, username, password string, aliases []string, fn func(string, []string)) { //nolint:unparam
|
||||||
userID, addrID, err := s.CreateUser(username, []byte(password))
|
userID, addrID, err := s.CreateUser(username, []byte(password))
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
|
require.NoError(tb, s.ChangeAddressDisplayName(userID, addrID, username+" (Display Name)"))
|
||||||
|
|
||||||
addrIDs := []string{addrID}
|
addrIDs := []string{addrID}
|
||||||
|
|
||||||
for _, email := range aliases {
|
for _, email := range aliases {
|
||||||
addrID, err := s.CreateAddress(userID, email, []byte(password))
|
addrID, err := s.CreateAddress(userID, email, []byte(password))
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
|
require.NoError(tb, s.ChangeAddressDisplayName(userID, addrID, email+" (Display Name)"))
|
||||||
|
|
||||||
addrIDs = append(addrIDs, addrID)
|
addrIDs = append(addrIDs, addrID)
|
||||||
}
|
}
|
||||||
@ -158,6 +165,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
|||||||
nullEventSubscription,
|
nullEventSubscription,
|
||||||
nil,
|
nil,
|
||||||
"",
|
"",
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
defer user.Close()
|
defer user.Close()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user