mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
102 Commits
v3.20.0
...
c32c431640
| Author | SHA1 | Date | |
|---|---|---|---|
| c32c431640 | |||
| 4cc2ded001 | |||
| 15880dfe19 | |||
| dbef40cfc5 | |||
| e9ea976773 | |||
| a00af3a398 | |||
| 8b891fb3e7 | |||
| 50ab740b92 | |||
| 39f2362996 | |||
| d2742c81e5 | |||
| 9cb914cf13 | |||
| 4088cf18c3 | |||
| c02bae5eb2 | |||
| 94125056ab | |||
| 2aa8acfb5b | |||
| 8109b384c5 | |||
| 6d79ad3e41 | |||
| 5d93ee0cfc | |||
| 675b37a2fa | |||
| c3e2201945 | |||
| 9d4415d8cc | |||
| 89da7335b6 | |||
| 4557f54e2f | |||
| 42605c1923 | |||
| 9f4801b738 | |||
| 4e6236611a | |||
| 0800aeea50 | |||
| b230f2ece6 | |||
| d44c488ed5 | |||
| 8237129670 | |||
| 8e634995c5 | |||
| 10a685a123 | |||
| 896f50c754 | |||
| 60633fc09c | |||
| 9c5b5c2ac3 | |||
| 4f4a2c3fd8 | |||
| 120a7b3626 | |||
| 7cf3b6fb7b | |||
| 03c9455b0d | |||
| 61ca604ace | |||
| a8caec560e | |||
| df78e29234 | |||
| 6105f32c75 | |||
| da76784290 | |||
| 43cbedafb8 | |||
| 0d33cc5000 | |||
| ed5adb18fb | |||
| 85a91c5572 | |||
| 56d4bfbb71 | |||
| 48a75b0dd7 | |||
| b84663dd7a | |||
| cd8db6fd1c | |||
| a5e0f85a58 | |||
| 6cbe51138a | |||
| 82607efe1c | |||
| 961dc9435f | |||
| b574ccb6ea | |||
| 2569e83e51 | |||
| f34a7ff0ed | |||
| da069a0155 | |||
| 384fa4eb4b | |||
| 0c6e4ffa35 | |||
| 4951244400 | |||
| d65d6ee2e5 | |||
| 097d6f86d3 | |||
| 9894cf9744 | |||
| f84067de3e | |||
| f885bfbcf4 | |||
| f3aac09ecb | |||
| 38d692ebfb | |||
| 1acc7eb7db | |||
| 248fbf5e33 | |||
| 8b12a454ea | |||
| 310fcffc7b | |||
| 318ad16378 | |||
| 8be4246f7e | |||
| e580f89106 | |||
| 01043e033e | |||
| 94b44b383a | |||
| a3b8fabb26 | |||
| 275b30e518 | |||
| bf244e5c86 | |||
| cf9651bb94 | |||
| ba65ffdbc7 | |||
| 4b95ef4d82 | |||
| 951c7c27fb | |||
| e7423a9519 | |||
| d3582fa981 | |||
| 80c852a5b2 | |||
| 51498e3e37 | |||
| b7ef6e1486 | |||
| 0d03f84711 | |||
| 949666724d | |||
| bbe19bf960 | |||
| bfe25e3a46 | |||
| 236c958703 | |||
| e6b312b437 | |||
| 384154c767 | |||
| 45d2e9ea63 | |||
| 86e8a566c7 | |||
| a80fd92018 | |||
| 71063ac5ee |
@ -127,6 +127,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
|
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
|
||||||
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
|
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
|
||||||
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
|
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
|
||||||
|
* [objx](https://github.com/stretchr/objx) available under [license](https://github.com/stretchr/objx/blob/master/LICENSE)
|
||||||
* [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE)
|
* [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE)
|
||||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||||
|
|||||||
34
Changelog.md
34
Changelog.md
@ -3,6 +3,40 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Kanmon Bridge 3.21.2
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-406: Fixed faulty certificate chain validation logic. Made certificate pin checks exclusive to leaf certificates.
|
||||||
|
|
||||||
|
|
||||||
|
## Kanmon Bridge 3.21.1
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-383: Extended internal mailbox conflict resolution logic and minor changes to the mailbox conflict pre-checker.
|
||||||
|
|
||||||
|
|
||||||
|
## Kanmon Bridge 3.21.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* BRIDGE-379: Mailbox pre-check on Bridge startup & conflict resolver for Bridge internal mailboxes.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-376: Explicitly catch Gluon DB mailbox name conflicts and report them to Sentry.
|
||||||
|
* BRIDGE-373: Extend user mailbox conflict resolver logging & report sync errors to Sentry.
|
||||||
|
* BRIDGE-366: Kill switch support for IMAP IDLE.
|
||||||
|
* BRIDGE-363: Observability metric support for IMAP connections.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-377: Correct API label field usage on user label conflict resolver - update handler (event loop).
|
||||||
|
* BRIDGE-378: Fix incorrect field usage for system mailbox names.
|
||||||
|
|
||||||
|
|
||||||
|
## Jubilee Bridge 3.20.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-362: Implemented logic for reconciling label conflicts.
|
||||||
|
|
||||||
|
|
||||||
## Jubilee Bridge 3.20.0
|
## Jubilee Bridge 3.20.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
|||||||
.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.20.0+git
|
BRIDGE_APP_VERSION?=3.21.2+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
|
||||||
|
|||||||
3
go.mod
3
go.mod
@ -7,7 +7,7 @@ toolchain go1.24.2
|
|||||||
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.20250324123053-2abce471ad71
|
github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d
|
||||||
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.20250417134000-e624a080f7ba
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
||||||
@ -114,6 +114,7 @@ require (
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
|||||||
22
go.sum
22
go.sum
@ -36,8 +36,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||||
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/gluon v0.17.1-0.20250324123053-2abce471ad71 h1:UC8SLrS6QbBeOUM8FJugyNoeV5gRGoQCwNePAMxuM20=
|
github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d h1:45W7G+X0w7nzLzeB0eiFkGho5DTK1jNmmNbt3IhN524=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20250324123053-2abce471ad71/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
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 v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
|
github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
|
||||||
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
|
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
|
||||||
@ -45,14 +45,6 @@ github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDx
|
|||||||
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/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.20250217140732-2e531f21de4c h1:dxnbB+ov77BDj1LC35fKZ14hLoTpU6OTpZySwxarVx0=
|
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409092940-13ddc20a05a1 h1:u3G9UB8prOnzOneOf0JFCIVnMRLiK4QgEpPQVu9Y8Q4=
|
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409092940-13ddc20a05a1/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409131808-0bbc8e7c32db h1:mOtbY5BB2eNr2QmbZhFn5EnsJcimTntPB6akN2r+AuE=
|
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409131808-0bbc8e7c32db/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250410050801-92de6e7c8517 h1:70JoDgXxfil4hbDoYGF98rMd47Rld6wXWyFAw4uFOTY=
|
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250410050801-92de6e7c8517/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba h1:DFBngZ7u/f69flRFzPp6Ipo6PKEyflJlA5OCh52yDB4=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba h1:DFBngZ7u/f69flRFzPp6Ipo6PKEyflJlA5OCh52yDB4=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||||
@ -506,8 +498,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
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=
|
||||||
@ -565,8 +555,6 @@ 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/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
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=
|
||||||
@ -583,8 +571,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -625,8 +611,6 @@ 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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
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=
|
||||||
@ -648,8 +632,6 @@ 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/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/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
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=
|
||||||
|
|||||||
@ -335,6 +335,7 @@ func newBridge(
|
|||||||
uidValidityGenerator,
|
uidValidityGenerator,
|
||||||
&bridgeIMAPSMTPTelemetry{b: bridge},
|
&bridgeIMAPSMTPTelemetry{b: bridge},
|
||||||
observabilityService,
|
observabilityService,
|
||||||
|
unleashService,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check whether username has changed and correct (macOS only)
|
// Check whether username has changed and correct (macOS only)
|
||||||
@ -746,7 +747,7 @@ func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric)
|
|||||||
bridge.observabilityService.AddMetrics(metric)
|
bridge.observabilityService.AddMetrics(metric)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||||
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
|
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,9 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
|||||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||||
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
|
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// It's called whenever a context is cancelled during sync. We should ought to remove this and make it more granular in the future.
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext("Failed to sync, will retry later", gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
return mocks
|
return mocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -88,3 +88,18 @@ func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interfac
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ReportWarningWithContext mocks base method.
|
||||||
|
func (m *MockReporter) ReportWarningWithContext(arg0 string, arg1 map[string]interface{}) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ReportWarningWithContext", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportWarningWithContext indicates an expected call of ReportWarningWithContext.
|
||||||
|
func (mr *MockReporterMockRecorder) ReportWarningWithContext(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportWarningWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
|
||||||
|
}
|
||||||
@ -25,7 +25,7 @@ func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySende
|
|||||||
|
|
||||||
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
|
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
|
||||||
|
|
||||||
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) {
|
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
m.ctrl.Call(m, "AddDistinctMetrics", errType)
|
m.ctrl.Call(m, "AddDistinctMetrics", errType)
|
||||||
}
|
}
|
||||||
@ -35,7 +35,18 @@ func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetr
|
|||||||
m.ctrl.Call(m, "AddMetrics", metrics)
|
m.ctrl.Call(m, "AddMetrics", metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
|
func (m *MockObservabilitySender) AddTimeLimitedMetric(metricType observability.DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "AddTimeLimitedMetric", metricType, metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockObservabilitySender) GetEmailClient() string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "GetEmailClient")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
|
||||||
"AddDistinctMetrics",
|
"AddDistinctMetrics",
|
||||||
@ -47,3 +58,13 @@ func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.Observab
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mr *MockObservabilitySenderRecorder) AddTimeLimitedMetric(metricType observability.DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTimeLimitedMetric", reflect.TypeOf((*MockObservabilitySender)(nil).AddTimeLimitedMetric), metricType, metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MockObservabilitySenderRecorder) GetEmailClient() {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
mr.mock.ctrl.Call(mr.mock, "GetEmailClient", reflect.TypeOf((*MockObservabilitySender)(nil).GetEmailClient))
|
||||||
|
}
|
||||||
|
|||||||
@ -551,7 +551,7 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
syncSettingsPath,
|
syncSettingsPath,
|
||||||
isNew,
|
isNew,
|
||||||
bridge.notificationStore,
|
bridge.notificationStore,
|
||||||
bridge.unleashService.GetFlagValue,
|
bridge.unleashService,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create user: %w", err)
|
return fmt.Errorf("failed to create user: %w", err)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@ -76,6 +77,9 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
|
|||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
}
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
closeCh()
|
closeCh()
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,8 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,6 +31,11 @@ type TLSDialer interface {
|
|||||||
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
|
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SecureTLSDialer interface {
|
||||||
|
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
|
||||||
|
ShouldSkipCertificateChainVerification(address string) bool
|
||||||
|
}
|
||||||
|
|
||||||
func SetBasicTransportTimeouts(t *http.Transport) {
|
func SetBasicTransportTimeouts(t *http.Transport) {
|
||||||
t.MaxIdleConns = 100
|
t.MaxIdleConns = 100
|
||||||
t.MaxIdleConnsPerHost = 100
|
t.MaxIdleConnsPerHost = 100
|
||||||
@ -71,6 +78,35 @@ func NewBasicTLSDialer(hostURL string) *BasicTLSDialer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractDomain(hostname string) string {
|
||||||
|
parts := strings.Split(hostname, ".")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
return strings.Join(parts[len(parts)-2:], ".")
|
||||||
|
}
|
||||||
|
return hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldSkipCertificateChainVerification determines whether certificate chain validation should be skipped.
|
||||||
|
// It compares the domain of the requested address with the configured host URL domain.
|
||||||
|
// Returns true if the domains don't match (skip verification), false if they do (perform verification).
|
||||||
|
//
|
||||||
|
// NOTE: This assumes single-part TLDs (.com, .me) and won't handle multi-part TLDs correctly.
|
||||||
|
func (d *BasicTLSDialer) ShouldSkipCertificateChainVerification(address string) bool {
|
||||||
|
parsedURL, err := url.Parse(d.hostURL)
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
addressHost, _, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
addressHost = address
|
||||||
|
}
|
||||||
|
|
||||||
|
hostDomain := extractDomain(parsedURL.Host)
|
||||||
|
addressDomain := extractDomain(addressHost)
|
||||||
|
return addressDomain != hostDomain
|
||||||
|
}
|
||||||
|
|
||||||
// DialTLSContext returns a connection to the given address using the given network.
|
// DialTLSContext returns a connection to the given address using the given network.
|
||||||
func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||||
return (&tls.Dialer{
|
return (&tls.Dialer{
|
||||||
@ -78,7 +114,7 @@ func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address st
|
|||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
Config: &tls.Config{
|
Config: &tls.Config{
|
||||||
InsecureSkipVerify: address != d.hostURL, //nolint:gosec
|
InsecureSkipVerify: d.ShouldSkipCertificateChainVerification(address), //nolint:gosec
|
||||||
},
|
},
|
||||||
}).DialContext(ctx, network, address)
|
}).DialContext(ctx, network, address)
|
||||||
}
|
}
|
||||||
|
|||||||
134
internal/dialer/dialer_basic_test.go
Normal file
134
internal/dialer/dialer_basic_test.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// Copyright (c) 2025 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 dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasicTLSDialer_ShouldSkipCertificateChainVerification(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
hostURL string
|
||||||
|
address string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
hostURL: "https://mail-api.proton.me",
|
||||||
|
address: "mail-api.proton.me:443",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me",
|
||||||
|
address: "proton.me",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://api.proton.me",
|
||||||
|
address: "mail.proton.me:443",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me",
|
||||||
|
address: "mail-api.proton.me:443",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://mail-api.proton.me",
|
||||||
|
address: "proton.me:443",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://mail.google.com",
|
||||||
|
address: "mail-api.proton.me:443",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://mail-api.protonmail.com",
|
||||||
|
address: "mail-api.proton.me:443",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me",
|
||||||
|
address: "google.com:443",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me",
|
||||||
|
address: "proton.com:443",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me",
|
||||||
|
address: "example.me:443",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me",
|
||||||
|
address: "mail.example.com:443",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me",
|
||||||
|
address: "proton.me",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me:8080",
|
||||||
|
address: "proton.me:443",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.me/api/v1",
|
||||||
|
address: "proton.me:443",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://proton.black",
|
||||||
|
address: "mail-api.pascal.proton.black",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://mail-api.pascal.proton.black",
|
||||||
|
address: "mail-api.pascal.proton.black",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://mail-api.pascal.proton.black",
|
||||||
|
address: "proton.black:332",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://mail-api.pascal.proton.black",
|
||||||
|
address: "proton.me",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostURL: "https://mail-api.pascal.proton.black",
|
||||||
|
address: "proton.me:332",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
dialer := NewBasicTLSDialer(tt.hostURL)
|
||||||
|
result := dialer.ShouldSkipCertificateChainVerification(tt.address)
|
||||||
|
require.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -50,12 +50,12 @@ var TrustedAPIPins = []string{ //nolint:gochecknoglobals
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TLSReportURI is the address where TLS reports should be sent.
|
// TLSReportURI is the address where TLS reports should be sent.
|
||||||
const TLSReportURI = "https://reports.protonmail.ch/reports/tls"
|
const TLSReportURI = "https://reports.proton.me/reports/tls"
|
||||||
|
|
||||||
// PinningTLSDialer wraps a TLSDialer to check fingerprints after connecting and
|
// PinningTLSDialer wraps a TLSDialer to check fingerprints after connecting and
|
||||||
// to report errors if the fingerprint check fails.
|
// to report errors if the fingerprint check fails.
|
||||||
type PinningTLSDialer struct {
|
type PinningTLSDialer struct {
|
||||||
dialer TLSDialer
|
dialer SecureTLSDialer
|
||||||
pinChecker PinChecker
|
pinChecker PinChecker
|
||||||
reporter Reporter
|
reporter Reporter
|
||||||
tlsIssueCh chan struct{}
|
tlsIssueCh chan struct{}
|
||||||
@ -68,13 +68,13 @@ type Reporter interface {
|
|||||||
|
|
||||||
// PinChecker is used to check TLS keys of connections.
|
// PinChecker is used to check TLS keys of connections.
|
||||||
type PinChecker interface {
|
type PinChecker interface {
|
||||||
CheckCertificate(conn net.Conn) error
|
CheckCertificate(conn net.Conn, certificateChainVerificationSkipped bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPinningTLSDialer constructs a new dialer which only returns TCP connections to servers
|
// NewPinningTLSDialer constructs a new dialer which only returns TCP connections to servers
|
||||||
// which present known certificates.
|
// which present known certificates.
|
||||||
// It checks pins using the given pinChecker and reports issues using the given reporter.
|
// It checks pins using the given pinChecker and reports issues using the given reporter.
|
||||||
func NewPinningTLSDialer(dialer TLSDialer, reporter Reporter, pinChecker PinChecker) *PinningTLSDialer {
|
func NewPinningTLSDialer(dialer SecureTLSDialer, reporter Reporter, pinChecker PinChecker) *PinningTLSDialer {
|
||||||
return &PinningTLSDialer{
|
return &PinningTLSDialer{
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
pinChecker: pinChecker,
|
pinChecker: pinChecker,
|
||||||
@ -85,6 +85,7 @@ func NewPinningTLSDialer(dialer TLSDialer, reporter Reporter, pinChecker PinChec
|
|||||||
|
|
||||||
// DialTLSContext dials the given network/address, returning an error if the certificates don't match the trusted pins.
|
// DialTLSContext dials the given network/address, returning an error if the certificates don't match the trusted pins.
|
||||||
func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
|
func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
shouldSkipCertificateChainVerification := p.dialer.ShouldSkipCertificateChainVerification(address)
|
||||||
conn, err := p.dialer.DialTLSContext(ctx, network, address)
|
conn, err := p.dialer.DialTLSContext(ctx, network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -95,7 +96,7 @@ func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.pinChecker.CheckCertificate(conn); err != nil {
|
if err := p.pinChecker.CheckCertificate(conn, shouldSkipCertificateChainVerification); err != nil {
|
||||||
if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil {
|
if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil {
|
||||||
p.reporter.ReportCertIssue(TLSReportURI, host, port, tlsConn.ConnectionState())
|
p.reporter.ReportCertIssue(TLSReportURI, host, port, tlsConn.ConnectionState())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,3 +41,15 @@ func NewTLSPinChecker(trustedPins []string) *TLSPinChecker {
|
|||||||
func certFingerprint(cert *x509.Certificate) string {
|
func certFingerprint(cert *x509.Certificate) string {
|
||||||
return fmt.Sprintf(`pin-sha256=%q`, algo.HashBase64SHA256(string(cert.RawSubjectPublicKeyInfo)))
|
return fmt.Sprintf(`pin-sha256=%q`, algo.HashBase64SHA256(string(cert.RawSubjectPublicKeyInfo)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *TLSPinChecker) isCertFoundInKnownPins(cert *x509.Certificate) bool {
|
||||||
|
fingerprint := certFingerprint(cert)
|
||||||
|
|
||||||
|
for _, pin := range p.trustedPins {
|
||||||
|
if pin == fingerprint {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -25,8 +25,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckCertificate returns whether the connection presents a known TLS certificate.
|
// CheckCertificate verifies that the connection presents a known pinned leaf TLS certificate.
|
||||||
func (p *TLSPinChecker) CheckCertificate(conn net.Conn) error {
|
func (p *TLSPinChecker) CheckCertificate(conn net.Conn, certificateChainVerificationSkipped bool) error {
|
||||||
tlsConn, ok := conn.(*tls.Conn)
|
tlsConn, ok := conn.(*tls.Conn)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("connection is not a TLS connection")
|
return errors.New("connection is not a TLS connection")
|
||||||
@ -34,14 +34,31 @@ func (p *TLSPinChecker) CheckCertificate(conn net.Conn) error {
|
|||||||
|
|
||||||
connState := tlsConn.ConnectionState()
|
connState := tlsConn.ConnectionState()
|
||||||
|
|
||||||
for _, peerCert := range connState.PeerCertificates {
|
// When certificate chain verification is enabled (e.g., for known API hosts), we expect the TLS handshake to produce verified chains.
|
||||||
fingerprint := certFingerprint(peerCert)
|
// We then validate that the leaf certificate of at least one verified chain matches a known pinned public key.
|
||||||
|
if !certificateChainVerificationSkipped {
|
||||||
|
if len(connState.VerifiedChains) == 0 {
|
||||||
|
return errors.New("no verified certificate chains")
|
||||||
|
}
|
||||||
|
|
||||||
for _, pin := range p.trustedPins {
|
for _, chain := range connState.VerifiedChains {
|
||||||
if pin == fingerprint {
|
// Check if the leaf certificate is one of the trusted pins.
|
||||||
|
if p.isCertFoundInKnownPins(chain[0]) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ErrTLSMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// When certificate chain verification is skipped (e.g., for DoH proxies using self-signed certs),
|
||||||
|
// we only validate the leaf certificate against known pinned public keys.
|
||||||
|
if len(connState.PeerCertificates) == 0 {
|
||||||
|
return errors.New("no peer certificates available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.isCertFoundInKnownPins(connState.PeerCertificates[0]) {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrTLSMismatch
|
return ErrTLSMismatch
|
||||||
|
|||||||
@ -23,6 +23,6 @@ import "net"
|
|||||||
|
|
||||||
// CheckCertificate returns whether the connection presents a known TLS certificate.
|
// CheckCertificate returns whether the connection presents a known TLS certificate.
|
||||||
// The QA implementation always returns nil.
|
// The QA implementation always returns nil.
|
||||||
func (p *TLSPinChecker) CheckCertificate(conn net.Conn) error {
|
func (p *TLSPinChecker) CheckCertificate(conn net.Conn, _ bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,8 +64,7 @@ func TestTLSPinInvalid(t *testing.T) {
|
|||||||
checkTLSIssueHandler(t, 1, called)
|
checkTLSIssueHandler(t, 1, called)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disabled for now we'll need to patch this up.
|
func TestTLSPinNoMatch(t *testing.T) {
|
||||||
func _TestTLSPinNoMatch(t *testing.T) { //nolint:unused
|
|
||||||
skipIfProxyIsSet(t)
|
skipIfProxyIsSet(t)
|
||||||
|
|
||||||
called, _, reporter, checker, cm := createClientWithPinningDialer(getRootURL())
|
called, _, reporter, checker, cm := createClientWithPinningDialer(getRootURL())
|
||||||
@ -96,7 +95,7 @@ func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
|||||||
|
|
||||||
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
||||||
copyTrustedPins(checker)
|
copyTrustedPins(checker)
|
||||||
checker.trustedPins = append(checker.trustedPins, `pin-sha256="hgraU1+uoS6kjiJaH5G+BiqQoyiIml1Nat+2FiUAcII="`)
|
checker.trustedPins = append(checker.trustedPins, `pin-sha256="FlvTPG/nIMKtOj9nelnEjujwSZ5EDyfiKYxZgbXREls="`)
|
||||||
_, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443")
|
_, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443")
|
||||||
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
|
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -157,7 +157,7 @@ func (r *Reporter) ReportExceptionWithContext(i interface{}, context map[string]
|
|||||||
SkipDuringUnwind()
|
SkipDuringUnwind()
|
||||||
|
|
||||||
err := fmt.Errorf("recover: %v", i)
|
err := fmt.Errorf("recover: %v", i)
|
||||||
return r.scopedReport(context, func() {
|
return r.scopedReport(context, func(_ *sentry.Scope) {
|
||||||
SkipDuringUnwind()
|
SkipDuringUnwind()
|
||||||
if eventID := sentry.CaptureException(err); eventID != nil {
|
if eventID := sentry.CaptureException(err); eventID != nil {
|
||||||
logrus.WithError(err).
|
logrus.WithError(err).
|
||||||
@ -169,7 +169,20 @@ func (r *Reporter) ReportExceptionWithContext(i interface{}, context map[string]
|
|||||||
|
|
||||||
func (r *Reporter) ReportMessageWithContext(msg string, context map[string]interface{}) error {
|
func (r *Reporter) ReportMessageWithContext(msg string, context map[string]interface{}) error {
|
||||||
SkipDuringUnwind()
|
SkipDuringUnwind()
|
||||||
return r.scopedReport(context, func() {
|
return r.scopedReport(context, func(_ *sentry.Scope) {
|
||||||
|
SkipDuringUnwind()
|
||||||
|
if eventID := sentry.CaptureMessage(msg); eventID != nil {
|
||||||
|
logrus.WithField("message", msg).
|
||||||
|
WithField("reportID", *eventID).
|
||||||
|
Warn("Captured message")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) ReportWarningWithContext(msg string, context map[string]interface{}) error {
|
||||||
|
SkipDuringUnwind()
|
||||||
|
return r.scopedReport(context, func(scope *sentry.Scope) {
|
||||||
|
scope.SetLevel(sentry.LevelWarning)
|
||||||
SkipDuringUnwind()
|
SkipDuringUnwind()
|
||||||
if eventID := sentry.CaptureMessage(msg); eventID != nil {
|
if eventID := sentry.CaptureMessage(msg); eventID != nil {
|
||||||
logrus.WithField("message", msg).
|
logrus.WithField("message", msg).
|
||||||
@ -180,7 +193,7 @@ func (r *Reporter) ReportMessageWithContext(msg string, context map[string]inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Report reports a sentry crash with stacktrace from all goroutines.
|
// Report reports a sentry crash with stacktrace from all goroutines.
|
||||||
func (r *Reporter) scopedReport(context map[string]interface{}, doReport func()) error {
|
func (r *Reporter) scopedReport(context map[string]interface{}, doReport func(scope *sentry.Scope)) error {
|
||||||
SkipDuringUnwind()
|
SkipDuringUnwind()
|
||||||
|
|
||||||
if os.Getenv("PROTONMAIL_ENV") == "dev" {
|
if os.Getenv("PROTONMAIL_ENV") == "dev" {
|
||||||
@ -206,7 +219,7 @@ func (r *Reporter) scopedReport(context map[string]interface{}, doReport func())
|
|||||||
map[string]sentry.Context{"bridge": contextToString(context)},
|
map[string]sentry.Context{"bridge": contextToString(context)},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
doReport()
|
doReport(scope)
|
||||||
})
|
})
|
||||||
|
|
||||||
if !sentry.Flush(time.Second * 10) {
|
if !sentry.Flush(time.Second * 10) {
|
||||||
@ -287,3 +300,25 @@ func contextToString(context sentry.Context) sentry.Context {
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NullSentryReporter struct{}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportException(any) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportMessage(string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportMessageWithContext(string, reporter.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportWarningWithContext(string, reporter.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportExceptionWithContext(any, reporter.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
388
internal/services/imapservice/conflicts.go
Normal file
388
internal/services/imapservice/conflicts.go
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
// Copyright (c) 2025 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"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/db"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GluonLabelNameProvider interface {
|
||||||
|
GetUserMailboxByName(ctx context.Context, addrID string, labelName []string) (imap.MailboxData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type gluonIDProvider interface {
|
||||||
|
GetGluonID(addrID string) (string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sentryReporter interface {
|
||||||
|
ReportMessageWithContext(string, reporter.Context) error
|
||||||
|
ReportWarningWithContext(string, reporter.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiClient interface {
|
||||||
|
GetLabel(ctx context.Context, labelID string, labelTypes ...proton.LabelType) (proton.Label, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mailboxFetcherFn func(ctx context.Context, label proton.Label) (imap.MailboxData, error)
|
||||||
|
|
||||||
|
type mailboxMessageCountFetcherFn func(ctx context.Context, internalMailboxID imap.InternalMailboxID) (int, error)
|
||||||
|
|
||||||
|
type LabelConflictManager struct {
|
||||||
|
gluonLabelNameProvider GluonLabelNameProvider
|
||||||
|
gluonIDProvider gluonIDProvider
|
||||||
|
client apiClient
|
||||||
|
reporter sentryReporter
|
||||||
|
featureFlagProvider unleash.FeatureFlagValueProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLabelConflictManager(
|
||||||
|
gluonLabelNameProvider GluonLabelNameProvider,
|
||||||
|
gluonIDProvider gluonIDProvider,
|
||||||
|
client apiClient,
|
||||||
|
reporter sentryReporter,
|
||||||
|
featureFlagProvider unleash.FeatureFlagValueProvider) *LabelConflictManager {
|
||||||
|
return &LabelConflictManager{
|
||||||
|
gluonLabelNameProvider: gluonLabelNameProvider,
|
||||||
|
gluonIDProvider: gluonIDProvider,
|
||||||
|
client: client,
|
||||||
|
reporter: reporter,
|
||||||
|
featureFlagProvider: featureFlagProvider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LabelConflictManager) generateMailboxFetcher(connectors []*Connector) mailboxFetcherFn {
|
||||||
|
return func(ctx context.Context, label proton.Label) (imap.MailboxData, error) {
|
||||||
|
for _, updateCh := range connectors {
|
||||||
|
addrID, ok := m.gluonIDProvider.GetGluonID(updateCh.addrID)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return m.gluonLabelNameProvider.GetUserMailboxByName(ctx, addrID, GetMailboxName(label))
|
||||||
|
}
|
||||||
|
return imap.MailboxData{}, errors.New("no gluon connectors found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LabelConflictManager) generateMailboxMessageCountFetcher(connectors []*Connector) mailboxMessageCountFetcherFn {
|
||||||
|
return func(ctx context.Context, id imap.InternalMailboxID) (int, error) {
|
||||||
|
var countSum int
|
||||||
|
var errs []error
|
||||||
|
for _, conn := range connectors {
|
||||||
|
count, err := conn.GetMailboxMessageCount(ctx, id)
|
||||||
|
countSum += count
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return countSum, errors.Join(errs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabelConflictResolver interface {
|
||||||
|
ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error)
|
||||||
|
}
|
||||||
|
type labelConflictResolverImpl struct {
|
||||||
|
mailboxFetch mailboxFetcherFn
|
||||||
|
client apiClient
|
||||||
|
reporter sentryReporter
|
||||||
|
log *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
type nullLabelConflictResolverImpl struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *nullLabelConflictResolverImpl) ResolveConflict(_ context.Context, _ proton.Label, _ map[string]bool) (func() []imap.Update, error) {
|
||||||
|
return func() []imap.Update {
|
||||||
|
return []imap.Update{}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LabelConflictManager) NewConflictResolver(connectors []*Connector) LabelConflictResolver {
|
||||||
|
if m.featureFlagProvider.GetFlagValue(unleash.LabelConflictResolverDisabled) {
|
||||||
|
return &nullLabelConflictResolverImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &labelConflictResolverImpl{
|
||||||
|
mailboxFetch: m.generateMailboxFetcher(connectors),
|
||||||
|
client: m.client,
|
||||||
|
reporter: m.reporter,
|
||||||
|
log: logrus.WithFields(logrus.Fields{
|
||||||
|
"pkg": "imapservice/labelConflictResolver",
|
||||||
|
"numberOfConnectors": len(connectors),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error) {
|
||||||
|
logger := r.log.WithFields(logrus.Fields{
|
||||||
|
"labelID": label.ID,
|
||||||
|
"labelPath": hashLabelPaths(GetMailboxName(label)),
|
||||||
|
})
|
||||||
|
|
||||||
|
// For system type labels we shouldn't care.
|
||||||
|
var updateFns []func() []imap.Update
|
||||||
|
|
||||||
|
// There's a cycle, such as in a label swap operation, we'll need to temporarily rename the label.
|
||||||
|
// The change will be overwritten by one of the previous recursive calls.
|
||||||
|
if visited[label.ID] {
|
||||||
|
logrus.Info("Cycle detected, applying temporary rename")
|
||||||
|
fn := func() []imap.Update {
|
||||||
|
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), getMailboxNameWithTempPrefix(label))}
|
||||||
|
}
|
||||||
|
updateFns = append(updateFns, fn)
|
||||||
|
return combineIMAPUpdateFns(updateFns), nil
|
||||||
|
}
|
||||||
|
visited[label.ID] = true
|
||||||
|
|
||||||
|
// Fetch the gluon mailbox data and verify whether there are conflicts with the name.
|
||||||
|
mailboxData, err := r.mailboxFetch(ctx, label)
|
||||||
|
if err != nil {
|
||||||
|
// Name is free, create the mailbox.
|
||||||
|
if db.IsErrNotFound(err) {
|
||||||
|
logger.Info("Label not found in DB, creating mailbox.")
|
||||||
|
fn := func() []imap.Update {
|
||||||
|
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label))}
|
||||||
|
}
|
||||||
|
updateFns = append(updateFns, fn)
|
||||||
|
return combineIMAPUpdateFns(updateFns), nil
|
||||||
|
}
|
||||||
|
return combineIMAPUpdateFns(updateFns), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify whether the label name corresponds to the same label ID. If true terminate, we don't need to update.
|
||||||
|
if mailboxData.RemoteID == label.ID {
|
||||||
|
logger.Info("Mailbox name matches label ID, no conflict.")
|
||||||
|
return combineIMAPUpdateFns(updateFns), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This means we've found a conflict. So let's log it.
|
||||||
|
logger = logger.WithFields(logrus.Fields{
|
||||||
|
"conflictingLabelID": mailboxData.RemoteID,
|
||||||
|
"conflictingLabelPath": hashLabelPaths(mailboxData.BridgeName),
|
||||||
|
})
|
||||||
|
logger.Info("Label conflict found")
|
||||||
|
|
||||||
|
// If the label name belongs to some other label ID. Fetch it's state from the remote.
|
||||||
|
conflictingLabel, err := r.client.GetLabel(ctx, mailboxData.RemoteID, proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem)
|
||||||
|
if err != nil {
|
||||||
|
// If it's not present on the remote we should delete it. And create the new label.
|
||||||
|
if errors.Is(err, proton.ErrNoSuchLabel) {
|
||||||
|
logger.Info("Conflicting label does not exist on remote. Deleting.")
|
||||||
|
fn := func() []imap.Update {
|
||||||
|
return []imap.Update{
|
||||||
|
imap.NewMailboxDeleted(imap.MailboxID(mailboxData.RemoteID)), // Should this be with remote ID
|
||||||
|
newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateFns = append(updateFns, fn)
|
||||||
|
return combineIMAPUpdateFns(updateFns), nil
|
||||||
|
}
|
||||||
|
logger.WithError(err).Error("Failed to fetch conflicting label from remote.")
|
||||||
|
return combineIMAPUpdateFns(updateFns), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the conflicting label name has changed. If not, then this is a BE inconsistency.
|
||||||
|
if compareLabelNames(GetMailboxName(conflictingLabel), mailboxData.BridgeName) {
|
||||||
|
if err := r.reporter.ReportMessageWithContext("Unexpected label conflict", reporter.Context{
|
||||||
|
"labelID": label.ID,
|
||||||
|
"conflictingLabelID": conflictingLabel.ID,
|
||||||
|
}); err != nil {
|
||||||
|
logger.WithError(err).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := fmt.Errorf("unexpected label conflict: the name of label ID %s is already used by label ID %s", label.ID, conflictingLabel.ID)
|
||||||
|
return combineIMAPUpdateFns(updateFns), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The name of the conflicting label has changed on the remote. We need to verify that the new name does not conflict with anything else.
|
||||||
|
// Thus, a recursive check can be performed.
|
||||||
|
logger.WithField("conflictingLabelNewPath", hashLabelPaths(conflictingLabel.Path)).
|
||||||
|
Info("Conflicting label name has changed. Recursively resolving conflict.")
|
||||||
|
childUpdateFns, err := r.ResolveConflict(ctx, conflictingLabel, visited)
|
||||||
|
if err != nil {
|
||||||
|
return combineIMAPUpdateFns(updateFns), err
|
||||||
|
}
|
||||||
|
updateFns = append(updateFns, childUpdateFns)
|
||||||
|
|
||||||
|
fn := func() []imap.Update {
|
||||||
|
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label))}
|
||||||
|
}
|
||||||
|
updateFns = append(updateFns, fn)
|
||||||
|
|
||||||
|
return combineIMAPUpdateFns(updateFns), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func combineIMAPUpdateFns(updateFunctions []func() []imap.Update) func() []imap.Update {
|
||||||
|
return func() []imap.Update {
|
||||||
|
var updates []imap.Update
|
||||||
|
for _, fn := range updateFunctions {
|
||||||
|
updates = append(updates, fn()...)
|
||||||
|
}
|
||||||
|
return updates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareLabelNames(labelName1, labelName2 []string) bool {
|
||||||
|
name1 := strings.Join(labelName1, "")
|
||||||
|
name2 := strings.Join(labelName2, "")
|
||||||
|
return name1 == name2
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashLabelPaths(path []string) string {
|
||||||
|
return algo.HashBase64SHA256(strings.Join(path, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalLabelConflictResolver interface {
|
||||||
|
ResolveConflict(ctx context.Context, apiLabels map[string]proton.Label) (func() []imap.Update, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type internalLabelConflictResolverImpl struct {
|
||||||
|
mailboxFetch mailboxFetcherFn
|
||||||
|
mailboxMessageCountFetch mailboxMessageCountFetcherFn
|
||||||
|
userLabelConflictResolver LabelConflictResolver
|
||||||
|
allowNonEmptyMailboxDeletion bool
|
||||||
|
client apiClient
|
||||||
|
reporter sentryReporter
|
||||||
|
log *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
type nullInternalLabelConflictResolver struct{}
|
||||||
|
|
||||||
|
func (r *nullInternalLabelConflictResolver) ResolveConflict(_ context.Context, _ map[string]proton.Label) (func() []imap.Update, error) {
|
||||||
|
return func() []imap.Update { return []imap.Update{} }, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LabelConflictManager) NewInternalLabelConflictResolver(connectors []*Connector) InternalLabelConflictResolver {
|
||||||
|
if m.featureFlagProvider.GetFlagValue(unleash.InternalLabelConflictResolverDisabled) {
|
||||||
|
return &nullInternalLabelConflictResolver{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &internalLabelConflictResolverImpl{
|
||||||
|
mailboxFetch: m.generateMailboxFetcher(connectors),
|
||||||
|
mailboxMessageCountFetch: m.generateMailboxMessageCountFetcher(connectors),
|
||||||
|
userLabelConflictResolver: m.NewConflictResolver(connectors),
|
||||||
|
allowNonEmptyMailboxDeletion: m.featureFlagProvider.GetFlagValue(unleash.ItnternalLabelConflictNonEmptyMailboxDeletion),
|
||||||
|
client: m.client,
|
||||||
|
reporter: m.reporter,
|
||||||
|
log: logrus.WithFields(logrus.Fields{
|
||||||
|
"pkg": "imapservice/internalLabelConflictResolver",
|
||||||
|
"numberOfConnectors": len(connectors),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *internalLabelConflictResolverImpl) ResolveConflict(ctx context.Context, apiLabels map[string]proton.Label) (func() []imap.Update, error) {
|
||||||
|
updateFns := []func() []imap.Update{}
|
||||||
|
|
||||||
|
for _, prefix := range []string{folderPrefix, labelPrefix} {
|
||||||
|
internalLabel := proton.Label{
|
||||||
|
Path: []string{prefix},
|
||||||
|
ID: prefix,
|
||||||
|
Name: prefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
mbox, err := r.mailboxFetch(ctx, internalLabel)
|
||||||
|
if err != nil {
|
||||||
|
if db.IsErrNotFound(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the ID's match then we don't have a discrepancy.
|
||||||
|
if mbox.RemoteID == internalLabel.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logFields := logrus.Fields{
|
||||||
|
"internalLabelID": internalLabel.ID,
|
||||||
|
"internalLabelName": internalLabel.Name,
|
||||||
|
"conflictingLabelID": mbox.RemoteID,
|
||||||
|
"conflictingLabelName": strings.Join(mbox.BridgeName, "/"),
|
||||||
|
}
|
||||||
|
reporterContext := reporter.Context(logFields)
|
||||||
|
logger := r.log.WithFields(logFields)
|
||||||
|
logger.Info("Encountered conflict, resolving.")
|
||||||
|
|
||||||
|
// There is a discrepancy, let's see if it comes from API.
|
||||||
|
apiLabel, ok := apiLabels[mbox.RemoteID]
|
||||||
|
if !ok {
|
||||||
|
// Label does not come from API, we should delete it.
|
||||||
|
// Due diligence, check if there are any messages associated with the mailbox.
|
||||||
|
msgCount, _ := r.mailboxMessageCountFetch(ctx, mbox.InternalID)
|
||||||
|
if msgCount != 0 {
|
||||||
|
logger.WithField("conflictingLabelMessageCount", msgCount).Info("Non-API conflicting label has associated messages")
|
||||||
|
|
||||||
|
reporterContext["conflictingLabelMessageCount"] = msgCount
|
||||||
|
if rerr := r.reporter.ReportWarningWithContext("Internal mailbox name conflict. Conflicting non-API label has messages.",
|
||||||
|
reporterContext); rerr != nil {
|
||||||
|
logger.WithError(rerr).Error("Failed to send report to sentry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.allowNonEmptyMailboxDeletion {
|
||||||
|
return combineIMAPUpdateFns(updateFns), fmt.Errorf("internal mailbox conflicting non-api label has associated messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := func() []imap.Update {
|
||||||
|
return []imap.Update{imap.NewMailboxDeletedSilent(imap.MailboxID(mbox.RemoteID))}
|
||||||
|
}
|
||||||
|
updateFns = append(updateFns, fn)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reporterContext["conflictingLabelType"] = apiLabel.Type
|
||||||
|
|
||||||
|
// Label is indeed from API let's see if it's name has changed.
|
||||||
|
if compareLabelNames(GetMailboxName(apiLabel), internalLabel.Path) {
|
||||||
|
logger.Error("Conflict, same-name mailbox is returned by API")
|
||||||
|
|
||||||
|
if err := r.reporter.ReportMessageWithContext("Internal mailbox name conflict. Same-name mailbox is returned by API", reporterContext); err != nil {
|
||||||
|
logger.WithError(err).Error("Could not send report to sentry")
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineIMAPUpdateFns(updateFns), fmt.Errorf("API label %s conflicts with internal label %s",
|
||||||
|
GetMailboxName(apiLabel),
|
||||||
|
strings.Join(mbox.BridgeName, "/"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's name has changed then we ought to rename it while still taking care of potential conflicts.
|
||||||
|
labelRenameUpdates, err := r.userLabelConflictResolver.ResolveConflict(ctx, apiLabel, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
reporterContext["err"] = err.Error()
|
||||||
|
if rerr := r.reporter.ReportMessageWithContext("Failed to resolve internal mailbox conflict", reporterContext); rerr != nil {
|
||||||
|
logger.WithError(rerr).Error("Could not send report to sentry")
|
||||||
|
}
|
||||||
|
return combineIMAPUpdateFns(updateFns),
|
||||||
|
fmt.Errorf("failed to resolve user label conflict for '%s': %w", apiLabel.Name, err)
|
||||||
|
}
|
||||||
|
updateFns = append(updateFns, labelRenameUpdates)
|
||||||
|
}
|
||||||
|
return combineIMAPUpdateFns(updateFns), nil
|
||||||
|
}
|
||||||
961
internal/services/imapservice/conflicts_test.go
Normal file
961
internal/services/imapservice/conflicts_test.go
Normal file
@ -0,0 +1,961 @@
|
|||||||
|
// Copyright (c) 2025 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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/db"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ffProviderFalse struct{}
|
||||||
|
type ffProviderTrue struct{}
|
||||||
|
|
||||||
|
func (f ffProviderFalse) GetFlagValue(_ string) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f ffProviderTrue) GetFlagValue(_ string) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockLabelNameProvider struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLabelNameProvider) GetUserMailboxByName(ctx context.Context, addrID string, labelName []string) (imap.MailboxData, error) {
|
||||||
|
args := m.Called(ctx, addrID, labelName)
|
||||||
|
v, ok := args.Get(0).(imap.MailboxData)
|
||||||
|
if !ok {
|
||||||
|
return imap.MailboxData{}, fmt.Errorf("failed to assert type")
|
||||||
|
}
|
||||||
|
return v, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockIDProvider struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockIDProvider) GetGluonID(addrID string) (string, bool) {
|
||||||
|
args := m.Called(addrID)
|
||||||
|
return args.String(0), args.Bool(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockAPIClient struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAPIClient) GetLabel(ctx context.Context, id string, types ...proton.LabelType) (proton.Label, error) {
|
||||||
|
args := m.Called(ctx, id, types)
|
||||||
|
v, ok := args.Get(0).(proton.Label)
|
||||||
|
if !ok {
|
||||||
|
return proton.Label{}, fmt.Errorf("failed to assert type")
|
||||||
|
}
|
||||||
|
return v, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockReporter struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReporter) ReportMessageWithContext(msg string, ctx reporter.Context) error {
|
||||||
|
args := m.Called(msg, ctx)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReporter) ReportWarningWithContext(msg string, ctx reporter.Context) error {
|
||||||
|
args := m.Called(msg, ctx)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConflict_UnexpectedLabelConflict(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
label := proton.Label{
|
||||||
|
ID: "label-1",
|
||||||
|
Path: []string{"Work"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
}
|
||||||
|
conflictingLabel := proton.Label{
|
||||||
|
ID: "label-2",
|
||||||
|
Path: []string{"Work"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
}
|
||||||
|
conflictMbox := imap.MailboxData{
|
||||||
|
RemoteID: "label-2",
|
||||||
|
BridgeName: []string{"Labels", "Work"},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id", imapservice.GetMailboxName(label)).
|
||||||
|
Return(conflictMbox, nil)
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id", true)
|
||||||
|
mockClient.On("GetLabel", mock.Anything, "label-2", mock.Anything).
|
||||||
|
Return(conflictingLabel, nil)
|
||||||
|
mockReporter.On("ReportMessageWithContext", "Unexpected label conflict", mock.Anything).
|
||||||
|
Return(nil)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}).
|
||||||
|
NewConflictResolver([]*imapservice.Connector{connector})
|
||||||
|
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
_, err := resolver.ResolveConflict(ctx, label, visited)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unexpected label conflict")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveDiscrepancy_LabelDoesNotExist(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
label := proton.Label{
|
||||||
|
ID: "label-id-1",
|
||||||
|
Name: "Inbox",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", imapservice.GetMailboxName(label)).
|
||||||
|
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewConflictResolver(connectors)
|
||||||
|
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, label, visited)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
updates := fn()
|
||||||
|
assert.Len(t, updates, 1)
|
||||||
|
muc, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(label.ID), muc.Mailbox.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConflict_MailboxFetchError(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
label := proton.Label{
|
||||||
|
ID: "111",
|
||||||
|
Path: []string{"Work"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id", imapservice.GetMailboxName(label)).
|
||||||
|
Return(imap.MailboxData{}, errors.New("database connection error"))
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id", true)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}).
|
||||||
|
NewConflictResolver([]*imapservice.Connector{connector})
|
||||||
|
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
_, err := resolver.ResolveConflict(ctx, label, visited)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "database connection error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveDiscrepancy_ConflictingLabelDeletedRemotely(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
label := proton.Label{
|
||||||
|
ID: "label-new",
|
||||||
|
Path: []string{"Work"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
}
|
||||||
|
conflictMbox := imap.MailboxData{
|
||||||
|
RemoteID: "label-old",
|
||||||
|
BridgeName: []string{"Labels", "Work"},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", imapservice.GetMailboxName(label)).
|
||||||
|
Return(conflictMbox, nil)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
mockClient.On("GetLabel", mock.Anything, "label-old", mock.Anything).
|
||||||
|
Return(proton.Label{}, proton.ErrNoSuchLabel)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewConflictResolver(connectors)
|
||||||
|
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, label, visited)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
updates := fn()
|
||||||
|
assert.Len(t, updates, 2)
|
||||||
|
deleted, ok := updates[0].(*imap.MailboxDeleted)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID("label-old"), deleted.MailboxID)
|
||||||
|
|
||||||
|
updated, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "Work", updated.Mailbox.Name[len(updated.Mailbox.Name)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveDiscrepancy_LabelAlreadyCorrect(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
label := proton.Label{
|
||||||
|
ID: "label-id-1",
|
||||||
|
Name: "Personal",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
}
|
||||||
|
mbox := imap.MailboxData{
|
||||||
|
RemoteID: "label-id-1",
|
||||||
|
BridgeName: []string{"Labels", "Personal"},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", imapservice.GetMailboxName(label)).
|
||||||
|
Return(mbox, nil)
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewConflictResolver(connectors)
|
||||||
|
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, label, visited)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, fn(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConflict_DeepNestedPath(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
label := proton.Label{
|
||||||
|
ID: "111",
|
||||||
|
Path: []string{"Level1", "Level2", "Level3", "DeepFolder"},
|
||||||
|
Type: proton.LabelTypeFolder,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id", imapservice.GetMailboxName(label)).
|
||||||
|
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id", true)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
resolver := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{}).
|
||||||
|
NewConflictResolver([]*imapservice.Connector{connector})
|
||||||
|
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, label, visited)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
updates := fn()
|
||||||
|
assert.Len(t, updates, 1)
|
||||||
|
|
||||||
|
updated, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID("111"), updated.Mailbox.ID)
|
||||||
|
expectedName := imapservice.GetMailboxName(label)
|
||||||
|
assert.Equal(t, expectedName, updated.Mailbox.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveLabelDiscrepancy_LabelSwap(t *testing.T) {
|
||||||
|
apiLabels := []proton.Label{
|
||||||
|
{
|
||||||
|
ID: "111",
|
||||||
|
Path: []string{"X"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "222",
|
||||||
|
Path: []string{"Y"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonLabels := []imap.MailboxData{
|
||||||
|
{
|
||||||
|
RemoteID: "111",
|
||||||
|
BridgeName: []string{"Labels", "Y"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RemoteID: "222",
|
||||||
|
BridgeName: []string{"Labels", "X"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
for _, mbox := range gluonLabels {
|
||||||
|
mockLabelProvider.
|
||||||
|
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||||
|
Return(mbox, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range apiLabels {
|
||||||
|
mockClient.
|
||||||
|
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
|
||||||
|
Return(label, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewConflictResolver(connectors)
|
||||||
|
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], visited)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.NotEmpty(t, updates)
|
||||||
|
assert.Equal(t, 3, len(updates)) // We expect three calls to be made for a swap operation.
|
||||||
|
|
||||||
|
updateOne, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateOne.Mailbox.ID)
|
||||||
|
assert.Equal(t, "tmp_X", updateOne.Mailbox.Name[len(updateOne.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateTwo, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[1].ID), updateTwo.Mailbox.ID)
|
||||||
|
assert.Equal(t, "Y", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateThree, ok := updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateThree.Mailbox.ID)
|
||||||
|
assert.Equal(t, "X", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveLabelDiscrepancy_LabelSwapExtended(t *testing.T) {
|
||||||
|
apiLabels := []proton.Label{
|
||||||
|
{
|
||||||
|
ID: "111",
|
||||||
|
Path: []string{"X"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "222",
|
||||||
|
Path: []string{"Y"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "333",
|
||||||
|
Path: []string{"Z"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "444",
|
||||||
|
Path: []string{"D"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonLabels := []imap.MailboxData{
|
||||||
|
{
|
||||||
|
RemoteID: "111",
|
||||||
|
BridgeName: []string{"Labels", "D"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RemoteID: "222",
|
||||||
|
BridgeName: []string{"Labels", "Z"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RemoteID: "333",
|
||||||
|
BridgeName: []string{"Labels", "Y"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RemoteID: "444",
|
||||||
|
BridgeName: []string{"Labels", "X"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
for _, mbox := range gluonLabels {
|
||||||
|
mockLabelProvider.
|
||||||
|
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||||
|
Return(mbox, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range apiLabels {
|
||||||
|
mockClient.
|
||||||
|
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
|
||||||
|
Return(label, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewConflictResolver(connectors)
|
||||||
|
|
||||||
|
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], make(map[string]bool))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.NotEmpty(t, updates)
|
||||||
|
// Three calls yet again for a swap operation.
|
||||||
|
assert.Equal(t, 3, len(updates))
|
||||||
|
updateOne, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateOne.Mailbox.ID)
|
||||||
|
assert.Equal(t, "tmp_X", updateOne.Mailbox.Name[len(updateOne.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateTwo, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[3].ID), updateTwo.Mailbox.ID)
|
||||||
|
assert.Equal(t, "D", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateThree, ok := updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateThree.Mailbox.ID)
|
||||||
|
assert.Equal(t, "X", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
// Fix the secondary swap.
|
||||||
|
fn, err = resolver.ResolveConflict(context.Background(), apiLabels[1], make(map[string]bool))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
updates = fn()
|
||||||
|
assert.Equal(t, 3, len(updates))
|
||||||
|
updateOne, ok = updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[1].ID), updateOne.Mailbox.ID)
|
||||||
|
assert.Equal(t, "tmp_Y", updateOne.Mailbox.Name[len(updateOne.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateTwo, ok = updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[2].ID), updateTwo.Mailbox.ID)
|
||||||
|
assert.Equal(t, "Z", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateThree, ok = updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[1].ID), updateThree.Mailbox.ID)
|
||||||
|
assert.Equal(t, "Y", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveLabelDiscrepancy_LabelSwapCyclic(t *testing.T) {
|
||||||
|
apiLabels := []proton.Label{
|
||||||
|
{ID: "111", Path: []string{"A"}, Type: proton.LabelTypeLabel},
|
||||||
|
{ID: "222", Path: []string{"B"}, Type: proton.LabelTypeLabel},
|
||||||
|
{ID: "333", Path: []string{"C"}, Type: proton.LabelTypeLabel},
|
||||||
|
{ID: "444", Path: []string{"D"}, Type: proton.LabelTypeLabel},
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonLabels := []imap.MailboxData{
|
||||||
|
{RemoteID: "111", BridgeName: []string{"Labels", "D"}}, // A <- D
|
||||||
|
{RemoteID: "222", BridgeName: []string{"Labels", "A"}}, // B <- A
|
||||||
|
{RemoteID: "333", BridgeName: []string{"Labels", "B"}}, // C <- B
|
||||||
|
{RemoteID: "444", BridgeName: []string{"Labels", "C"}}, // D <- C
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
for _, mbox := range gluonLabels {
|
||||||
|
mockLabelProvider.
|
||||||
|
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||||
|
Return(mbox, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range apiLabels {
|
||||||
|
mockClient.
|
||||||
|
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
|
||||||
|
Return(label, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewConflictResolver(connectors)
|
||||||
|
|
||||||
|
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[0], make(map[string]bool))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.NotEmpty(t, updates)
|
||||||
|
assert.Equal(t, 5, len(updates))
|
||||||
|
|
||||||
|
updateOne, ok := updates[0].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateOne.Mailbox.ID)
|
||||||
|
assert.Equal(t, "tmp_A", updateOne.Mailbox.Name[len(updateOne.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateTwo, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[3].ID), updateTwo.Mailbox.ID)
|
||||||
|
assert.Equal(t, "D", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateThree, ok := updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[2].ID), updateThree.Mailbox.ID)
|
||||||
|
assert.Equal(t, "C", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateFour, ok := updates[3].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[1].ID), updateFour.Mailbox.ID)
|
||||||
|
assert.Equal(t, "B", updateFour.Mailbox.Name[len(updateFour.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateFive, ok := updates[4].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateFive.Mailbox.ID)
|
||||||
|
assert.Equal(t, "A", updateFive.Mailbox.Name[len(updateFive.Mailbox.Name)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveLabelDiscrepancy_LabelSwapCyclicWithDeletedLabel(t *testing.T) {
|
||||||
|
apiLabels := []proton.Label{
|
||||||
|
{ID: "111", Path: []string{"A"}, Type: proton.LabelTypeLabel},
|
||||||
|
{ID: "333", Path: []string{"C"}, Type: proton.LabelTypeLabel},
|
||||||
|
{ID: "444", Path: []string{"D"}, Type: proton.LabelTypeLabel},
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonLabels := []imap.MailboxData{
|
||||||
|
{RemoteID: "111", BridgeName: []string{"Labels", "D"}},
|
||||||
|
{RemoteID: "222", BridgeName: []string{"Labels", "A"}},
|
||||||
|
{RemoteID: "333", BridgeName: []string{"Labels", "B"}},
|
||||||
|
{RemoteID: "444", BridgeName: []string{"Labels", "C"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
for _, mbox := range gluonLabels {
|
||||||
|
mockLabelProvider.
|
||||||
|
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||||
|
Return(mbox, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range apiLabels {
|
||||||
|
mockClient.
|
||||||
|
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
|
||||||
|
Return(label, nil)
|
||||||
|
}
|
||||||
|
mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).Return(proton.Label{}, proton.ErrNoSuchLabel)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewConflictResolver(connectors)
|
||||||
|
|
||||||
|
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[2], make(map[string]bool))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.NotEmpty(t, updates)
|
||||||
|
assert.Equal(t, 3, len(updates))
|
||||||
|
|
||||||
|
updateOne, ok := updates[0].(*imap.MailboxDeleted)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID("222"), updateOne.MailboxID)
|
||||||
|
|
||||||
|
updateTwo, ok := updates[1].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[0].ID), updateTwo.Mailbox.ID)
|
||||||
|
assert.Equal(t, "A", updateTwo.Mailbox.Name[len(updateTwo.Mailbox.Name)-1])
|
||||||
|
|
||||||
|
updateThree, ok := updates[2].(*imap.MailboxUpdatedOrCreated)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID(apiLabels[2].ID), updateThree.Mailbox.ID)
|
||||||
|
assert.Equal(t, "D", updateThree.Mailbox.Name[len(updateThree.Mailbox.Name)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveLabelDiscrepancy_LabelSwapCyclicWithDeletedLabel_KillSwitchEnabled(t *testing.T) {
|
||||||
|
apiLabels := []proton.Label{
|
||||||
|
{ID: "111", Path: []string{"A"}, Type: proton.LabelTypeLabel},
|
||||||
|
{ID: "333", Path: []string{"C"}, Type: proton.LabelTypeLabel},
|
||||||
|
{ID: "444", Path: []string{"D"}, Type: proton.LabelTypeLabel},
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonLabels := []imap.MailboxData{
|
||||||
|
{RemoteID: "111", BridgeName: []string{"Labels", "D"}},
|
||||||
|
{RemoteID: "222", BridgeName: []string{"Labels", "A"}},
|
||||||
|
{RemoteID: "333", BridgeName: []string{"Labels", "B"}},
|
||||||
|
{RemoteID: "444", BridgeName: []string{"Labels", "C"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
for _, mbox := range gluonLabels {
|
||||||
|
mockLabelProvider.
|
||||||
|
On("GetUserMailboxByName", mock.Anything, "gluon-id-1", mbox.BridgeName).
|
||||||
|
Return(mbox, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range apiLabels {
|
||||||
|
mockClient.
|
||||||
|
On("GetLabel", mock.Anything, label.ID, []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).
|
||||||
|
Return(label, nil)
|
||||||
|
}
|
||||||
|
mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel, proton.LabelTypeSystem}).Return(proton.Label{}, proton.ErrNoSuchLabel)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderTrue{})
|
||||||
|
resolver := manager.NewConflictResolver(connectors)
|
||||||
|
|
||||||
|
fn, err := resolver.ResolveConflict(context.Background(), apiLabels[2], make(map[string]bool))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.Empty(t, updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalLabelConflictResolver_NoConflicts(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
|
||||||
|
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
|
||||||
|
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewInternalLabelConflictResolver(connectors)
|
||||||
|
|
||||||
|
apiLabels := make(map[string]proton.Label)
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, apiLabels)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.Empty(t, updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalLabelConflictResolver_CorrectIDs(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
|
||||||
|
Return(imap.MailboxData{RemoteID: "Folders", BridgeName: []string{"Folders"}}, nil)
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
|
||||||
|
Return(imap.MailboxData{RemoteID: "Labels", BridgeName: []string{"Labels"}}, nil)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewInternalLabelConflictResolver(connectors)
|
||||||
|
|
||||||
|
apiLabels := make(map[string]proton.Label)
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, apiLabels)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.Empty(t, updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockMailboxCountProvider struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMailboxCountProvider) GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error) {
|
||||||
|
args := m.Called(ctx, addrID, internalID)
|
||||||
|
return args.Int(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalLabelConflictResolver_ConflictingNonAPILabel_ZeroCount(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
mockCountProvider := new(mockMailboxCountProvider)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
// Mock mailbox fetch to return conflicting mailbox
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
|
||||||
|
Return(imap.MailboxData{RemoteID: "wrong-id", BridgeName: []string{"Folders"}, InternalID: imap.InternalMailboxID(123)}, nil)
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
|
||||||
|
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||||
|
|
||||||
|
// Mock message count fetch to return 0 messages.
|
||||||
|
mockLabelProvider.On("GetMailboxMessageCount", mock.Anything, "gluon-id-1", imap.InternalMailboxID(123)).
|
||||||
|
Return(0, nil)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
mockCountProvider.On("GetUserMailboxCountByInternalID",
|
||||||
|
mock.Anything,
|
||||||
|
"addr-1",
|
||||||
|
imap.InternalMailboxID(123)).
|
||||||
|
Return(0, nil)
|
||||||
|
|
||||||
|
connector.SetMailboxCountProviderTest(mockCountProvider)
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewInternalLabelConflictResolver(connectors)
|
||||||
|
|
||||||
|
// API labels don't contain the conflicting label ID
|
||||||
|
apiLabels := make(map[string]proton.Label)
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, apiLabels)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.Len(t, updates, 1)
|
||||||
|
|
||||||
|
deleted, ok := updates[0].(*imap.MailboxDeletedSilent)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, imap.MailboxID("wrong-id"), deleted.MailboxID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalLabelConflictResolver_ConflictingNonAPILabel_PositiveCount(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
mockCountProvider := new(mockMailboxCountProvider)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
mockReporter.On("ReportWarningWithContext", mock.Anything, mock.Anything).
|
||||||
|
Return(nil)
|
||||||
|
|
||||||
|
// Mock mailbox fetch to return conflicting mailbox
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
|
||||||
|
Return(imap.MailboxData{RemoteID: "wrong-id", BridgeName: []string{"Folders"}, InternalID: imap.InternalMailboxID(123)}, nil)
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
|
||||||
|
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||||
|
|
||||||
|
// Mock message count fetch to return 0 messages.
|
||||||
|
mockLabelProvider.On("GetMailboxMessageCount", mock.Anything, "gluon-id-1", imap.InternalMailboxID(123)).
|
||||||
|
Return(0, nil)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
mockCountProvider.On("GetUserMailboxCountByInternalID",
|
||||||
|
mock.Anything,
|
||||||
|
"addr-1",
|
||||||
|
imap.InternalMailboxID(123)).
|
||||||
|
Return(10, nil)
|
||||||
|
|
||||||
|
connector.SetMailboxCountProviderTest(mockCountProvider)
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewInternalLabelConflictResolver(connectors)
|
||||||
|
|
||||||
|
// API labels don't contain the conflicting label ID
|
||||||
|
apiLabels := make(map[string]proton.Label)
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, apiLabels)
|
||||||
|
assert.EqualError(t, err, "internal mailbox conflicting non-api label has associated messages")
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.Empty(t, updates, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalLabelConflictResolver_ConflictingAPILabelSameName(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
mockCountProvider := new(mockMailboxCountProvider)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
|
||||||
|
Return(imap.MailboxData{RemoteID: "api-label-id", BridgeName: []string{"Folders"}}, nil)
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
|
||||||
|
Return(imap.MailboxData{}, db.ErrNotFound)
|
||||||
|
|
||||||
|
mockReporter.On("ReportMessageWithContext", "Internal mailbox name conflict. Same-name mailbox is returned by API", mock.Anything).
|
||||||
|
Return(nil)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connector.SetMailboxCountProviderTest(mockCountProvider)
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewInternalLabelConflictResolver(connectors)
|
||||||
|
|
||||||
|
// API user label with empty path.
|
||||||
|
apiLabels := map[string]proton.Label{
|
||||||
|
"api-label-id": {
|
||||||
|
ID: "api-label-id",
|
||||||
|
Name: "Folders",
|
||||||
|
Path: []string{""},
|
||||||
|
Type: proton.LabelTypeFolder,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := resolver.ResolveConflict(ctx, apiLabels)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "API label")
|
||||||
|
assert.Contains(t, err.Error(), "conflicts with internal label")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalLabelConflictResolver_MailboxFetchError(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
|
||||||
|
Return(imap.MailboxData{}, errors.New("database connection error"))
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderFalse{})
|
||||||
|
resolver := manager.NewInternalLabelConflictResolver(connectors)
|
||||||
|
|
||||||
|
apiLabels := make(map[string]proton.Label)
|
||||||
|
_, err := resolver.ResolveConflict(ctx, apiLabels)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "database connection error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewInternalLabelConflictResolver_KillSwitchEnabled(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockLabelProvider := new(mockLabelNameProvider)
|
||||||
|
mockClient := new(mockAPIClient)
|
||||||
|
mockIDProvider := new(mockIDProvider)
|
||||||
|
mockReporter := new(mockReporter)
|
||||||
|
|
||||||
|
mockIDProvider.On("GetGluonID", "addr-1").Return("gluon-id-1", true)
|
||||||
|
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Folders"}).
|
||||||
|
Return(imap.MailboxData{RemoteID: "wrong-folders-id", BridgeName: []string{"Folders"}}, nil)
|
||||||
|
mockLabelProvider.On("GetUserMailboxByName", mock.Anything, "gluon-id-1", []string{"Labels"}).
|
||||||
|
Return(imap.MailboxData{RemoteID: "wrong-labels-id", BridgeName: []string{"Labels"}}, nil)
|
||||||
|
|
||||||
|
connector := &imapservice.Connector{}
|
||||||
|
connector.SetAddrIDTest("addr-1")
|
||||||
|
connectors := []*imapservice.Connector{connector}
|
||||||
|
|
||||||
|
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, ffProviderTrue{})
|
||||||
|
resolver := manager.NewInternalLabelConflictResolver(connectors)
|
||||||
|
|
||||||
|
apiLabels := map[string]proton.Label{
|
||||||
|
"some-api-label": {
|
||||||
|
ID: "some-api-label",
|
||||||
|
Name: "SomeLabel",
|
||||||
|
Path: []string{"SomeLabel"},
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn, err := resolver.ResolveConflict(ctx, apiLabels)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
updates := fn()
|
||||||
|
assert.Empty(t, updates)
|
||||||
|
}
|
||||||
@ -45,6 +45,10 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type mailboxCountProvider interface {
|
||||||
|
GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Connector contains all IMAP state required to satisfy sync and or imap queries.
|
// Connector contains all IMAP state required to satisfy sync and or imap queries.
|
||||||
type Connector struct {
|
type Connector struct {
|
||||||
addrID string
|
addrID string
|
||||||
@ -67,6 +71,8 @@ type Connector struct {
|
|||||||
|
|
||||||
sharedCache *SharedCache
|
sharedCache *SharedCache
|
||||||
syncState *SyncState
|
syncState *SyncState
|
||||||
|
|
||||||
|
mailboxCountProvider mailboxCountProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
var errNoSenderAddressMatch = errors.New("no matching sender found in address list")
|
var errNoSenderAddressMatch = errors.New("no matching sender found in address list")
|
||||||
@ -82,6 +88,7 @@ func NewConnector(
|
|||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
showAllMail bool,
|
showAllMail bool,
|
||||||
syncState *SyncState,
|
syncState *SyncState,
|
||||||
|
mailboxCountProvider mailboxCountProvider,
|
||||||
) *Connector {
|
) *Connector {
|
||||||
userID := identityState.UserID()
|
userID := identityState.UserID()
|
||||||
|
|
||||||
@ -115,6 +122,8 @@ func NewConnector(
|
|||||||
|
|
||||||
sharedCache: NewSharedCached(),
|
sharedCache: NewSharedCached(),
|
||||||
syncState: syncState,
|
syncState: syncState,
|
||||||
|
|
||||||
|
mailboxCountProvider: mailboxCountProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -800,8 +809,10 @@ func (s *Connector) createDraftWithParser(ctx context.Context, parser *parser.Pa
|
|||||||
return draft, nil
|
return draft, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Connector) publishUpdate(_ context.Context, update imap.Update) {
|
func (s *Connector) publishUpdate(_ context.Context, updates ...imap.Update) {
|
||||||
s.updateCh.Enqueue(update)
|
for _, update := range updates {
|
||||||
|
s.updateCh.Enqueue(update)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fixGODT3003Labels(
|
func fixGODT3003Labels(
|
||||||
@ -903,3 +914,16 @@ func (s *Connector) getSenderProtonAddress(p *parser.Parser) (proton.Address, er
|
|||||||
|
|
||||||
return addressList[index], nil
|
return addressList[index], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Connector) SetAddrIDTest(addrID string) {
|
||||||
|
s.addrID = addrID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connector) GetMailboxMessageCount(ctx context.Context, mailboxInternalID imap.InternalMailboxID) (int, error) {
|
||||||
|
return s.mailboxCountProvider.GetUserMailboxCountByInternalID(ctx, s.addrID, mailboxInternalID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMailboxCountProviderTest - sets the relevant provider. Should only be used for testing.
|
||||||
|
func (s *Connector) SetMailboxCountProviderTest(provider mailboxCountProvider) {
|
||||||
|
s.mailboxCountProvider = provider
|
||||||
|
}
|
||||||
|
|||||||
@ -102,6 +102,16 @@ func newMailboxCreatedUpdate(labelID imap.MailboxID, labelName []string) *imap.M
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newMailboxUpdatedOrCreated(labelID imap.MailboxID, labelName []string) *imap.MailboxUpdatedOrCreated {
|
||||||
|
return imap.NewMailboxUpdatedOrCreated(imap.Mailbox{
|
||||||
|
ID: labelID,
|
||||||
|
Name: labelName,
|
||||||
|
Flags: defaultMailboxFlags(),
|
||||||
|
PermanentFlags: defaultMailboxPermanentFlags(),
|
||||||
|
Attributes: imap.NewFlagSet(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func GetMailboxName(label proton.Label) []string {
|
func GetMailboxName(label proton.Label) []string {
|
||||||
var name []string
|
var name []string
|
||||||
|
|
||||||
@ -112,9 +122,10 @@ func GetMailboxName(label proton.Label) []string {
|
|||||||
case proton.LabelTypeLabel:
|
case proton.LabelTypeLabel:
|
||||||
name = append([]string{labelPrefix}, label.Path...)
|
name = append([]string{labelPrefix}, label.Path...)
|
||||||
|
|
||||||
case proton.LabelTypeContactGroup:
|
|
||||||
fallthrough
|
|
||||||
case proton.LabelTypeSystem:
|
case proton.LabelTypeSystem:
|
||||||
|
name = []string{label.Name}
|
||||||
|
|
||||||
|
case proton.LabelTypeContactGroup:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
name = label.Path
|
name = label.Path
|
||||||
@ -122,3 +133,12 @@ func GetMailboxName(label proton.Label) []string {
|
|||||||
|
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nameWithTempPrefix(path []string) []string {
|
||||||
|
path[len(path)-1] = "tmp_" + path[len(path)-1]
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMailboxNameWithTempPrefix(label proton.Label) []string {
|
||||||
|
return nameWithTempPrefix(GetMailboxName(label))
|
||||||
|
}
|
||||||
|
|||||||
223
internal/services/imapservice/labelchecker.go
Normal file
223
internal/services/imapservice/labelchecker.go
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
// Copyright (c) 2025 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"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/db"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type labelDiscrepancyType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
discrepancyInternal labelDiscrepancyType = iota
|
||||||
|
discrepancySystem
|
||||||
|
discrepancyUser
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t labelDiscrepancyType) String() string {
|
||||||
|
switch t {
|
||||||
|
case discrepancyInternal:
|
||||||
|
return "internal"
|
||||||
|
case discrepancySystem:
|
||||||
|
return "system"
|
||||||
|
case discrepancyUser:
|
||||||
|
return "user"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type labelDiscrepancy struct {
|
||||||
|
labelName string
|
||||||
|
labelPath string
|
||||||
|
labelPathParsed string
|
||||||
|
labelID string
|
||||||
|
conflictingLabelName string
|
||||||
|
conflictingLabelID string
|
||||||
|
Type labelDiscrepancyType
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinStrings(input []string) string {
|
||||||
|
return strings.Join(input, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLabelDiscrepancy(label proton.Label, mbox imap.MailboxData, dType labelDiscrepancyType) labelDiscrepancy {
|
||||||
|
discrepancy := labelDiscrepancy{
|
||||||
|
labelName: label.Name,
|
||||||
|
labelID: label.ID,
|
||||||
|
conflictingLabelID: mbox.RemoteID,
|
||||||
|
Type: dType,
|
||||||
|
}
|
||||||
|
|
||||||
|
if dType == discrepancyUser {
|
||||||
|
discrepancy.labelName = algo.HashBase64SHA256(label.Name)
|
||||||
|
discrepancy.labelPath = algo.HashBase64SHA256(joinStrings(label.Path))
|
||||||
|
discrepancy.labelPathParsed = algo.HashBase64SHA256(joinStrings(GetMailboxName(label)))
|
||||||
|
discrepancy.conflictingLabelName = algo.HashBase64SHA256(joinStrings(mbox.BridgeName))
|
||||||
|
} else {
|
||||||
|
discrepancy.labelName = label.Name
|
||||||
|
discrepancy.labelPath = joinStrings(label.Path)
|
||||||
|
discrepancy.labelPathParsed = joinStrings(GetMailboxName(label))
|
||||||
|
discrepancy.conflictingLabelName = joinStrings(mbox.BridgeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return discrepancy
|
||||||
|
}
|
||||||
|
|
||||||
|
func discrepanciesToContext(discrepancies []labelDiscrepancy) reporter.Context {
|
||||||
|
ctx := make(reporter.Context)
|
||||||
|
|
||||||
|
for i, d := range discrepancies {
|
||||||
|
prefix := fmt.Sprintf("discrepancy_%d_", i)
|
||||||
|
|
||||||
|
ctx[prefix+"type"] = d.Type.String()
|
||||||
|
ctx[prefix+"label_id"] = d.labelID
|
||||||
|
ctx[prefix+"label_name"] = d.labelName
|
||||||
|
ctx[prefix+"label_path"] = d.labelPath
|
||||||
|
ctx[prefix+"label_path_parsed"] = d.labelPathParsed
|
||||||
|
ctx[prefix+"conflicting_label_name"] = d.conflictingLabelName
|
||||||
|
ctx[prefix+"conflicting_label_id"] = d.conflictingLabelID
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx["discrepancy_count"] = len(discrepancies)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectorGetter interface {
|
||||||
|
getConnectors() []*Connector
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabelConflictChecker struct {
|
||||||
|
gluonLabelNameProvider GluonLabelNameProvider
|
||||||
|
gluonIDProvider gluonIDProvider
|
||||||
|
connectorGetter ConnectorGetter
|
||||||
|
reporter reporter.Reporter
|
||||||
|
logger *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConflictChecker(connectorGetter ConnectorGetter, reporter reporter.Reporter, provider gluonIDProvider, nameProvider GluonLabelNameProvider) *LabelConflictChecker {
|
||||||
|
return &LabelConflictChecker{
|
||||||
|
gluonLabelNameProvider: nameProvider,
|
||||||
|
gluonIDProvider: provider,
|
||||||
|
connectorGetter: connectorGetter,
|
||||||
|
reporter: reporter,
|
||||||
|
logger: logrus.WithFields(logrus.Fields{
|
||||||
|
"pkg": "imapservice/labelConflictChecker",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LabelConflictChecker) getFn() mailboxFetcherFn {
|
||||||
|
connectors := c.connectorGetter.getConnectors()
|
||||||
|
|
||||||
|
return func(ctx context.Context, label proton.Label) (imap.MailboxData, error) {
|
||||||
|
for _, updateCh := range connectors {
|
||||||
|
addrID, ok := c.gluonIDProvider.GetGluonID(updateCh.addrID)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return c.gluonLabelNameProvider.GetUserMailboxByName(ctx, addrID, GetMailboxName(label))
|
||||||
|
}
|
||||||
|
return imap.MailboxData{}, errors.New("no gluon connectors found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LabelConflictChecker) CheckAndReportConflicts(ctx context.Context, labels map[string]proton.Label) error {
|
||||||
|
labelDiscrepancies, err := c.checkConflicts(ctx, labels, c.getFn())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(labelDiscrepancies) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reporterCtx := discrepanciesToContext(labelDiscrepancies)
|
||||||
|
if err := c.reporter.ReportMessageWithContext("Found label conflicts on Bridge start", reporterCtx); err != nil {
|
||||||
|
c.logger.WithError(err).Error("Failed to report label conflicts to Sentry")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LabelConflictChecker) checkConflicts(ctx context.Context, labels map[string]proton.Label, mboxFetch mailboxFetcherFn) ([]labelDiscrepancy, error) {
|
||||||
|
discrepancies := []labelDiscrepancy{}
|
||||||
|
|
||||||
|
// Verify bridge internal mailboxes.
|
||||||
|
for _, prefix := range []string{folderPrefix, labelPrefix} {
|
||||||
|
label := proton.Label{
|
||||||
|
Path: []string{prefix},
|
||||||
|
ID: prefix,
|
||||||
|
Name: prefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
mbox, err := mboxFetch(ctx, label)
|
||||||
|
if err != nil {
|
||||||
|
if db.IsErrNotFound(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if mbox.RemoteID != label.ID {
|
||||||
|
discrepancies = append(discrepancies, newLabelDiscrepancy(label, mbox, discrepancyInternal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify system and user mailboxes.
|
||||||
|
for _, label := range labels {
|
||||||
|
if !WantLabel(label) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mbox, err := mboxFetch(ctx, label)
|
||||||
|
if err != nil {
|
||||||
|
if db.IsErrNotFound(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if mbox.RemoteID != label.ID {
|
||||||
|
var dType labelDiscrepancyType
|
||||||
|
switch label.Type {
|
||||||
|
case proton.LabelTypeSystem:
|
||||||
|
dType = discrepancySystem
|
||||||
|
case proton.LabelTypeFolder, proton.LabelTypeLabel:
|
||||||
|
dType = discrepancyUser
|
||||||
|
case proton.LabelTypeContactGroup:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
dType = discrepancySystem
|
||||||
|
}
|
||||||
|
discrepancies = append(discrepancies, newLabelDiscrepancy(label, mbox, dType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return discrepancies, nil
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/connector"
|
"github.com/ProtonMail/gluon/connector"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,6 +37,14 @@ type IMAPServerManager interface {
|
|||||||
RemoveIMAPUser(ctx context.Context, deleteData bool, provider GluonIDProvider, addrID ...string) error
|
RemoveIMAPUser(ctx context.Context, deleteData bool, provider GluonIDProvider, addrID ...string) error
|
||||||
|
|
||||||
LogRemoteLabelIDs(ctx context.Context, provider GluonIDProvider, addrID ...string) error
|
LogRemoteLabelIDs(ctx context.Context, provider GluonIDProvider, addrID ...string) error
|
||||||
|
|
||||||
|
GetUserMailboxByName(ctx context.Context, addrID string, mailboxName []string) (imap.MailboxData, error)
|
||||||
|
|
||||||
|
GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error)
|
||||||
|
|
||||||
|
GetOpenIMAPSessionCount() int
|
||||||
|
|
||||||
|
GetRollingIMAPConnectionCount() int
|
||||||
}
|
}
|
||||||
|
|
||||||
type NullIMAPServerManager struct{}
|
type NullIMAPServerManager struct{}
|
||||||
@ -67,6 +76,22 @@ func (n NullIMAPServerManager) LogRemoteLabelIDs(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n NullIMAPServerManager) GetUserMailboxByName(_ context.Context, _ string, _ []string) (imap.MailboxData, error) {
|
||||||
|
return imap.MailboxData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullIMAPServerManager) GetUserMailboxCountByInternalID(_ context.Context, _ string, _ imap.InternalMailboxID) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullIMAPServerManager) GetOpenIMAPSessionCount() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullIMAPServerManager) GetRollingIMAPConnectionCount() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func NewNullIMAPServerManager() *NullIMAPServerManager {
|
func NewNullIMAPServerManager() *NullIMAPServerManager {
|
||||||
return &NullIMAPServerManager{}
|
return &NullIMAPServerManager{}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -91,7 +92,9 @@ type Service struct {
|
|||||||
lastHandledEventID string
|
lastHandledEventID string
|
||||||
isSyncing atomic.Bool
|
isSyncing atomic.Bool
|
||||||
|
|
||||||
observabilitySender observability.Sender
|
observabilitySender observability.Sender
|
||||||
|
labelConflictManager *LabelConflictManager
|
||||||
|
LabelConflictChecker *LabelConflictChecker
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -112,6 +115,7 @@ func NewService(
|
|||||||
maxSyncMemory uint64,
|
maxSyncMemory uint64,
|
||||||
showAllMail bool,
|
showAllMail bool,
|
||||||
observabilitySender observability.Sender,
|
observabilitySender observability.Sender,
|
||||||
|
featureFlagProvider unleash.FeatureFlagValueProvider,
|
||||||
) *Service {
|
) *Service {
|
||||||
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
||||||
|
|
||||||
@ -121,11 +125,12 @@ func NewService(
|
|||||||
})
|
})
|
||||||
rwIdentity := newRWIdentity(identityState, bridgePassProvider, keyPassProvider)
|
rwIdentity := newRWIdentity(identityState, bridgePassProvider, keyPassProvider)
|
||||||
|
|
||||||
syncUpdateApplier := NewSyncUpdateApplier()
|
labelConflictManager := NewLabelConflictManager(serverManager, gluonIDProvider, client, reporter, featureFlagProvider)
|
||||||
|
syncUpdateApplier := NewSyncUpdateApplier(labelConflictManager)
|
||||||
syncMessageBuilder := NewSyncMessageBuilder(rwIdentity)
|
syncMessageBuilder := NewSyncMessageBuilder(rwIdentity)
|
||||||
syncReporter := newSyncReporter(identityState.User.ID, eventPublisher, time.Second)
|
syncReporter := newSyncReporter(identityState.User.ID, eventPublisher, time.Second)
|
||||||
|
|
||||||
return &Service{
|
service := &Service{
|
||||||
cpc: cpc.NewCPC(),
|
cpc: cpc.NewCPC(),
|
||||||
client: client,
|
client: client,
|
||||||
log: log,
|
log: log,
|
||||||
@ -156,8 +161,12 @@ func NewService(
|
|||||||
syncReporter: syncReporter,
|
syncReporter: syncReporter,
|
||||||
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||||
|
|
||||||
observabilitySender: observabilitySender,
|
observabilitySender: observabilitySender,
|
||||||
|
labelConflictManager: labelConflictManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service.LabelConflictChecker = NewConflictChecker(service, reporter, gluonIDProvider, serverManager)
|
||||||
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Start(
|
func (s *Service) Start(
|
||||||
@ -176,7 +185,14 @@ func (s *Service) Start(
|
|||||||
s.syncStateProvider = syncStateProvider
|
s.syncStateProvider = syncStateProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
s.syncHandler = syncservice.NewHandler(syncRegulator, s.client, s.identityState.UserID(), s.syncStateProvider, s.log, s.panicHandler)
|
s.syncHandler = syncservice.NewHandler(
|
||||||
|
syncRegulator,
|
||||||
|
s.client,
|
||||||
|
s.identityState.UserID(),
|
||||||
|
s.syncStateProvider,
|
||||||
|
s.log,
|
||||||
|
s.panicHandler,
|
||||||
|
s.reporter)
|
||||||
|
|
||||||
// Get user labels
|
// Get user labels
|
||||||
apiLabels, err := s.client.GetLabels(ctx, proton.LabelTypeSystem, proton.LabelTypeFolder, proton.LabelTypeLabel)
|
apiLabels, err := s.client.GetLabels(ctx, proton.LabelTypeSystem, proton.LabelTypeFolder, proton.LabelTypeLabel)
|
||||||
@ -524,6 +540,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
|
|||||||
s.reporter,
|
s.reporter,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
s.syncStateProvider,
|
s.syncStateProvider,
|
||||||
|
s.serverManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
return connectors, nil
|
return connectors, nil
|
||||||
@ -541,6 +558,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
|
|||||||
s.reporter,
|
s.reporter,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
s.syncStateProvider,
|
s.syncStateProvider,
|
||||||
|
s.serverManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -651,7 +669,7 @@ func (s *Service) setShowAllMail(v bool) {
|
|||||||
|
|
||||||
func (s *Service) startSyncing() {
|
func (s *Service) startSyncing() {
|
||||||
s.isSyncing.Store(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, s.LabelConflictChecker)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) cancelSync() {
|
func (s *Service) cancelSync() {
|
||||||
@ -659,6 +677,10 @@ func (s *Service) cancelSync() {
|
|||||||
s.isSyncing.Store(false)
|
s.isSyncing.Store(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) getConnectors() []*Connector {
|
||||||
|
return maps.Values(s.connectors)
|
||||||
|
}
|
||||||
|
|
||||||
type resyncReq struct{}
|
type resyncReq struct{}
|
||||||
|
|
||||||
type getLabelsReq struct{}
|
type getLabelsReq struct{}
|
||||||
|
|||||||
@ -157,6 +157,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
|
|||||||
s.reporter,
|
s.reporter,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
s.syncStateProvider,
|
s.syncStateProvider,
|
||||||
|
s.serverManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
||||||
@ -165,7 +166,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
|
|||||||
|
|
||||||
s.connectors[connector.addrID] = connector
|
s.connectors[connector.addrID] = connector
|
||||||
|
|
||||||
updates, err := syncLabels(ctx, s.labels.GetLabelMap(), []*Connector{connector})
|
updates, err := syncLabels(ctx, s.labels.GetLabelMap(), []*Connector{connector}, s.labelConflictManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create labels updates for new address: %w", err)
|
return fmt.Errorf("failed to create labels updates for new address: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,10 @@ func (s *Service) HandleLabelEvents(ctx context.Context, events []proton.LabelEv
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := onLabelCreated(ctx, s, event)
|
updates, err := onLabelCreated(ctx, s, event)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to handle create label event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -74,8 +77,8 @@ func (s *Service) HandleLabelEvents(ctx context.Context, events []proton.LabelEv
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []imap.Update {
|
func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) ([]imap.Update, error) {
|
||||||
updates := make([]imap.Update, 0, len(s.connectors))
|
updates := []imap.Update{}
|
||||||
|
|
||||||
s.log.WithFields(logrus.Fields{
|
s.log.WithFields(logrus.Fields{
|
||||||
"labelID": event.ID,
|
"labelID": event.ID,
|
||||||
@ -87,7 +90,17 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []
|
|||||||
|
|
||||||
wr.SetLabel(event.Label.ID, event.Label, "onLabelCreated")
|
wr.SetLabel(event.Label.ID, event.Label, "onLabelCreated")
|
||||||
|
|
||||||
|
labelConflictResolver := s.labelConflictManager.NewConflictResolver(maps.Values(s.connectors))
|
||||||
|
conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, event.Label, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return updates, err
|
||||||
|
}
|
||||||
|
|
||||||
for _, updateCh := range maps.Values(s.connectors) {
|
for _, updateCh := range maps.Values(s.connectors) {
|
||||||
|
conflictUpdates := conflictUpdatesGenerator()
|
||||||
|
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||||
|
updates = append(updates, conflictUpdates...)
|
||||||
|
|
||||||
update := newMailboxCreatedUpdate(imap.MailboxID(event.ID), GetMailboxName(event.Label))
|
update := newMailboxCreatedUpdate(imap.MailboxID(event.ID), GetMailboxName(event.Label))
|
||||||
updateCh.publishUpdate(ctx, update)
|
updateCh.publishUpdate(ctx, update)
|
||||||
updates = append(updates, update)
|
updates = append(updates, update)
|
||||||
@ -99,7 +112,7 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []
|
|||||||
Name: event.Label.Name,
|
Name: event.Label.Name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return updates
|
return updates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([]imap.Update, error) {
|
func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([]imap.Update, error) {
|
||||||
@ -136,8 +149,19 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([
|
|||||||
// Update the label in the map.
|
// Update the label in the map.
|
||||||
wr.SetLabel(apiLabel.ID, apiLabel, "onLabelUpdatedApiID")
|
wr.SetLabel(apiLabel.ID, apiLabel, "onLabelUpdatedApiID")
|
||||||
|
|
||||||
|
// Resolve potential conflicts
|
||||||
|
labelConflictResolver := s.labelConflictManager.NewConflictResolver(maps.Values(s.connectors))
|
||||||
|
conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, apiLabel, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return updates, err
|
||||||
|
}
|
||||||
|
|
||||||
// Notify the IMAP clients.
|
// Notify the IMAP clients.
|
||||||
for _, updateCh := range maps.Values(s.connectors) {
|
for _, updateCh := range maps.Values(s.connectors) {
|
||||||
|
conflictUpdates := conflictUpdatesGenerator()
|
||||||
|
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||||
|
updates = append(updates, conflictUpdates...)
|
||||||
|
|
||||||
update := imap.NewMailboxUpdated(
|
update := imap.NewMailboxUpdated(
|
||||||
imap.MailboxID(apiLabel.ID),
|
imap.MailboxID(apiLabel.ID),
|
||||||
GetMailboxName(apiLabel),
|
GetMailboxName(apiLabel),
|
||||||
|
|||||||
@ -31,8 +31,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SyncUpdateApplier struct {
|
type SyncUpdateApplier struct {
|
||||||
requestCh chan updateRequest
|
requestCh chan updateRequest
|
||||||
replyCh chan updateReply
|
replyCh chan updateReply
|
||||||
|
labelConflictManager *LabelConflictManager
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateReply struct {
|
type updateReply struct {
|
||||||
@ -42,10 +43,11 @@ type updateReply struct {
|
|||||||
|
|
||||||
type updateRequest = func(ctx context.Context, mode usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error)
|
type updateRequest = func(ctx context.Context, mode usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error)
|
||||||
|
|
||||||
func NewSyncUpdateApplier() *SyncUpdateApplier {
|
func NewSyncUpdateApplier(labelConflictManager *LabelConflictManager) *SyncUpdateApplier {
|
||||||
return &SyncUpdateApplier{
|
return &SyncUpdateApplier{
|
||||||
requestCh: make(chan updateRequest),
|
requestCh: make(chan updateRequest),
|
||||||
replyCh: make(chan updateReply),
|
replyCh: make(chan updateReply),
|
||||||
|
labelConflictManager: labelConflictManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +115,7 @@ func (s *SyncUpdateApplier) ApplySyncUpdates(ctx context.Context, updates []sync
|
|||||||
|
|
||||||
func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]proton.Label) error {
|
func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]proton.Label) error {
|
||||||
request := func(ctx context.Context, _ usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error) {
|
request := func(ctx context.Context, _ usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error) {
|
||||||
return syncLabels(ctx, labels, maps.Values(connectors))
|
return syncLabels(ctx, labels, maps.Values(connectors), s.labelConflictManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
updates, err := s.sendRequest(ctx, request)
|
updates, err := s.sendRequest(ctx, request)
|
||||||
@ -128,15 +130,34 @@ func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint:exhaustive
|
// nolint:exhaustive
|
||||||
func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors []*Connector) ([]imap.Update, error) {
|
func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors []*Connector, labelConflictManager *LabelConflictManager) ([]imap.Update, error) {
|
||||||
var updates []imap.Update
|
var updates []imap.Update
|
||||||
|
|
||||||
|
userLabelConflictResolver := labelConflictManager.NewConflictResolver(connectors)
|
||||||
|
internalLabelConflictResolver := labelConflictManager.NewInternalLabelConflictResolver(connectors)
|
||||||
|
|
||||||
|
conflictUpdateGenerator, err := internalLabelConflictResolver.ResolveConflict(ctx, labels)
|
||||||
|
if err != nil {
|
||||||
|
return updates, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, updateCh := range connectors {
|
||||||
|
conflictUpdates := conflictUpdateGenerator()
|
||||||
|
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||||
|
updates = append(updates, conflictUpdates...)
|
||||||
|
}
|
||||||
|
|
||||||
// Create placeholder Folders/Labels mailboxes with the \Noselect attribute.
|
// Create placeholder Folders/Labels mailboxes with the \Noselect attribute.
|
||||||
for _, prefix := range []string{folderPrefix, labelPrefix} {
|
for _, prefix := range []string{folderPrefix, labelPrefix} {
|
||||||
for _, updateCh := range connectors {
|
for _, updateCh := range connectors {
|
||||||
update := newPlaceHolderMailboxCreatedUpdate(prefix)
|
update := newPlaceHolderMailboxCreatedUpdate(prefix)
|
||||||
updateCh.publishUpdate(ctx, update)
|
updateCh.publishUpdate(ctx, update)
|
||||||
updates = append(updates, update)
|
updates = append(updates, update)
|
||||||
|
|
||||||
|
// Ensure we perform a rename operation as well. The created event won't update the name if the ID exists.
|
||||||
|
renameUpdate := imap.NewMailboxUpdated(imap.MailboxID(prefix), []string{prefix})
|
||||||
|
updateCh.publishUpdate(ctx, renameUpdate)
|
||||||
|
updates = append(updates, renameUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +176,16 @@ func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors
|
|||||||
}
|
}
|
||||||
|
|
||||||
case proton.LabelTypeFolder, proton.LabelTypeLabel:
|
case proton.LabelTypeFolder, proton.LabelTypeLabel:
|
||||||
|
conflictUpdatesGenerator, err := userLabelConflictResolver.ResolveConflict(ctx, label, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return updates, err
|
||||||
|
}
|
||||||
|
|
||||||
for _, updateCh := range connectors {
|
for _, updateCh := range connectors {
|
||||||
|
conflictUpdates := conflictUpdatesGenerator()
|
||||||
|
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||||
|
updates = append(updates, conflictUpdates...)
|
||||||
|
|
||||||
update := newMailboxCreatedUpdate(imap.MailboxID(labelID), GetMailboxName(label))
|
update := newMailboxCreatedUpdate(imap.MailboxID(labelID), GetMailboxName(label))
|
||||||
updateCh.publishUpdate(ctx, update)
|
updateCh.publishUpdate(ctx, update)
|
||||||
updates = append(updates, update)
|
updates = append(updates, update)
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
@ -37,9 +38,16 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/files"
|
"github.com/ProtonMail/proton-bridge/v3/internal/files"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rollingCounterNewConnectionThreshold = 300
|
||||||
|
rollingCounterNumberOfBuckets = 6
|
||||||
|
rollingCounterBucketRotationInterval = time.Second * 10
|
||||||
|
)
|
||||||
|
|
||||||
var logIMAP = logrus.WithField("pkg", "server/imap") //nolint:gochecknoglobals
|
var logIMAP = logrus.WithField("pkg", "server/imap") //nolint:gochecknoglobals
|
||||||
|
|
||||||
type IMAPSettingsProvider interface {
|
type IMAPSettingsProvider interface {
|
||||||
@ -81,6 +89,7 @@ func newIMAPServer(
|
|||||||
uidValidityGenerator imap.UIDValidityGenerator,
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
observabilitySender observability.Sender,
|
observabilitySender observability.Sender,
|
||||||
|
featureFlagProvider unleash.FeatureFlagValueProvider,
|
||||||
) (*gluon.Server, error) {
|
) (*gluon.Server, error) {
|
||||||
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
||||||
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
||||||
@ -126,6 +135,8 @@ func newIMAPServer(
|
|||||||
gluon.WithUIDValidityGenerator(uidValidityGenerator),
|
gluon.WithUIDValidityGenerator(uidValidityGenerator),
|
||||||
gluon.WithPanicHandler(panicHandler),
|
gluon.WithPanicHandler(panicHandler),
|
||||||
gluon.WithObservabilitySender(observability.NewAdapter(observabilitySender), int(observability.GluonImapError), int(observability.GluonMessageError), int(observability.GluonOtherError)),
|
gluon.WithObservabilitySender(observability.NewAdapter(observabilitySender), int(observability.GluonImapError), int(observability.GluonMessageError), int(observability.GluonOtherError)),
|
||||||
|
gluon.WithConnectionRollingCounter(rollingCounterNewConnectionThreshold, rollingCounterNumberOfBuckets, rollingCounterBucketRotationInterval),
|
||||||
|
gluon.WithFeatureFlagProvider(featureFlagProvider),
|
||||||
}
|
}
|
||||||
|
|
||||||
if disableIMAPAuthenticate {
|
if disableIMAPAuthenticate {
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
bridgesmtp "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
bridgesmtp "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -63,6 +64,7 @@ type Service struct {
|
|||||||
telemetry Telemetry
|
telemetry Telemetry
|
||||||
|
|
||||||
observabilitySender observability.Sender
|
observabilitySender observability.Sender
|
||||||
|
featureFlagProvider unleash.FeatureFlagValueProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -75,6 +77,7 @@ func NewService(
|
|||||||
uidValidityGenerator imap.UIDValidityGenerator,
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
telemetry Telemetry,
|
telemetry Telemetry,
|
||||||
observabilitySender observability.Sender,
|
observabilitySender observability.Sender,
|
||||||
|
featureFlagProvider unleash.FeatureFlagValueProvider,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
requests: cpc.NewCPC(),
|
requests: cpc.NewCPC(),
|
||||||
@ -91,6 +94,7 @@ func NewService(
|
|||||||
telemetry: telemetry,
|
telemetry: telemetry,
|
||||||
|
|
||||||
observabilitySender: observabilitySender,
|
observabilitySender: observabilitySender,
|
||||||
|
featureFlagProvider: featureFlagProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,6 +204,22 @@ func (sm *Service) RemoveSMTPAccount(ctx context.Context, service *bridgesmtp.Se
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sm *Service) GetUserMailboxByName(ctx context.Context, addrID string, mailboxName []string) (imap.MailboxData, error) {
|
||||||
|
return sm.imapServer.GetUserMailboxByName(ctx, addrID, mailboxName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *Service) GetUserMailboxCountByInternalID(ctx context.Context, addrID string, internalID imap.InternalMailboxID) (int, error) {
|
||||||
|
return sm.imapServer.GetUserMailboxCountByInternalID(ctx, addrID, internalID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *Service) GetOpenIMAPSessionCount() int {
|
||||||
|
return sm.imapServer.GetOpenSessionCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *Service) GetRollingIMAPConnectionCount() int {
|
||||||
|
return sm.imapServer.GetRollingIMAPConnectionCount()
|
||||||
|
}
|
||||||
|
|
||||||
func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
|
func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
|
||||||
eventSub := subscription.Add()
|
eventSub := subscription.Add()
|
||||||
defer subscription.Remove(eventSub)
|
defer subscription.Remove(eventSub)
|
||||||
@ -498,6 +518,7 @@ func (sm *Service) createIMAPServer(ctx context.Context) (*gluon.Server, error)
|
|||||||
sm.uidValidityGenerator,
|
sm.uidValidityGenerator,
|
||||||
sm.panicHandler,
|
sm.panicHandler,
|
||||||
sm.observabilitySender,
|
sm.observabilitySender,
|
||||||
|
sm.featureFlagProvider,
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerCreated{})
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerCreated{})
|
||||||
|
|||||||
@ -44,7 +44,7 @@ type Service struct {
|
|||||||
|
|
||||||
store *Store
|
store *Store
|
||||||
|
|
||||||
getFlagValueFn unleash.GetFlagValueFn
|
featureFlagValueProvider unleash.FeatureFlagValueProvider
|
||||||
|
|
||||||
observabilitySender observability.Sender
|
observabilitySender observability.Sender
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ type Service struct {
|
|||||||
const bitfieldRegexPattern = `^\\\d+`
|
const bitfieldRegexPattern = `^\\\d+`
|
||||||
|
|
||||||
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
|
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
|
||||||
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service {
|
featureFlagValueProvider unleash.FeatureFlagValueProvider, observabilitySender observability.Sender) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
userID: userID,
|
userID: userID,
|
||||||
|
|
||||||
@ -68,8 +68,8 @@ func NewService(userID string, service userevents.Subscribable, eventPublisher e
|
|||||||
|
|
||||||
store: store,
|
store: store,
|
||||||
|
|
||||||
getFlagValueFn: getFlagFn,
|
featureFlagValueProvider: featureFlagValueProvider,
|
||||||
observabilitySender: observabilitySender,
|
observabilitySender: observabilitySender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ func (s *Service) run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEvents []proton.NotificationEvent) error {
|
func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEvents []proton.NotificationEvent) error {
|
||||||
if s.getFlagValueFn(unleash.EventLoopNotificationDisabled) {
|
if s.featureFlagValueProvider.GetFlagValue(unleash.EventLoopNotificationDisabled) {
|
||||||
s.log.Info("Received notification events. Skipping as kill switch is enabled.")
|
s.log.Info("Received notification events. Skipping as kill switch is enabled.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package observability
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability/gluonmetrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Adapter struct {
|
type Adapter struct {
|
||||||
@ -88,6 +89,15 @@ func (adapter *Adapter) AddDistinctMetrics(errType interface{}, metrics ...map[s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(typedMetrics) > 0 {
|
if len(typedMetrics) > 0 {
|
||||||
adapter.sender.AddDistinctMetrics(DistinctionErrorTypeEnum(errTypeInt), typedMetrics...)
|
adapter.sender.AddDistinctMetrics(DistinctionMetricTypeEnum(errTypeInt), typedMetrics...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (adapter *Adapter) AddIMAPConnectionsExceededThresholdMetric(totalOpenIMAPConnections, newIMAPConnections int) {
|
||||||
|
metric := gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold(
|
||||||
|
adapter.sender.GetEmailClient(),
|
||||||
|
BucketIMAPConnections(totalOpenIMAPConnections),
|
||||||
|
BucketIMAPConnections(newIMAPConnections))
|
||||||
|
|
||||||
|
adapter.sender.AddTimeLimitedMetric(NewIMAPConnectionsExceedThreshold, metric)
|
||||||
|
}
|
||||||
|
|||||||
@ -19,21 +19,22 @@ package observability
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// DistinctionErrorTypeEnum - maps to the specific error schema for which we
|
// DistinctionMetricTypeEnum - used to distinct specific metrics which we want to limit over some interval.
|
||||||
// want to send a user update.
|
// Most enums are tied to a specific error schema for which we also send a specific distinction user update.
|
||||||
type DistinctionErrorTypeEnum int
|
type DistinctionMetricTypeEnum int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SyncError DistinctionErrorTypeEnum = iota
|
SyncError DistinctionMetricTypeEnum = iota
|
||||||
GluonImapError
|
GluonImapError
|
||||||
GluonMessageError
|
GluonMessageError
|
||||||
GluonOtherError
|
GluonOtherError
|
||||||
SMTPError
|
SMTPError
|
||||||
EventLoopError // EventLoopError - should always be kept last when inserting new keys.
|
EventLoopError // EventLoopError - should always be kept last when inserting new keys.
|
||||||
|
NewIMAPConnectionsExceedThreshold
|
||||||
)
|
)
|
||||||
|
|
||||||
// errorSchemaMap - maps between the DistinctionErrorTypeEnum and the relevant schema name.
|
// errorSchemaMap - maps between some DistinctionMetricTypeEnum and the relevant schema name.
|
||||||
var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglobals
|
var errorSchemaMap = map[DistinctionMetricTypeEnum]string{ //nolint:gochecknoglobals
|
||||||
SyncError: "bridge_sync_errors_users_total",
|
SyncError: "bridge_sync_errors_users_total",
|
||||||
EventLoopError: "bridge_event_loop_events_errors_users_total",
|
EventLoopError: "bridge_event_loop_events_errors_users_total",
|
||||||
GluonImapError: "bridge_gluon_imap_errors_users_total",
|
GluonImapError: "bridge_gluon_imap_errors_users_total",
|
||||||
@ -43,9 +44,9 @@ var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglob
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createLastSentMap - needs to be updated whenever we make changes to the enum.
|
// createLastSentMap - needs to be updated whenever we make changes to the enum.
|
||||||
func createLastSentMap() map[DistinctionErrorTypeEnum]time.Time {
|
func createLastSentMap() map[DistinctionMetricTypeEnum]time.Time {
|
||||||
registerTime := time.Now().Add(-updateInterval)
|
registerTime := time.Now().Add(-updateInterval)
|
||||||
lastSentMap := make(map[DistinctionErrorTypeEnum]time.Time)
|
lastSentMap := make(map[DistinctionMetricTypeEnum]time.Time)
|
||||||
|
|
||||||
for errType := SyncError; errType <= EventLoopError; errType++ {
|
for errType := SyncError; errType <= EventLoopError; errType++ {
|
||||||
lastSentMap[errType] = registerTime
|
lastSentMap[errType] = registerTime
|
||||||
|
|||||||
@ -40,7 +40,7 @@ type distinctionUtility struct {
|
|||||||
|
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
|
|
||||||
lastSentMap map[DistinctionErrorTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins.
|
lastSentMap map[DistinctionMetricTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins.
|
||||||
|
|
||||||
observabilitySender observabilitySender
|
observabilitySender observabilitySender
|
||||||
settingsGetter settingsGetter
|
settingsGetter settingsGetter
|
||||||
@ -87,7 +87,7 @@ func (d *distinctionUtility) setSettingsGetter(getter settingsGetter) {
|
|||||||
|
|
||||||
// checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric
|
// checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric
|
||||||
// within the last 5 minutes.
|
// within the last 5 minutes.
|
||||||
func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeEnum) bool {
|
func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionMetricTypeEnum) bool {
|
||||||
curTime := time.Now()
|
curTime := time.Now()
|
||||||
val, ok := d.lastSentMap[key]
|
val, ok := d.lastSentMap[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -107,7 +107,7 @@ func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeE
|
|||||||
// and the relevant settings. In the future this will need to be expanded to support multiple
|
// and the relevant settings. In the future this will need to be expanded to support multiple
|
||||||
// versions of the metric if we ever decide to change them.
|
// versions of the metric if we ever decide to change them.
|
||||||
func (d *distinctionUtility) generateUserMetric(
|
func (d *distinctionUtility) generateUserMetric(
|
||||||
metricType DistinctionErrorTypeEnum,
|
metricType DistinctionMetricTypeEnum,
|
||||||
) proton.ObservabilityMetric {
|
) proton.ObservabilityMetric {
|
||||||
schemaName, ok := errorSchemaMap[metricType]
|
schemaName, ok := errorSchemaMap[metricType]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -138,7 +138,7 @@ func generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric {
|
func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric {
|
||||||
d.updateHeartbeatData(errType)
|
d.updateHeartbeatData(errType)
|
||||||
|
|
||||||
if d.checkAndUpdateLastSentMap(errType) {
|
if d.checkAndUpdateLastSentMap(errType) {
|
||||||
|
|||||||
45
internal/services/observability/gluonmetrics/metrics.go
Normal file
45
internal/services/observability/gluonmetrics/metrics.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2025 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 gluonmetrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
newIMAPConnectionThresholdExceededSchemaName = "bridge_imap_recently_opened_connections_total"
|
||||||
|
newIMAPConnectionThresholdExceededVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateNewOpenedIMAPConnectionsExceedThreshold(emailClient, totalOpenIMAPConnectionCount, newlyOpenedIMAPConnectionCount string) proton.ObservabilityMetric {
|
||||||
|
return proton.ObservabilityMetric{
|
||||||
|
Name: newIMAPConnectionThresholdExceededSchemaName,
|
||||||
|
Version: newIMAPConnectionThresholdExceededVersion,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Value": 1,
|
||||||
|
"Labels": map[string]string{
|
||||||
|
"mailClient": emailClient,
|
||||||
|
"numberOfOpenIMAPConnectionsBuckets": totalOpenIMAPConnectionCount,
|
||||||
|
"numberOfRecentlyOpenedIMAPConnectionsBuckets": newlyOpenedIMAPConnectionCount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -42,7 +42,7 @@ func (d *distinctionUtility) resetHeartbeatData() {
|
|||||||
d.heartbeatData.receivedGluonError = false
|
d.heartbeatData.receivedGluonError = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) {
|
func (d *distinctionUtility) updateHeartbeatData(errType DistinctionMetricTypeEnum) {
|
||||||
d.withUpdateHeartbeatDataLock(func() {
|
d.withUpdateHeartbeatDataLock(func() {
|
||||||
//nolint:exhaustive
|
//nolint:exhaustive
|
||||||
switch errType {
|
switch errType {
|
||||||
|
|||||||
@ -45,7 +45,9 @@ type client struct {
|
|||||||
// so we can easily pass them down to relevant components.
|
// so we can easily pass them down to relevant components.
|
||||||
type Sender interface {
|
type Sender interface {
|
||||||
AddMetrics(metrics ...proton.ObservabilityMetric)
|
AddMetrics(metrics ...proton.ObservabilityMetric)
|
||||||
AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric)
|
AddDistinctMetrics(errType DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric)
|
||||||
|
AddTimeLimitedMetric(metricType DistinctionMetricTypeEnum, metric proton.ObservabilityMetric)
|
||||||
|
GetEmailClient() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -325,11 +327,25 @@ func (s *Service) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
|||||||
// what number of events come from what number of users.
|
// what number of events come from what number of users.
|
||||||
// As the binning interval is what allows us to do this we
|
// As the binning interval is what allows us to do this we
|
||||||
// should not send these if there are no logged-in users at that moment.
|
// should not send these if there are no logged-in users at that moment.
|
||||||
func (s *Service) AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
func (s *Service) AddDistinctMetrics(errType DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||||
metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...)
|
metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...)
|
||||||
s.addMetricsIfClients(metrics...)
|
s.addMetricsIfClients(metrics...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddTimeLimitedMetric - schedules a metric to be sent if a metric of the same type has not been sent within some interval.
|
||||||
|
// The interval is defined in the distinction utility.
|
||||||
|
func (s *Service) AddTimeLimitedMetric(metricType DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) {
|
||||||
|
if !s.distinctionUtility.checkAndUpdateLastSentMap(metricType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.addMetricsIfClients(metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetEmailClient() string {
|
||||||
|
return s.distinctionUtility.getEmailClientUserAgent()
|
||||||
|
}
|
||||||
|
|
||||||
// ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker.
|
// ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker.
|
||||||
func (s *Service) ModifyHeartbeatInterval(duration time.Duration) {
|
func (s *Service) ModifyHeartbeatInterval(duration time.Duration) {
|
||||||
s.distinctionUtility.heartbeatTicker.Reset(duration)
|
s.distinctionUtility.heartbeatTicker.Reset(duration)
|
||||||
|
|||||||
@ -66,3 +66,30 @@ func getEnabled(value bool) string {
|
|||||||
}
|
}
|
||||||
return "enabled"
|
return "enabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BucketIMAPConnections(val int) string {
|
||||||
|
switch {
|
||||||
|
case val < 10:
|
||||||
|
return "<10"
|
||||||
|
case val < 25:
|
||||||
|
return "10-24"
|
||||||
|
case val < 50:
|
||||||
|
return "25-49"
|
||||||
|
case val < 100:
|
||||||
|
return "50-99"
|
||||||
|
case val < 200:
|
||||||
|
return "100-199"
|
||||||
|
case val < 300:
|
||||||
|
return "200-299"
|
||||||
|
case val < 500:
|
||||||
|
return "300-499"
|
||||||
|
case val < 1000:
|
||||||
|
return "500-999"
|
||||||
|
case val < 2000:
|
||||||
|
return "1000-1999"
|
||||||
|
case val < 3000:
|
||||||
|
return "2000-2999"
|
||||||
|
default:
|
||||||
|
return "3000+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -29,6 +30,9 @@ const (
|
|||||||
|
|
||||||
smtpSendSuccessSchemaName = "bridge_smtp_send_success_total"
|
smtpSendSuccessSchemaName = "bridge_smtp_send_success_total"
|
||||||
smtpSendSuccessSchemaVersion = 1
|
smtpSendSuccessSchemaVersion = 1
|
||||||
|
|
||||||
|
smtpSubmissionRequestSchemaName = "bridge_smtp_send_request_total"
|
||||||
|
smtpSubmissionRequestSchemaVersion = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateSMTPErrorObservabilityMetric(errorType string) proton.ObservabilityMetric {
|
func generateSMTPErrorObservabilityMetric(errorType string) proton.ObservabilityMetric {
|
||||||
@ -88,3 +92,19 @@ func GenerateSMTPSendSuccess() proton.ObservabilityMetric {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateSMTPSubmissionRequest(emailClient string, numberOfOpenIMAPConnections, numberOfRecentlyOpenedIMAPConnections int) proton.ObservabilityMetric {
|
||||||
|
return proton.ObservabilityMetric{
|
||||||
|
Name: smtpSubmissionRequestSchemaName,
|
||||||
|
Version: smtpSubmissionRequestSchemaVersion,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Value": 1,
|
||||||
|
"Labels": map[string]string{
|
||||||
|
"numberOfOpenIMAPConnections": observability.BucketIMAPConnections(numberOfOpenIMAPConnections),
|
||||||
|
"numberOfRecentlyOpenedIMAPConnections": observability.BucketIMAPConnections(numberOfRecentlyOpenedIMAPConnections),
|
||||||
|
"mailClient": emailClient,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -32,13 +32,24 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
newlyOpenedIMAPConnectionsThreshold = 300
|
||||||
|
)
|
||||||
|
|
||||||
|
type imapSessionCountProvider interface {
|
||||||
|
GetOpenIMAPSessionCount() int
|
||||||
|
GetRollingIMAPConnectionCount() int
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
userID string
|
userID string
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
@ -59,6 +70,9 @@ type Service struct {
|
|||||||
serverManager ServerManager
|
serverManager ServerManager
|
||||||
|
|
||||||
observabilitySender observability.Sender
|
observabilitySender observability.Sender
|
||||||
|
|
||||||
|
imapSessionCountProvider imapSessionCountProvider
|
||||||
|
featureFlagValueProvider unleash.FeatureFlagValueProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -74,6 +88,8 @@ func NewService(
|
|||||||
identityState *useridentity.State,
|
identityState *useridentity.State,
|
||||||
serverManager ServerManager,
|
serverManager ServerManager,
|
||||||
observabilitySender observability.Sender,
|
observabilitySender observability.Sender,
|
||||||
|
imapSessionCountProvider imapSessionCountProvider,
|
||||||
|
featureFlagValueProvider unleash.FeatureFlagValueProvider,
|
||||||
) *Service {
|
) *Service {
|
||||||
subscriberName := fmt.Sprintf("smpt-%v", userID)
|
subscriberName := fmt.Sprintf("smpt-%v", userID)
|
||||||
|
|
||||||
@ -99,7 +115,9 @@ func NewService(
|
|||||||
addressMode: mode,
|
addressMode: mode,
|
||||||
serverManager: serverManager,
|
serverManager: serverManager,
|
||||||
|
|
||||||
observabilitySender: observabilitySender,
|
imapSessionCountProvider: imapSessionCountProvider,
|
||||||
|
observabilitySender: observabilitySender,
|
||||||
|
featureFlagValueProvider: featureFlagValueProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +225,6 @@ func (s *Service) run(ctx context.Context) {
|
|||||||
|
|
||||||
switch r := request.Value().(type) {
|
switch r := request.Value().(type) {
|
||||||
case *sendMailReq:
|
case *sendMailReq:
|
||||||
s.log.Debug("Received send mail request")
|
|
||||||
err := s.sendMail(ctx, r)
|
err := s.sendMail(ctx, r)
|
||||||
request.Reply(ctx, nil, err)
|
request.Reply(ctx, nil, err)
|
||||||
|
|
||||||
@ -252,15 +269,38 @@ type sendMailReq struct {
|
|||||||
|
|
||||||
func (s *Service) sendMail(ctx context.Context, req *sendMailReq) error {
|
func (s *Service) sendMail(ctx context.Context, req *sendMailReq) error {
|
||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
|
openSessionCount := s.imapSessionCountProvider.GetOpenIMAPSessionCount()
|
||||||
|
newlyOpenedSessions := s.imapSessionCountProvider.GetRollingIMAPConnectionCount()
|
||||||
|
log := s.log.WithFields(logrus.Fields{
|
||||||
|
"newlyOpenedIMAPConnectionsCount": newlyOpenedSessions,
|
||||||
|
"openIMAPConnectionsCount": openSessionCount,
|
||||||
|
})
|
||||||
|
log.Debug("Received send mail request")
|
||||||
|
|
||||||
|
// Send SMTP send request metric to observability.
|
||||||
|
s.observabilitySender.AddMetrics(observabilitymetrics.GenerateSMTPSubmissionRequest(s.observabilitySender.GetEmailClient(), openSessionCount, newlyOpenedSessions))
|
||||||
|
|
||||||
|
// Send report to sentry if kill switch is disabled & number of newly opened IMAP connections exceed threshold.
|
||||||
|
if !s.featureFlagValueProvider.GetFlagValue(unleash.SMTPSubmissionRequestSentryReportDisabled) && newlyOpenedSessions >= newlyOpenedIMAPConnectionsThreshold {
|
||||||
|
if err := s.reporter.ReportMessageWithContext("SMTP Send Mail Request - newly opened IMAP connections exceed threshold", reporter.Context{
|
||||||
|
"newlyOpenedIMAPConnectionsCount": newlyOpenedSessions,
|
||||||
|
"openIMAPConnectionsCount": openSessionCount,
|
||||||
|
"emailClient": s.observabilitySender.GetEmailClient(),
|
||||||
|
}); err != nil {
|
||||||
|
s.log.WithError(err).Error("Failed to submit report to sentry (SMTP Send Mail Request)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
end := time.Now()
|
end := time.Now()
|
||||||
s.log.Debugf("Send mail request finished in %v", end.Sub(start))
|
log.Debugf("Send mail request finished in %v", end.Sub(start))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := s.smtpSendMail(ctx, req.authID, req.from, req.to, req.r); err != nil {
|
if err := s.smtpSendMail(ctx, req.authID, req.from, req.to, req.r); err != nil {
|
||||||
if apiErr := new(proton.APIError); errors.As(err, &apiErr) {
|
if apiErr := new(proton.APIError); errors.As(err, &apiErr) {
|
||||||
s.log.WithError(apiErr).WithField("Details", apiErr.DetailsToString()).Error("failed to send message")
|
log.WithError(apiErr).WithField("Details", apiErr.DetailsToString()).Error("failed to send message")
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -19,10 +19,13 @@ package syncservice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/db"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/network"
|
"github.com/ProtonMail/proton-bridge/v3/internal/network"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -33,6 +36,10 @@ const NumSyncStages = 4
|
|||||||
|
|
||||||
type LabelMap = map[string]proton.Label
|
type LabelMap = map[string]proton.Label
|
||||||
|
|
||||||
|
type labelConflictChecker interface {
|
||||||
|
CheckAndReportConflicts(ctx context.Context, labels map[string]proton.Label) error
|
||||||
|
}
|
||||||
|
|
||||||
// Handler is the interface from which we control the syncing of the IMAP data. One instance should be created for each
|
// Handler is the interface from which we control the syncing of the IMAP data. One instance should be created for each
|
||||||
// user and used for every subsequent sync request.
|
// user and used for every subsequent sync request.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
@ -45,6 +52,7 @@ type Handler struct {
|
|||||||
syncFinishedCh chan error
|
syncFinishedCh chan error
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
downloadCache *DownloadCache
|
downloadCache *DownloadCache
|
||||||
|
sentryReporter reporter.Reporter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(
|
func NewHandler(
|
||||||
@ -54,6 +62,7 @@ func NewHandler(
|
|||||||
state StateProvider,
|
state StateProvider,
|
||||||
log *logrus.Entry,
|
log *logrus.Entry,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
|
sentryReporter reporter.Reporter,
|
||||||
) *Handler {
|
) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
client: client,
|
client: client,
|
||||||
@ -65,6 +74,7 @@ func NewHandler(
|
|||||||
regulator: regulator,
|
regulator: regulator,
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
downloadCache: newDownloadCache(),
|
downloadCache: newDownloadCache(),
|
||||||
|
sentryReporter: sentryReporter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,12 +101,17 @@ func (t *Handler) Execute(
|
|||||||
updateApplier UpdateApplier,
|
updateApplier UpdateApplier,
|
||||||
messageBuilder MessageBuilder,
|
messageBuilder MessageBuilder,
|
||||||
coolDown time.Duration,
|
coolDown time.Duration,
|
||||||
|
labelConflictChecker labelConflictChecker,
|
||||||
) {
|
) {
|
||||||
t.log.Info("Sync triggered")
|
t.log.Info("Sync triggered")
|
||||||
t.group.Once(func(ctx context.Context) {
|
t.group.Once(func(ctx context.Context) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
t.log.WithField("start", start).Info("Beginning user sync")
|
t.log.WithField("start", start).Info("Beginning user sync")
|
||||||
|
|
||||||
|
if err := labelConflictChecker.CheckAndReportConflicts(ctx, labels); err != nil {
|
||||||
|
t.log.WithError(err).Error("Failed to check and report label conflicts")
|
||||||
|
}
|
||||||
|
|
||||||
syncReporter.OnStart(ctx)
|
syncReporter.OnStart(ctx)
|
||||||
var err error
|
var err error
|
||||||
for {
|
for {
|
||||||
@ -104,6 +119,20 @@ func (t *Handler) Execute(
|
|||||||
t.log.WithError(err).Error("Sync aborted")
|
t.log.WithError(err).Error("Sync aborted")
|
||||||
break
|
break
|
||||||
} else if err = t.run(ctx, syncReporter, labels, updateApplier, messageBuilder); err != nil {
|
} else if err = t.run(ctx, syncReporter, labels, updateApplier, messageBuilder); err != nil {
|
||||||
|
if db.IsUniqueLabelConstraintError(err) {
|
||||||
|
if sentryErr := t.sentryReporter.ReportMessageWithContext("Failed to sync due to label unique constraint conflict",
|
||||||
|
reporter.Context{"err": err}); sentryErr != nil {
|
||||||
|
t.log.WithError(sentryErr).Error("Failed to report label unique constraint conflict error to Sentry")
|
||||||
|
}
|
||||||
|
} else if !(errors.Is(err, context.Canceled)) {
|
||||||
|
if sentryErr := t.sentryReporter.ReportMessageWithContext("Failed to sync, will retry later", reporter.Context{
|
||||||
|
"err": err.Error(),
|
||||||
|
"user_id": t.userID,
|
||||||
|
}); sentryErr != nil {
|
||||||
|
t.log.WithError(sentryErr).Error("Failed to report sentry message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
t.log.WithError(err).Error("Failed to sync, will retry later")
|
t.log.WithError(err).Error("Failed to sync, will retry later")
|
||||||
sleepCtx(ctx, coolDown)
|
sleepCtx(ctx, coolDown)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -25,6 +25,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/sentry"
|
||||||
"github.com/bradenaw/juniper/xmaps"
|
"github.com/bradenaw/juniper/xmaps"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -208,6 +209,13 @@ func TestTask_StateHasSyncedState(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockLabelConflictChecker struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLabelConflictChecker) CheckAndReportConflicts(_ context.Context, _ map[string]proton.Label) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestTask_RepeatsOnSyncFailure(t *testing.T) {
|
func TestTask_RepeatsOnSyncFailure(t *testing.T) {
|
||||||
const MessageTotal int64 = 50
|
const MessageTotal int64 = 50
|
||||||
const MessageID string = "foo"
|
const MessageID string = "foo"
|
||||||
@ -271,7 +279,7 @@ func TestTask_RepeatsOnSyncFailure(t *testing.T) {
|
|||||||
tt.syncReporter.EXPECT().OnFinished(gomock.Any())
|
tt.syncReporter.EXPECT().OnFinished(gomock.Any())
|
||||||
tt.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(MessageDelta))
|
tt.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(MessageDelta))
|
||||||
|
|
||||||
tt.task.Execute(tt.syncReporter, labels, tt.updateApplier, tt.messageBuilder, time.Microsecond)
|
tt.task.Execute(tt.syncReporter, labels, tt.updateApplier, tt.messageBuilder, time.Microsecond, &mockLabelConflictChecker{})
|
||||||
require.NoError(t, <-tt.task.OnSyncFinishedCH())
|
require.NoError(t, <-tt.task.OnSyncFinishedCH())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,7 +350,7 @@ func newTestHandler(mockCtrl *gomock.Controller, userID string) thandler { // no
|
|||||||
client := NewMockAPIClient(mockCtrl)
|
client := NewMockAPIClient(mockCtrl)
|
||||||
messageBuilder := NewMockMessageBuilder(mockCtrl)
|
messageBuilder := NewMockMessageBuilder(mockCtrl)
|
||||||
syncReporter := NewMockReporter(mockCtrl)
|
syncReporter := NewMockReporter(mockCtrl)
|
||||||
task := NewHandler(regulator, client, userID, syncState, logrus.WithField("test", "test"), &async.NoopPanicHandler{})
|
task := NewHandler(regulator, client, userID, syncState, logrus.WithField("test", "test"), &async.NoopPanicHandler{}, sentry.NullSentryReporter{})
|
||||||
|
|
||||||
return thandler{
|
return thandler{
|
||||||
task: task,
|
task: task,
|
||||||
|
|||||||
@ -29,6 +29,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/db"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gluon/watcher"
|
"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"
|
||||||
@ -70,6 +72,8 @@ type Service struct {
|
|||||||
eventPollWaitersLock sync.Mutex
|
eventPollWaitersLock sync.Mutex
|
||||||
eventSubscription events.Subscription
|
eventSubscription events.Subscription
|
||||||
eventWatcher *watcher.Watcher[events.Event]
|
eventWatcher *watcher.Watcher[events.Event]
|
||||||
|
|
||||||
|
sentryReporter reporter.Reporter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -82,6 +86,7 @@ func NewService(
|
|||||||
eventTimeout time.Duration,
|
eventTimeout time.Duration,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
eventSubscription events.Subscription,
|
eventSubscription events.Subscription,
|
||||||
|
sentryReporter reporter.Reporter,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
cpc: cpc.NewCPC(),
|
cpc: cpc.NewCPC(),
|
||||||
@ -99,6 +104,7 @@ func NewService(
|
|||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
eventSubscription: eventSubscription,
|
eventSubscription: eventSubscription,
|
||||||
eventWatcher: eventSubscription.Add(events.ConnStatusDown{}, events.ConnStatusUp{}),
|
eventWatcher: eventSubscription.Add(events.ConnStatusDown{}, events.ConnStatusUp{}),
|
||||||
|
sentryReporter: sentryReporter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,6 +420,14 @@ func (s *Service) handleEventError(ctx context.Context, lastEventID string, even
|
|||||||
return subscriberName, fmt.Errorf("failed to handle event due to server error: %w", err)
|
return subscriberName, fmt.Errorf("failed to handle event due to server error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if db.IsUniqueLabelConstraintError(err) {
|
||||||
|
if err := s.sentryReporter.ReportMessageWithContext("Unique label constraint error occurred on event", reporter.Context{
|
||||||
|
"err": err,
|
||||||
|
}); err != nil {
|
||||||
|
s.log.WithError(err).Error("Failed to report label constraint error to sentry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, the error is a client-side issue; notify bridge to handle it.
|
// Otherwise, the error is a client-side issue; notify bridge to handle it.
|
||||||
s.log.WithField("event", event).Warn("Failed to handle API event")
|
s.log.WithField("event", event).Warn("Failed to handle API event")
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"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"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -49,6 +50,7 @@ func TestServiceHandleEventError_SubscriberEventUnwrapping(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
@ -87,6 +89,7 @@ func TestServiceHandleEventError_BadEventPutsServiceOnPause(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
service.Resume()
|
service.Resume()
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
@ -121,6 +124,7 @@ func TestServiceHandleEventError_BadEventFromPublishTimeout(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
@ -152,6 +156,7 @@ func TestServiceHandleEventError_NoBadEventCheck(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
@ -178,6 +183,7 @@ func TestServiceHandleEventError_JsonUnmarshalEventProducesUncategorizedErrorEve
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"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"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -69,6 +70,7 @@ func TestServiceHandleEvent_CheckEventCategoriesHandledInOrder(t *testing.T) {
|
|||||||
10*time.Second,
|
10*time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
@ -130,6 +132,7 @@ func TestServiceHandleEvent_CheckEventFailureCausesError(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
@ -168,6 +171,7 @@ func TestServiceHandleEvent_CheckEventFailureCausesErrorParallel(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import (
|
|||||||
"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"
|
||||||
mocks2 "github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
mocks2 "github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents/mocks"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
@ -76,6 +77,7 @@ func TestService_EventIDLoadStore(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := service.Start(context.Background(), group)
|
_, err := service.Start(context.Background(), group)
|
||||||
@ -132,6 +134,7 @@ func TestService_RetryEventOnNonCatastrophicFailure(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
service.Subscribe(NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber}))
|
service.Subscribe(NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber}))
|
||||||
|
|
||||||
@ -182,6 +185,7 @@ func TestService_OnBadEventServiceIsPaused(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event publisher expectations.
|
// Event publisher expectations.
|
||||||
@ -249,6 +253,7 @@ func TestService_UnsubscribeDuringEventHandlingDoesNotCauseDeadlock(t *testing.T
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber})
|
subscription := NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber})
|
||||||
@ -309,6 +314,7 @@ func TestService_UnsubscribeBeforeHandlingEventIsNotConsideredError(t *testing.T
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewEventSubscriber("Foo")
|
subscription := NewEventSubscriber("Foo")
|
||||||
@ -369,6 +375,7 @@ func TestService_WaitOnEventPublishAfterPause(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
||||||
@ -442,6 +449,7 @@ func TestService_EventRewind(t *testing.T) {
|
|||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
events.NewNullSubscription(),
|
events.NewNullSubscription(),
|
||||||
|
sentry.NullSentryReporter{},
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := service.Start(context.Background(), group)
|
_, err := service.Start(context.Background(), group)
|
||||||
|
|||||||
@ -37,15 +37,32 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals
|
|||||||
const filename = "unleash_flags"
|
const filename = "unleash_flags"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled"
|
EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled"
|
||||||
IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled"
|
IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled"
|
||||||
UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled"
|
UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled"
|
||||||
UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled"
|
UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled"
|
||||||
|
LabelConflictResolverDisabled = "InboxBridgeLabelConflictResolverDisabled"
|
||||||
|
SMTPSubmissionRequestSentryReportDisabled = "InboxBridgeSmtpSubmissionRequestSentryReportDisabled"
|
||||||
|
InternalLabelConflictResolverDisabled = "InboxBridgeUnexpectedFoldersLabelsStartupFixupDisabled"
|
||||||
|
ItnternalLabelConflictNonEmptyMailboxDeletion = "InboxBridgeUnknownNonEmptyMailboxDeletion"
|
||||||
)
|
)
|
||||||
|
|
||||||
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
|
type FeatureFlagValueProvider interface {
|
||||||
type GetFlagValueFn func(key string) bool
|
GetFlagValue(key string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NullUnleashService - mock of the unleash service. Should be used for testing.
|
||||||
|
type NullUnleashService struct{}
|
||||||
|
|
||||||
|
func (n NullUnleashService) GetFlagValue(_ string) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNullUnleashService() *NullUnleashService {
|
||||||
|
return &NullUnleashService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
|
||||||
type Service struct {
|
type Service struct {
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
timer *proton.Ticker
|
timer *proton.Ticker
|
||||||
|
|||||||
@ -110,7 +110,7 @@ func New(
|
|||||||
syncConfigDir string,
|
syncConfigDir string,
|
||||||
isNew bool,
|
isNew bool,
|
||||||
notificationStore *notifications.Store,
|
notificationStore *notifications.Store,
|
||||||
getFlagValFn unleash.GetFlagValueFn,
|
featureFlagValueProvider unleash.FeatureFlagValueProvider,
|
||||||
) (*User, error) {
|
) (*User, error) {
|
||||||
user, err := newImpl(
|
user, err := newImpl(
|
||||||
ctx,
|
ctx,
|
||||||
@ -130,7 +130,7 @@ func New(
|
|||||||
syncConfigDir,
|
syncConfigDir,
|
||||||
isNew,
|
isNew,
|
||||||
notificationStore,
|
notificationStore,
|
||||||
getFlagValFn,
|
featureFlagValueProvider,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Cleanup any pending resources on error
|
// Cleanup any pending resources on error
|
||||||
@ -163,7 +163,7 @@ func newImpl(
|
|||||||
syncConfigDir string,
|
syncConfigDir string,
|
||||||
isNew bool,
|
isNew bool,
|
||||||
notificationStore *notifications.Store,
|
notificationStore *notifications.Store,
|
||||||
getFlagValueFn unleash.GetFlagValueFn,
|
featureFlagValueProvider unleash.FeatureFlagValueProvider,
|
||||||
) (*User, error) {
|
) (*User, error) {
|
||||||
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
||||||
|
|
||||||
@ -241,6 +241,7 @@ func newImpl(
|
|||||||
5*time.Minute,
|
5*time.Minute,
|
||||||
crashHandler,
|
crashHandler,
|
||||||
eventSubscription,
|
eventSubscription,
|
||||||
|
reporter,
|
||||||
)
|
)
|
||||||
|
|
||||||
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode())
|
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode())
|
||||||
@ -262,6 +263,8 @@ func newImpl(
|
|||||||
identityState.Clone(),
|
identityState.Clone(),
|
||||||
smtpServerManager,
|
smtpServerManager,
|
||||||
observabilityService,
|
observabilityService,
|
||||||
|
imapServerManager,
|
||||||
|
featureFlagValueProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
user.imapService = imapservice.NewService(
|
user.imapService = imapservice.NewService(
|
||||||
@ -282,9 +285,10 @@ func newImpl(
|
|||||||
user.maxSyncMemory,
|
user.maxSyncMemory,
|
||||||
showAllMail,
|
showAllMail,
|
||||||
observabilityService,
|
observabilityService,
|
||||||
|
featureFlagValueProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService)
|
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, featureFlagValueProvider, observabilityService)
|
||||||
|
|
||||||
// When we receive an auth object, we update it in the vault.
|
// When we receive an auth object, we update it in the vault.
|
||||||
// This will be used to authorize the user on the next run.
|
// This will be used to authorize the user on the next run.
|
||||||
|
|||||||
@ -28,11 +28,13 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api/server/backend"
|
"github.com/ProtonMail/go-proton-api/server/backend"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/notifications"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/notifications"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/tests"
|
"github.com/ProtonMail/proton-bridge/v3/tests"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
@ -150,12 +152,13 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
|||||||
nullEventSubscription := events.NewNullSubscription()
|
nullEventSubscription := events.NewNullSubscription()
|
||||||
nullIMAPServerManager := imapservice.NewNullIMAPServerManager()
|
nullIMAPServerManager := imapservice.NewNullIMAPServerManager()
|
||||||
nullSMTPServerManager := smtp.NewNullServerManager()
|
nullSMTPServerManager := smtp.NewNullServerManager()
|
||||||
|
nullUnleashService := unleash.NewNullUnleashService()
|
||||||
|
|
||||||
user, err := New(
|
user, err := New(
|
||||||
ctx,
|
ctx,
|
||||||
vaultUser,
|
vaultUser,
|
||||||
client,
|
client,
|
||||||
nil,
|
sentry.NullSentryReporter{},
|
||||||
apiUser,
|
apiUser,
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
@ -171,9 +174,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
|||||||
notifications.NewStore(func() (string, error) {
|
notifications.NewStore(func() (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}),
|
}),
|
||||||
func(_ string) bool {
|
nullUnleashService,
|
||||||
return false
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
defer user.Close()
|
defer user.Close()
|
||||||
|
|||||||
@ -89,8 +89,11 @@ func (r *reportRecorder) close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *reportRecorder) assertEmpty() {
|
func (r *reportRecorder) assertEmpty() {
|
||||||
if !r.skipAssert {
|
if !r.skipAssert && len(r.reports) > 0 {
|
||||||
r.assert.Empty(r.reports)
|
for _, report := range r.reports {
|
||||||
|
// Sentry reports with failed syncs are expected, mostly due to sync context cancellations.
|
||||||
|
r.assert.Equal(report.message, "Failed to sync, will retry later")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +146,11 @@ func (r *reportRecorder) ReportMessageWithContext(message string, context report
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) ReportWarningWithContext(message string, context reporter.Context) error {
|
||||||
|
r.add(false, message, context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *reportRecorder) ReportExceptionWithContext(data any, context reporter.Context) error {
|
func (r *reportRecorder) ReportExceptionWithContext(data any, context reporter.Context) error {
|
||||||
if context == nil {
|
if context == nil {
|
||||||
context = reporter.Context{}
|
context = reporter.Context{}
|
||||||
|
|||||||
@ -45,5 +45,9 @@ Feature: Bridge send remote notification observability metrics
|
|||||||
And the user with username "[user:user1]" sends SMTP send success observability metric
|
And the user with username "[user:user1]" sends SMTP send success observability metric
|
||||||
Then it succeeds
|
Then it succeeds
|
||||||
|
|
||||||
|
Scenario: Test SMTP send request observability metric
|
||||||
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
|
And the user with username "[user:user1]" sends an SMTP send request observability metric
|
||||||
|
Then it succeeds
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,3 +9,8 @@ Feature: Bridge send remote notification observability metrics
|
|||||||
When the user logs in with username "[user:user1]" and password "password"
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
And the user with username "[user:user1]" sends all possible gluon error observability metrics
|
And the user with username "[user:user1]" sends all possible gluon error observability metrics
|
||||||
Then it succeeds
|
Then it succeeds
|
||||||
|
|
||||||
|
Scenario: Test newly opened IMAP connections in Gluon exceed threshold metric
|
||||||
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
|
And the user with username "[user:user1]" sends a Gluon metric indicating that the number of newly opened IMAP connections within some interval have exceed a threshold value
|
||||||
|
Then it succeeds
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability/gluonmetrics"
|
||||||
smtpMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics"
|
smtpMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
||||||
)
|
)
|
||||||
@ -188,3 +189,48 @@ func (s *scenario) SMTPSendSuccessObservabilityMetric(username string) error {
|
|||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *scenario) SMTPSendRequestObservabilityMetric(username string) error {
|
||||||
|
batch := proton.ObservabilityBatch{
|
||||||
|
Metrics: []proton.ObservabilityMetric{
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 1, 10),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 10, 25),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 30, 45),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 50, 75),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 100, 150),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 200, 250),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 300, 450),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 500, 750),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 1000, 1500),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 1900, 2500),
|
||||||
|
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 3000, 3500),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
err := c.SendObservabilityBatch(ctx, batch)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scenario) GluonNewlyOpenedIMAPConnectionsExceedThreshold(username string) error {
|
||||||
|
batch := proton.ObservabilityBatch{
|
||||||
|
Metrics: []proton.ObservabilityMetric{
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(1), observability.BucketIMAPConnections(10)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(10), observability.BucketIMAPConnections(25)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(30), observability.BucketIMAPConnections(45)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(50), observability.BucketIMAPConnections(75)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(100), observability.BucketIMAPConnections(150)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(200), observability.BucketIMAPConnections(250)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(300), observability.BucketIMAPConnections(450)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(500), observability.BucketIMAPConnections(750)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(1000), observability.BucketIMAPConnections(1500)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(1900), observability.BucketIMAPConnections(2500)),
|
||||||
|
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(3000), observability.BucketIMAPConnections(3500)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
err := c.SendObservabilityBatch(ctx, batch)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -242,7 +242,11 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
|
|||||||
// SMTP metrics
|
// SMTP metrics
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends all possible SMTP error observability metrics$`, s.SMTPErrorObservabilityMetrics)
|
ctx.Step(`^the user with username "([^"]*)" sends all possible SMTP error observability metrics$`, s.SMTPErrorObservabilityMetrics)
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends SMTP send success observability metric$`, s.SMTPSendSuccessObservabilityMetric)
|
ctx.Step(`^the user with username "([^"]*)" sends SMTP send success observability metric$`, s.SMTPSendSuccessObservabilityMetric)
|
||||||
|
// SMTP submission metric
|
||||||
|
ctx.Step(`^the user with username "([^"]*)" sends an SMTP send request observability metric$`, s.SMTPSendRequestObservabilityMetric)
|
||||||
|
|
||||||
// Gluon related metrics
|
// Gluon related metrics
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends all possible gluon error observability metrics$`, s.testGluonErrorObservabilityMetrics)
|
ctx.Step(`^the user with username "([^"]*)" sends all possible gluon error observability metrics$`, s.testGluonErrorObservabilityMetrics)
|
||||||
|
// Gluon metric - on newly opened IMAP connections exceeding threshold.
|
||||||
|
ctx.Step(`^the user with username "([^"]*)" sends a Gluon metric indicating that the number of newly opened IMAP connections within some interval have exceed a threshold value$`, s.GluonNewlyOpenedIMAPConnectionsExceedThreshold)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user