mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 20:56:51 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53f28bcbec | |||
| 89f7878910 | |||
| 01043e033e | |||
| 94b44b383a | |||
| 4b95ef4d82 | |||
| 951c7c27fb | |||
| e7423a9519 | |||
| b7ef6e1486 | |||
| 0d03f84711 | |||
| 949666724d | |||
| bbe19bf960 | |||
| bfe25e3a46 | |||
| 236c958703 | |||
| e6b312b437 | |||
| 45d2e9ea63 | |||
| 86e8a566c7 |
34
Changelog.md
34
Changelog.md
@ -3,9 +3,40 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.4
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3033: Unable to receive new mail.
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2963: Use multi error to report file removal errors.
|
||||||
|
* GODT-2956: Restore old deletion rules.
|
||||||
|
* GODT-2951: Negative WaitGroup Counter.
|
||||||
|
* GODT-2590: Fix send on closed channel.
|
||||||
|
* GODT-2949: Fix close of close channel in event service.
|
||||||
|
|
||||||
|
|
||||||
## Umshiang Bridge 3.5.0
|
## Umshiang Bridge 3.5.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
* GODT-2734: Add testing steps to modify account settings.
|
||||||
|
* GODT-2746: Integration tests for reporting a problem.
|
||||||
* GODT-2891: Allow message create & delete during sync.
|
* GODT-2891: Allow message create & delete during sync.
|
||||||
* GODT-2848: Decouple IMAP service from Event Loop.
|
* GODT-2848: Decouple IMAP service from Event Loop.
|
||||||
* Add trace profiling option.
|
* Add trace profiling option.
|
||||||
@ -19,6 +50,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2803: Bridge Database access.
|
* GODT-2803: Bridge Database access.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
* GODT-2909: Remove Timeout on event publish.
|
||||||
|
* GODT-2913: Reduce the number of configuration failure detected.
|
||||||
* GODT-2828: Increase sync progress report frequency.
|
* GODT-2828: Increase sync progress report frequency.
|
||||||
* Test: Fix TestBridge_SyncWithOnGoingEvents.
|
* Test: Fix TestBridge_SyncWithOnGoingEvents.
|
||||||
* GODT-2871: Is telemetry enabled as service.
|
* GODT-2871: Is telemetry enabled as service.
|
||||||
@ -71,6 +104,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2780: Fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup.
|
* GODT-2780: Fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup.
|
||||||
* GODT-2778: Fix login screen being disabled after an 'already logged in' error.
|
* GODT-2778: Fix login screen being disabled after an 'already logged in' error.
|
||||||
* Fix typos found by codespell.
|
* Fix typos found by codespell.
|
||||||
|
* GODT-2577: Answered flag should only be applied to replied messages.
|
||||||
|
|
||||||
|
|
||||||
## Trift Bridge 3.4.1
|
## Trift Bridge 3.4.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.5.0+git
|
BRIDGE_APP_VERSION?=3.5.4+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
|
||||||
|
|
||||||
|
|||||||
8
go.mod
8
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.20230829112217-5d5c25c504b5
|
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c
|
||||||
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.20230831064234-0e3a549b3f36
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231011062329-f3b976b7dbca
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.3-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
|
||||||
@ -52,7 +52,7 @@ require (
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
24
go.sum
24
go.sum
@ -23,24 +23,23 @@ 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.20230829112217-5d5c25c504b5 h1:C/8P5NHAKi2yCKez+OZ5rSR8SsL7k8si4pK4SE2QtV8=
|
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c h1:gUDu4pOswgbou0QczfreNiXQFrmvVlpSh8Q+vft/JvI=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c/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.20230831064234-0e3a549b3f36 h1:JVMK2w90bCWayUCXJIb3wkQ5+j2P/NbnrX3BrDoLzsc=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231011062329-f3b976b7dbca h1:nO/xuvyEgWWLo2cBAqfxCHh7Ri0ofV3PXnTOfk0QcyI=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36/go.mod h1:nS8hMGjJLgC0Iej0JMYbsI388LesEkM1Hj/jCCxQeaQ=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231011062329-f3b976b7dbca/go.mod h1:IGVXKy6NLHt4WeWiOnAFmSsXRpd6elkjDZMtr5vBLJ8=
|
||||||
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.3-proton h1:wuAxBUU9qF2wyDVJprn/2xPDx000eol5gwlKbOUYY88=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.3-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 +63,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=
|
||||||
@ -417,6 +417,7 @@ 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.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
@ -464,6 +465,7 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
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-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=
|
||||||
@ -512,6 +514,8 @@ 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=
|
||||||
@ -519,6 +523,7 @@ 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/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=
|
||||||
@ -529,6 +534,7 @@ 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 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
|
|||||||
@ -585,7 +585,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) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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()),
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
@ -62,6 +63,7 @@ type Connector struct {
|
|||||||
log *logrus.Entry
|
log *logrus.Entry
|
||||||
|
|
||||||
sharedCache *SharedCache
|
sharedCache *SharedCache
|
||||||
|
syncState *SyncState
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnector(
|
func NewConnector(
|
||||||
@ -74,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()
|
||||||
|
|
||||||
@ -105,6 +108,7 @@ func NewConnector(
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
sharedCache: NewSharedCached(),
|
sharedCache: NewSharedCached(),
|
||||||
|
syncState: syncState,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,9 +117,35 @@ 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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
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 {
|
||||||
@ -334,9 +364,70 @@ func (s *Connector) RemoveMessagesFromMailbox(ctx context.Context, _ connector.I
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mboxID == proton.TrashLabel || mboxID == proton.DraftsLabel {
|
if mboxID == proton.TrashLabel || mboxID == proton.DraftsLabel {
|
||||||
if err := s.client.DeleteMessage(ctx, msgIDs...); err != nil {
|
const ChunkSize = 150
|
||||||
|
var msgToPermaDelete []string
|
||||||
|
|
||||||
|
rdLabels := s.labels.Read()
|
||||||
|
defer rdLabels.Close()
|
||||||
|
|
||||||
|
// There's currently no limit on how many IDs we can filter on,
|
||||||
|
// but to be nice to API, let's chunk it by 150.
|
||||||
|
for _, messageIDs := range xslices.Chunk(messageIDs, ChunkSize) {
|
||||||
|
metadata, err := s.client.GetMessageMetadataPage(ctx, 0, ChunkSize, proton.MessageFilter{
|
||||||
|
ID: usertypes.MapTo[imap.MessageID, string](messageIDs),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a message is not preset in any other label other than AllMail, AllDrafts and AllSent, it can be
|
||||||
|
// permanently deleted.
|
||||||
|
for _, m := range metadata {
|
||||||
|
var remainingLabels []string
|
||||||
|
|
||||||
|
for _, id := range m.LabelIDs {
|
||||||
|
label, ok := rdLabels.GetLabel(id)
|
||||||
|
if !ok {
|
||||||
|
// Handle case where this label was newly introduced and we do not yet know about it.
|
||||||
|
logrus.WithField("labelID", id).Warnf("Unknown label found during expung from Trash, attempting to locate it")
|
||||||
|
label, err = s.client.GetLabel(ctx, id, proton.LabelTypeFolder, proton.LabelTypeSystem, proton.LabelTypeSystem)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, proton.ErrNoSuchLabel) {
|
||||||
|
logrus.WithField("labelID", id).Warn("Label does not exist, ignoring")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithField("labelID", id).Errorf("Failed to resolve label: %v", err)
|
||||||
|
return fmt.Errorf("failed to resolve label: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !WantLabel(label) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if label.Type == proton.LabelTypeSystem && (id == proton.AllDraftsLabel ||
|
||||||
|
id == proton.AllMailLabel ||
|
||||||
|
id == proton.AllSentLabel ||
|
||||||
|
id == proton.AllScheduledLabel) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingLabels = append(remainingLabels, m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(remainingLabels) == 0 {
|
||||||
|
msgToPermaDelete = append(msgToPermaDelete, m.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msgToPermaDelete) != 0 {
|
||||||
|
logrus.Debugf("Following message(s) will be perma-deleted: %v", msgToPermaDelete)
|
||||||
|
|
||||||
|
if err := s.client.DeleteMessage(ctx, msgToPermaDelete...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -683,3 +774,41 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
138
internal/services/imapservice/mocks/mocks.go
Normal file
138
internal/services/imapservice/mocks/mocks.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
@ -158,7 +159,7 @@ func NewService(
|
|||||||
syncUpdateApplier: syncUpdateApplier,
|
syncUpdateApplier: syncUpdateApplier,
|
||||||
syncMessageBuilder: syncMessageBuilder,
|
syncMessageBuilder: syncMessageBuilder,
|
||||||
syncReporter: syncReporter,
|
syncReporter: syncReporter,
|
||||||
syncConfigPath: getSyncConfigPath(syncConfigDir, identityState.User.ID),
|
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,10 +406,14 @@ 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")
|
||||||
|
go func() {
|
||||||
if err := s.eventProvider.RewindEventID(ctx, s.lastHandledEventID); err != nil {
|
if err := s.eventProvider.RewindEventID(ctx, s.lastHandledEventID); err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.WithError(err).Error("Failed to rewind event service")
|
s.log.WithError(err).Error("Failed to rewind event service")
|
||||||
@ -421,7 +426,8 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
s.isSyncing = false
|
s.isSyncing.Store(false)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
case request, ok := <-s.syncUpdateApplier.requestCh:
|
case request, ok := <-s.syncUpdateApplier.requestCh:
|
||||||
@ -443,7 +449,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
|
||||||
}
|
}
|
||||||
@ -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,13 +621,13 @@ 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{}
|
||||||
@ -644,6 +652,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)
|
||||||
|
|||||||
@ -113,13 +113,14 @@ func (j *Job) onStageCompleted(ctx context.Context, count int64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (j *Job) onJobFinished(ctx context.Context, lastMessageID string, count int64) {
|
func (j *Job) onJobFinished(ctx context.Context, lastMessageID string, count int64) {
|
||||||
defer j.wg.Done()
|
|
||||||
|
|
||||||
if err := j.state.SetLastMessageID(ctx, lastMessageID, count); err != nil {
|
if err := j.state.SetLastMessageID(ctx, lastMessageID, count); err != nil {
|
||||||
j.log.WithError(err).Error("Failed to store last synced message id")
|
j.log.WithError(err).Error("Failed to store last synced message id")
|
||||||
j.onError(err)
|
j.onError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// j.onError() also calls j.wg.Done().
|
||||||
|
j.wg.Done()
|
||||||
j.syncReporter.OnProgress(ctx, count)
|
j.syncReporter.OnProgress(ctx, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -192,7 +192,6 @@ func (s *Service) run(ctx context.Context, lastEventID string) {
|
|||||||
defer s.cpc.Close()
|
defer s.cpc.Close()
|
||||||
defer s.timer.Stop()
|
defer s.timer.Stop()
|
||||||
defer s.log.Info("Exiting service")
|
defer s.log.Info("Exiting service")
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
client := network.NewClientRetryWrapper(s.eventSource, &network.ExpCoolDown{})
|
client := network.NewClientRetryWrapper(s.eventSource, &network.ExpCoolDown{})
|
||||||
|
|
||||||
@ -303,14 +302,15 @@ func (s *Service) Close() {
|
|||||||
|
|
||||||
// Cleanup pending removes.
|
// Cleanup pending removes.
|
||||||
for _, s := range s.pendingSubscriptions {
|
for _, s := range s.pendingSubscriptions {
|
||||||
if s.op == pendingOpRemove {
|
|
||||||
if !processed.Contains(s.sub) {
|
if !processed.Contains(s.sub) {
|
||||||
|
processed.Add(s.sub)
|
||||||
|
|
||||||
|
if s.op == pendingOpRemove {
|
||||||
s.sub.close()
|
s.sub.close()
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
s.sub.cancel()
|
s.sub.cancel()
|
||||||
s.sub.close()
|
s.sub.close()
|
||||||
processed.Add(s.sub)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -589,6 +589,8 @@ func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
|||||||
return fmt.Errorf("failed to remove user from imap server: %w", err)
|
return fmt.Errorf("failed to remove user from imap server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.tasks.CancelAndWait()
|
||||||
|
|
||||||
// Stop Services
|
// Stop Services
|
||||||
user.serviceGroup.CancelAndWait()
|
user.serviceGroup.CancelAndWait()
|
||||||
|
|
||||||
@ -598,8 +600,6 @@ func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
|||||||
// Close imap service.
|
// Close imap service.
|
||||||
user.imapService.Close()
|
user.imapService.Close()
|
||||||
|
|
||||||
user.tasks.CancelAndWait()
|
|
||||||
|
|
||||||
if withAPI {
|
if withAPI {
|
||||||
user.log.Debug("Logging out from API")
|
user.log.Debug("Logging out from API")
|
||||||
|
|
||||||
@ -621,6 +621,9 @@ func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
|||||||
func (user *User) Close() {
|
func (user *User) Close() {
|
||||||
user.log.Info("Closing user")
|
user.log.Info("Closing user")
|
||||||
|
|
||||||
|
// Stop any ongoing background tasks.
|
||||||
|
user.tasks.CancelAndWait()
|
||||||
|
|
||||||
// Stop Services
|
// Stop Services
|
||||||
user.serviceGroup.CancelAndWait()
|
user.serviceGroup.CancelAndWait()
|
||||||
|
|
||||||
@ -630,9 +633,6 @@ func (user *User) Close() {
|
|||||||
// Close imap service.
|
// Close imap service.
|
||||||
user.imapService.Close()
|
user.imapService.Close()
|
||||||
|
|
||||||
// Stop any ongoing background tasks.
|
|
||||||
user.tasks.CancelAndWait()
|
|
||||||
|
|
||||||
// Close the user's API client.
|
// Close the user's API client.
|
||||||
user.client.Close()
|
user.client.Close()
|
||||||
|
|
||||||
|
|||||||
@ -72,11 +72,12 @@ func remove(dir string, except ...string) error {
|
|||||||
|
|
||||||
sort.Sort(sort.Reverse(sort.StringSlice(toRemove)))
|
sort.Sort(sort.Reverse(sort.StringSlice(toRemove)))
|
||||||
|
|
||||||
|
var multiErr error
|
||||||
for _, target := range toRemove {
|
for _, target := range toRemove {
|
||||||
if err := os.RemoveAll(target); err != nil {
|
if err := os.RemoveAll(target); err != nil {
|
||||||
return err
|
multiErr = multierror.Append(multiErr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return multiErr
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,3 +85,18 @@ Feature: IMAP copy messages
|
|||||||
| from | to | subject | unread |
|
| from | to | subject | unread |
|
||||||
| john.doe@mail.com | [user:user]@[domain] | foo | false |
|
| john.doe@mail.com | [user:user]@[domain] | foo | false |
|
||||||
|
|
||||||
|
Scenario: Move message to trash then copy to folder does not delete message
|
||||||
|
When IMAP client "1" moves the message with subject "foo" from "INBOX" to "Trash"
|
||||||
|
And it succeeds
|
||||||
|
Then IMAP client "1" eventually sees the following messages in "Trash":
|
||||||
|
| from | to | subject | unread |
|
||||||
|
| john.doe@mail.com | [user:user]@[domain] | foo | false |
|
||||||
|
When IMAP client "1" copies the message with subject "foo" from "Trash" to "Folders/mbox"
|
||||||
|
And it succeeds
|
||||||
|
When IMAP client "1" marks the message with subject "foo" as deleted
|
||||||
|
Then it succeeds
|
||||||
|
When IMAP client "1" expunges
|
||||||
|
Then it succeeds
|
||||||
|
Then IMAP client "1" eventually sees the following messages in "Folders/mbox":
|
||||||
|
| from | to | subject | unread |
|
||||||
|
| john.doe@mail.com | [user:user]@[domain] | foo | false |
|
||||||
|
|||||||
@ -7,7 +7,7 @@ Feature: IMAP remove messages from Trash
|
|||||||
| label | label |
|
| label | label |
|
||||||
Then it succeeds
|
Then it succeeds
|
||||||
|
|
||||||
Scenario Outline: Message in Trash and some other label is permanently deleted
|
Scenario Outline: Message in Trash and some other label is not permanently deleted
|
||||||
Given the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Trash":
|
Given the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Trash":
|
||||||
| from | to | subject | body |
|
| from | to | subject | body |
|
||||||
| john.doe@mail.com | [user:user]@[domain] | foo | hello |
|
| john.doe@mail.com | [user:user]@[domain] | foo | hello |
|
||||||
@ -27,8 +27,8 @@ Feature: IMAP remove messages from Trash
|
|||||||
When IMAP client "1" expunges
|
When IMAP client "1" expunges
|
||||||
Then it succeeds
|
Then it succeeds
|
||||||
And IMAP client "1" eventually sees 1 messages in "Trash"
|
And IMAP client "1" eventually sees 1 messages in "Trash"
|
||||||
And IMAP client "1" eventually sees 1 messages in "All Mail"
|
And IMAP client "1" eventually sees 2 messages in "All Mail"
|
||||||
And IMAP client "1" eventually sees 0 messages in "Labels/label"
|
And IMAP client "1" eventually sees 1 messages in "Labels/label"
|
||||||
|
|
||||||
Scenario Outline: Message in Trash only is permanently deleted
|
Scenario Outline: Message in Trash only is permanently deleted
|
||||||
Given the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Trash":
|
Given the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Trash":
|
||||||
|
|||||||
Reference in New Issue
Block a user