mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
102 Commits
v3.19.0
...
94125056ab
| Author | SHA1 | Date | |
|---|---|---|---|
| 94125056ab | |||
| 675b37a2fa | |||
| 9d4415d8cc | |||
| 4557f54e2f | |||
| 05623a9e49 | |||
| a305ee1113 | |||
| e38f7748d0 | |||
| 92b2024e3e | |||
| 37a8fc95d2 | |||
| 0c63533aa7 | |||
| af98bc2273 | |||
| b37f2d138a | |||
| 7831a98e6c | |||
| 4d415675e0 | |||
| 291f44d1b5 | |||
| a4b315d67a | |||
| a15d4eb3ef | |||
| 4e764fe93d | |||
| df409925ec | |||
| e68f3441d7 | |||
| 42605c1923 | |||
| 899d3293bc | |||
| c66f0b800a | |||
| b9c75d02b2 | |||
| 4b91e66505 | |||
| 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 |
@ -1 +1 @@
|
||||
* @go/bridge-ppl/devs
|
||||
* inbox-desktop-approvers
|
||||
@ -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)
|
||||
* [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)
|
||||
* [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)
|
||||
* [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)
|
||||
@ -141,6 +142,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [appengine](https://google.golang.org/appengine) available under [license](https://pkg.go.dev/google.golang.org/appengine?tab=licenses)
|
||||
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
|
||||
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||
* [go-autostart](https://github.com/ElectroNafta/go-autostart) available under [license](https://github.com/ElectroNafta/go-autostart/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE)
|
||||
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)
|
||||
|
||||
27
Changelog.md
27
Changelog.md
@ -3,6 +3,33 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Jubilee Bridge 3.20.1
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-362: Implemented logic for reconciling label conflicts.
|
||||
|
||||
|
||||
## Jubilee Bridge 3.20.0
|
||||
|
||||
### Added
|
||||
* BRIDGE-348: Enable display of BYOE addresses in Bridge.
|
||||
* BRIDGE-340: Added additional logging for label operations and related bad events.
|
||||
* BRIDGE-324: Log a hash of the vault key on Bridge start.
|
||||
|
||||
### Changed
|
||||
* BRIDGE-352: Chore: bump go to 1.24.2.
|
||||
* BRIDGE-353: Chore: update x/net package to 0.38.0.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-351: Allow draft creation and import to BYOE addresses in combined mode.
|
||||
* BRIDGE-301: Prevent imports into non-BYOE external addresses.
|
||||
* BRIDGE-341: Replaced go-autostart with a fork to support creating autostart shortcuts in directories with Unicode characters on Windows.
|
||||
* BRIDGE-332: Strip newline characters from username and password fields in the Bridge GUI.
|
||||
* BRIDGE-336: Ensure all remote labels are verified and created in Gluon at Bridge startup.
|
||||
* BRIDGE-335: Persist the last successfully used keychain helper as a user preference on Linux.
|
||||
* BRIDGE-333: Ignore unknown label IDs during Bridge synchronization.
|
||||
|
||||
|
||||
## Infinity Bridge 3.19.0
|
||||
|
||||
### Changed
|
||||
|
||||
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=3.19.0+git
|
||||
BRIDGE_APP_VERSION?=3.20.1+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
|
||||
18
go.mod
18
go.mod
@ -2,14 +2,14 @@ module github.com/ProtonMail/proton-bridge/v3
|
||||
|
||||
go 1.24
|
||||
|
||||
toolchain go1.24.0
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250527153202-a7383713882a
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
@ -46,10 +46,10 @@ require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/oauth2 v0.7.0
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/text v0.23.0
|
||||
google.golang.org/api v0.114.0
|
||||
google.golang.org/grpc v1.56.3
|
||||
google.golang.org/protobuf v1.33.0
|
||||
@ -114,6 +114,7 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
@ -121,9 +122,9 @@ require (
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
@ -131,6 +132,7 @@ require (
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/ProtonMail/go-autostart => github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
|
||||
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
||||
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
|
||||
|
||||
36
go.sum
36
go.sum
@ -23,6 +23,8 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033 h1:d2RB9rQmSusb0K+qSgB+DAY+8i+AXZ/o+oDHj2vAUaA=
|
||||
github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033/go.mod h1:o0nKiWcK0e2G/90uL6akWRkzOV4mFcZmvpBPpigJvdw=
|
||||
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
|
||||
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a h1:eQO/GF/+H8/9udc9QAgieFr+jr1tjXlJo35RAhsUbWY=
|
||||
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||
@ -34,10 +36,10 @@ 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-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2 h1:lDgMidI/9j2eedavcy7YICv8+F73ooVTUoUGBE4dO0s=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250527093338-0b0a59c5f7d2 h1:BWo8ntIFkCeh6o2f2btbDQbUT0GYXuF5BNUOkaCbgws=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250527093338-0b0a59c5f7d2/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250527153202-a7383713882a h1:6OhwrrhJ7/agGXC0ulIqgfzuAs33ILUs61KW5AcHcH4=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250527153202-a7383713882a/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 v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
|
||||
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
|
||||
@ -45,10 +47,8 @@ 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-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-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3 h1:YYnLBVcg7WrEbYVmF1PBr4AEQlob9rCphsMHAmF4CAo=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||
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.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-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
@ -500,8 +500,8 @@ 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.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
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/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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -557,8 +557,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
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/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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -573,8 +573,8 @@ 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-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.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/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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -613,8 +613,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/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/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@ -634,8 +634,8 @@ 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.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.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/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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
|
||||
@ -138,7 +138,7 @@ func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get helper: %w", err)
|
||||
}
|
||||
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
keychain, _, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create keychain: %w", err)
|
||||
}
|
||||
|
||||
@ -134,7 +134,7 @@ func TestKeychainMigration(t *testing.T) {
|
||||
func TestUserMigration(t *testing.T) {
|
||||
kcl := keychain.NewTestKeychainsList()
|
||||
|
||||
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
|
||||
kc, _, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, kc.Put("brokenID", "broken"))
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
@ -67,11 +69,12 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
|
||||
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
|
||||
|
||||
var (
|
||||
vaultKey []byte
|
||||
insecure bool
|
||||
vaultKey []byte
|
||||
insecure bool
|
||||
lastUsedHelper string
|
||||
)
|
||||
|
||||
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
|
||||
if key, helper, err := loadVaultKey(vaultDir, keychains); err != nil {
|
||||
if reporter != nil {
|
||||
if rerr := reporter.ReportMessageWithContext("Could not load/create vault key", map[string]any{
|
||||
"keychainDefaultHelper": keychains.GetDefaultHelper(),
|
||||
@ -89,6 +92,8 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
|
||||
vaultDir = path.Join(vaultDir, "insecure")
|
||||
} else {
|
||||
vaultKey = key
|
||||
lastUsedHelper = helper
|
||||
logHashedVaultKey(vaultKey) // Log a hash of the vault key.
|
||||
}
|
||||
|
||||
gluonCacheDir, err := locations.ProvideGluonCachePath()
|
||||
@ -96,34 +101,47 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
|
||||
return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
|
||||
userVault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
|
||||
if err != nil {
|
||||
return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
return vault, insecure, corrupt, nil
|
||||
// Remember the last successfully used keychain and store that as the user preference.
|
||||
if err := vault.SetHelper(vaultDir, lastUsedHelper); err != nil {
|
||||
logrus.WithError(err).Error("Could not store last used keychain helper")
|
||||
}
|
||||
|
||||
return userVault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
// loadVaultKey - loads the key used to encrypt the vault alongside the keychain helper used to access it.
|
||||
func loadVaultKey(vaultDir string, keychains *keychain.List) (key []byte, keychainHelper string, err error) {
|
||||
keychainHelper, err = vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
return nil, keychainHelper, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
kc, keychainHelper, err := keychain.NewKeychain(keychainHelper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
return nil, keychainHelper, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
key, err := vault.GetVaultKey(kc)
|
||||
key, err = vault.GetVaultKey(kc)
|
||||
if err != nil {
|
||||
if keychain.IsErrKeychainNoItem(err) {
|
||||
logrus.WithError(err).Warn("no vault key found, generating new")
|
||||
return vault.NewVaultKey(kc)
|
||||
key, err := vault.NewVaultKey(kc)
|
||||
return key, keychainHelper, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not check for vault key: %w", err)
|
||||
return nil, keychainHelper, fmt.Errorf("could not check for vault key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
return key, keychainHelper, nil
|
||||
}
|
||||
|
||||
// logHashedVaultKey - computes a sha256 hash and encodes it to base 64. The resulting string is logged.
|
||||
func logHashedVaultKey(vaultKey []byte) {
|
||||
hashedKey := sha256.Sum256(vaultKey)
|
||||
logrus.WithField("hashedKey", hex.EncodeToString(hashedKey[:])).Info("Found vault key")
|
||||
}
|
||||
|
||||
@ -618,7 +618,7 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user; it will not have keys.
|
||||
aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password"))
|
||||
aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password"), true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an API client so we can remove the address keys.
|
||||
@ -785,7 +785,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a second address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create 10 messages for the user.
|
||||
|
||||
@ -127,9 +127,9 @@ func TestBridge_Observability_UserMetric(t *testing.T) {
|
||||
}
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
userMetricPeriod := time.Millisecond * 200
|
||||
userMetricPeriod := time.Millisecond * 600
|
||||
heartbeatPeriod := time.Second * 10
|
||||
throttlePeriod := time.Millisecond * 100
|
||||
throttlePeriod := time.Millisecond * 300
|
||||
observability.ModifyUserMetricInterval(userMetricPeriod)
|
||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||
|
||||
|
||||
@ -355,7 +355,7 @@ func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
|
||||
|
||||
// Create a new address
|
||||
newAddress := "foo@proton.ch"
|
||||
addrID, err := s.CreateAddress(userID, newAddress, password)
|
||||
addrID, err := s.CreateAddress(userID, newAddress, password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
event := <-addressCreatedCh
|
||||
@ -430,7 +430,7 @@ func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||
})
|
||||
|
||||
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password)
|
||||
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
var allowSyncToProgress atomic.Bool
|
||||
@ -469,7 +469,7 @@ func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
|
||||
})
|
||||
|
||||
// User AddrID2 event as a check point to see when the new address was created.
|
||||
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
||||
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
allowSyncToProgress.Store(true)
|
||||
@ -552,7 +552,7 @@ func TestBridge_MessageCreateDuringSync(t *testing.T) {
|
||||
})
|
||||
|
||||
// User AddrID2 event as a check point to see when the new address was created.
|
||||
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
||||
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// At most two events can be published, one for the first address, then for the second.
|
||||
@ -663,7 +663,7 @@ func TestBridge_AddressOrderChangeDuringSyncInCombinedModeDoesNotTriggerBadEvent
|
||||
require.Equal(t, 1, len(info.Addresses))
|
||||
require.Equal(t, info.Addresses[0], "user@proton.local")
|
||||
|
||||
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password)
|
||||
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, s.SetAddressOrder(userID, []string{addrID2, addrID}))
|
||||
|
||||
@ -304,7 +304,7 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
addrID, err = s.CreateAddress(userID, "other@pm.me", password)
|
||||
addrID, err = s.CreateAddress(userID, "other@pm.me", password, true)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
@ -312,7 +312,7 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
otherID, err := s.CreateAddress(userID, "another@pm.me", password)
|
||||
otherID, err := s.CreateAddress(userID, "another@pm.me", password, true)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.RemoveAddress(userID, otherID))
|
||||
|
||||
@ -328,6 +328,87 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_AddressEvents_BYOEAddressAdded(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, addrID, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
// Create an additional proton address
|
||||
addrID, err = s.CreateAddress(userID, "other@pm.me", password, true)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
require.NoError(t, s.AddAddressCreatedEvent(userID, addrID))
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
userInfo, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(userInfo.Addresses))
|
||||
|
||||
// Create an external address with sending disabled.
|
||||
externalID, err := s.CreateExternalAddress(userID, "another@yahoo.com", password, false)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
require.NoError(t, s.AddAddressCreatedEvent(userID, externalID))
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
// User addresses should still return 2, as we ignore the external address.
|
||||
userInfo, err = bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(userInfo.Addresses))
|
||||
|
||||
// Create an external address w. sending enabled. This is considered a BYOE address.
|
||||
BYOEAddrID, err := s.CreateExternalAddress(userID, "other@yahoo.com", password, true)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
require.NoError(t, s.AddAddressCreatedEvent(userID, BYOEAddrID))
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
userInfo, err = bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(userInfo.Addresses))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_AddressEvents_ExternalAddressSendChanged(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
// Create an additional external address.
|
||||
externalID, err := s.CreateExternalAddress(userID, "other@yahoo.me", password, false)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
require.NoError(t, s.AddAddressCreatedEvent(userID, externalID))
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
// We expect only one address, the external one without sending should not be considered a valid address.
|
||||
userInfo, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(userInfo.Addresses))
|
||||
|
||||
// Change it to allow sending such that it becomes a BYOE address.
|
||||
err = s.ChangeAddressAllowSend(userID, externalID, true)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
require.NoError(t, s.AddAddressUpdatedEvent(userID, externalID))
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
// We should now have 2 usable addresses listed.
|
||||
userInfo, err = bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(userInfo.Addresses))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_AddressEventUpdatedForAddressThatDoesNotExist_NoBadEvent(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
@ -694,7 +775,7 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
@ -745,7 +826,7 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Immediately disable the address.
|
||||
|
||||
@ -658,7 +658,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Give the new user an alias.
|
||||
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"))))
|
||||
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"), true)))
|
||||
|
||||
// Login the user.
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "primary", []byte("password"), nil, nil)))
|
||||
@ -706,7 +706,7 @@ func TestBridge_User_GetAddresses(t *testing.T) {
|
||||
// Create a user.
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password"))
|
||||
addrID2, err := s.CreateAddress(userID, "user@external.com", password, false)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
|
||||
|
||||
@ -720,6 +720,29 @@ func TestBridge_User_GetAddresses(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_GetAddresses_BYOE(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
// Add a non-sending external address.
|
||||
_, err = s.CreateExternalAddress(userID, "user@external.com", password, false)
|
||||
require.NoError(t, err)
|
||||
// Add a BYOE address.
|
||||
_, err = s.CreateExternalAddress(userID, "user2@external.com", password, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(info.Addresses))
|
||||
require.Equal(t, info.Addresses[0], "user@proton.local")
|
||||
require.Equal(t, info.Addresses[1], "user2@external.com")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// getErr returns the error that was passed to it.
|
||||
func getErr[T any](_ T, err error) error {
|
||||
return err
|
||||
|
||||
@ -91,13 +91,12 @@ func TestTLSSignedCertWrongPublicKey(t *testing.T) {
|
||||
r.Error(t, err, "expected dial to fail because of wrong public key")
|
||||
}
|
||||
|
||||
// GODT-2293 bump badssl cert and re enable this.
|
||||
func _TestTLSSignedCertTrustedPublicKey(t *testing.T) { //nolint:unused,deadcode
|
||||
func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
||||
copyTrustedPins(checker)
|
||||
checker.trustedPins = append(checker.trustedPins, `pin-sha256="LwnIKjNLV3z243ap8y0yXNPghsqE76J08Eq3COvUt2E="`)
|
||||
checker.trustedPins = append(checker.trustedPins, `pin-sha256="FlvTPG/nIMKtOj9nelnEjujwSZ5EDyfiKYxZgbXREls="`)
|
||||
_, 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")
|
||||
}
|
||||
|
||||
@ -271,7 +271,10 @@ FocusScope {
|
||||
usernameTextField.enabled = false;
|
||||
passwordTextField.enabled = false;
|
||||
loading = true;
|
||||
Backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text));
|
||||
|
||||
let usernameTextFiltered = usernameTextField.text.replace(/[\n\r]+$/, "");
|
||||
let passwordTextFiltered = passwordTextField.text.replace(/[\n\r]+$/, "");
|
||||
Backend.login(usernameTextFiltered, Qt.btoa(passwordTextFiltered));
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
211
internal/services/imapservice/conflicts.go
Normal file
211
internal/services/imapservice/conflicts.go
Normal file
@ -0,0 +1,211 @@
|
||||
// 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/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
|
||||
}
|
||||
|
||||
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 LabelConflictManager struct {
|
||||
gluonLabelNameProvider GluonLabelNameProvider
|
||||
gluonIDProvider gluonIDProvider
|
||||
client apiClient
|
||||
reporter sentryReporter
|
||||
getFeatureFlagValueFn unleash.GetFlagValueFn
|
||||
}
|
||||
|
||||
func NewLabelConflictManager(
|
||||
gluonLabelNameProvider GluonLabelNameProvider,
|
||||
gluonIDProvider gluonIDProvider,
|
||||
client apiClient,
|
||||
reporter sentryReporter,
|
||||
getFeatureFlagValueFn unleash.GetFlagValueFn) *LabelConflictManager {
|
||||
return &LabelConflictManager{
|
||||
gluonLabelNameProvider: gluonLabelNameProvider,
|
||||
gluonIDProvider: gluonIDProvider,
|
||||
client: client,
|
||||
reporter: reporter,
|
||||
getFeatureFlagValueFn: getFeatureFlagValueFn,
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.getFeatureFlagValueFn(unleash.LabelConflictResolverDisabled) {
|
||||
return &nullLabelConflictResolverImpl{}
|
||||
}
|
||||
|
||||
return &labelConflictResolverImpl{
|
||||
mailboxFetch: m.generateMailboxFetcher(connectors),
|
||||
client: m.client,
|
||||
reporter: m.reporter,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error) {
|
||||
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] {
|
||||
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) {
|
||||
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 {
|
||||
return combineIMAPUpdateFns(updateFns), nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
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) {
|
||||
fn := func() []imap.Update {
|
||||
return []imap.Update{
|
||||
imap.NewMailboxDeleted(imap.MailboxID(mailboxData.RemoteID)),
|
||||
newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label)),
|
||||
}
|
||||
}
|
||||
updateFns = append(updateFns, fn)
|
||||
return combineIMAPUpdateFns(updateFns), nil
|
||||
}
|
||||
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 {
|
||||
logrus.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.
|
||||
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
|
||||
}
|
||||
682
internal/services/imapservice/conflicts_test.go
Normal file
682
internal/services/imapservice/conflicts_test.go
Normal file
@ -0,0 +1,682 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
func getFeatureFlagValueMock(_ string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
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 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, getFeatureFlagValueMock).
|
||||
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, getFeatureFlagValueMock)
|
||||
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, getFeatureFlagValueMock).
|
||||
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, getFeatureFlagValueMock)
|
||||
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, getFeatureFlagValueMock)
|
||||
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, getFeatureFlagValueMock).
|
||||
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}).
|
||||
Return(label, nil)
|
||||
}
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
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}).
|
||||
Return(label, nil)
|
||||
}
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
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}).
|
||||
Return(label, nil)
|
||||
}
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
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}).
|
||||
Return(label, nil)
|
||||
}
|
||||
mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).Return(proton.Label{}, proton.ErrNoSuchLabel)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagValueMock)
|
||||
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}).
|
||||
Return(label, nil)
|
||||
}
|
||||
mockClient.On("GetLabel", mock.Anything, "222", []proton.LabelType{proton.LabelTypeFolder, proton.LabelTypeLabel}).Return(proton.Label{}, proton.ErrNoSuchLabel)
|
||||
|
||||
connector := &imapservice.Connector{}
|
||||
connector.SetAddrIDTest("addr-1")
|
||||
connectors := []*imapservice.Connector{connector}
|
||||
|
||||
getFeatureFlagFn := func(_ string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
manager := imapservice.NewLabelConflictManager(mockLabelProvider, mockIDProvider, mockClient, mockReporter, getFeatureFlagFn)
|
||||
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)
|
||||
}
|
||||
@ -257,7 +257,7 @@ func (s *Connector) DeleteMailbox(ctx context.Context, _ connector.IMAPStateWrit
|
||||
wLabels := s.labels.Write()
|
||||
defer wLabels.Close()
|
||||
|
||||
wLabels.Delete(string(mboxID))
|
||||
wLabels.Delete(string(mboxID), "connectorDeleteMailbox")
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -555,7 +555,7 @@ func (s *Connector) createLabel(ctx context.Context, name []string) (imap.Mailbo
|
||||
wLabels := s.labels.Write()
|
||||
defer wLabels.Close()
|
||||
|
||||
wLabels.SetLabel(label.ID, label)
|
||||
wLabels.SetLabel(label.ID, label, "connectorCreateLabel")
|
||||
|
||||
return toIMAPMailbox(label, s.flags, s.permFlags, s.attrs), nil
|
||||
}
|
||||
@ -593,7 +593,7 @@ func (s *Connector) createFolder(ctx context.Context, name []string) (imap.Mailb
|
||||
}
|
||||
|
||||
// Add label to list so subsequent sub folder create requests work correct.
|
||||
wLabels.SetLabel(label.ID, label)
|
||||
wLabels.SetLabel(label.ID, label, "connectorCreateFolder")
|
||||
|
||||
return toIMAPMailbox(label, s.flags, s.permFlags, s.attrs), nil
|
||||
}
|
||||
@ -619,7 +619,7 @@ func (s *Connector) updateLabel(ctx context.Context, labelID imap.MailboxID, nam
|
||||
wLabels := s.labels.Write()
|
||||
defer wLabels.Close()
|
||||
|
||||
wLabels.SetLabel(label.ID, update)
|
||||
wLabels.SetLabel(label.ID, update, "connectorUpdateLabel")
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -660,7 +660,7 @@ func (s *Connector) updateFolder(ctx context.Context, labelID imap.MailboxID, na
|
||||
return err
|
||||
}
|
||||
|
||||
wLabels.SetLabel(label.ID, update)
|
||||
wLabels.SetLabel(label.ID, update, "connectorUpdateFolder")
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -680,7 +680,7 @@ func (s *Connector) importMessage(
|
||||
}
|
||||
|
||||
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
|
||||
addr, err := s.getImportAddress(p, isDraft)
|
||||
addr, err := getImportAddress(p, isDraft, s.addrID, s)
|
||||
if err != nil {
|
||||
return imap.Message{}, nil, err
|
||||
}
|
||||
@ -800,8 +800,10 @@ func (s *Connector) createDraftWithParser(ctx context.Context, parser *parser.Pa
|
||||
return draft, nil
|
||||
}
|
||||
|
||||
func (s *Connector) publishUpdate(_ context.Context, update imap.Update) {
|
||||
s.updateCh.Enqueue(update)
|
||||
func (s *Connector) publishUpdate(_ context.Context, updates ...imap.Update) {
|
||||
for _, update := range updates {
|
||||
s.updateCh.Enqueue(update)
|
||||
}
|
||||
}
|
||||
|
||||
func fixGODT3003Labels(
|
||||
@ -871,45 +873,6 @@ func equalAddresses(a, b string) bool {
|
||||
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
|
||||
}
|
||||
|
||||
func (s *Connector) getImportAddress(p *parser.Parser, isDraft bool) (proton.Address, error) {
|
||||
// addr is primary for combined mode or active for split mode
|
||||
address, ok := s.identityState.GetAddress(s.addrID)
|
||||
if !ok {
|
||||
return proton.Address{}, errors.New("could not find account address")
|
||||
}
|
||||
|
||||
inCombinedMode := s.addressMode == usertypes.AddressModeCombined
|
||||
if !inCombinedMode {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
senderAddr, err := s.getSenderProtonAddress(p)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errNoSenderAddressMatch) {
|
||||
s.log.WithError(err).Warn("Could not get import address")
|
||||
}
|
||||
|
||||
// We did not find a match, so we use the default address.
|
||||
return address, nil
|
||||
}
|
||||
|
||||
if senderAddr.ID == address.ID {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// GODT-3185 / BRIDGE-120 In combined mode, in certain cases we adapt the address used for encryption.
|
||||
// - draft with non-default address in combined mode: using sender address
|
||||
// - import with non-default address in combined mode: using sender address
|
||||
// - import with non-default disabled address in combined mode: using sender address
|
||||
|
||||
isSenderAddressDisabled := (!bool(senderAddr.Send)) || (senderAddr.Status != proton.AddressStatusEnabled)
|
||||
if isDraft && isSenderAddressDisabled {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
return senderAddr, nil
|
||||
}
|
||||
|
||||
func (s *Connector) getSenderProtonAddress(p *parser.Parser) (proton.Address, error) {
|
||||
// Step 1: extract sender email address from message
|
||||
if (p == nil) || (p.Root() == nil) || (p.Root().Header.Len() == 0) {
|
||||
@ -942,3 +905,7 @@ func (s *Connector) getSenderProtonAddress(p *parser.Parser) (proton.Address, er
|
||||
|
||||
return addressList[index], nil
|
||||
}
|
||||
|
||||
func (s *Connector) SetAddrIDTest(addrID string) {
|
||||
s.addrID = addrID
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ func TestFixGODT3003Labels(t *testing.T) {
|
||||
Path: []string{"bar", "Foo"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeFolder,
|
||||
})
|
||||
}, "")
|
||||
|
||||
wr.SetLabel("0", proton.Label{
|
||||
ID: "0",
|
||||
@ -52,7 +52,7 @@ func TestFixGODT3003Labels(t *testing.T) {
|
||||
Path: []string{"Inbox"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeSystem,
|
||||
})
|
||||
}, "")
|
||||
|
||||
wr.SetLabel("bar", proton.Label{
|
||||
ID: "bar",
|
||||
@ -61,7 +61,7 @@ func TestFixGODT3003Labels(t *testing.T) {
|
||||
Path: []string{"bar"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeFolder,
|
||||
})
|
||||
}, "")
|
||||
|
||||
wr.SetLabel("my_label", proton.Label{
|
||||
ID: "my_label",
|
||||
@ -70,7 +70,7 @@ func TestFixGODT3003Labels(t *testing.T) {
|
||||
Path: []string{"MyLabel"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeLabel,
|
||||
})
|
||||
}, "")
|
||||
|
||||
wr.SetLabel("my_label2", proton.Label{
|
||||
ID: "my_label2",
|
||||
@ -79,7 +79,7 @@ func TestFixGODT3003Labels(t *testing.T) {
|
||||
Path: []string{labelPrefix, "MyLabel2"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeLabel,
|
||||
})
|
||||
}, "")
|
||||
wr.Close()
|
||||
|
||||
mboxs := []imap.MailboxNoAttrib{
|
||||
@ -133,7 +133,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
|
||||
Path: []string{folderPrefix, "bar", "Foo"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeFolder,
|
||||
})
|
||||
}, "")
|
||||
|
||||
wr.SetLabel("0", proton.Label{
|
||||
ID: "0",
|
||||
@ -142,7 +142,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
|
||||
Path: []string{"Inbox"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeSystem,
|
||||
})
|
||||
}, "")
|
||||
|
||||
wr.SetLabel("bar", proton.Label{
|
||||
ID: "bar",
|
||||
@ -151,7 +151,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
|
||||
Path: []string{folderPrefix, "bar"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeFolder,
|
||||
})
|
||||
}, "")
|
||||
|
||||
wr.SetLabel("my_label", proton.Label{
|
||||
ID: "my_label",
|
||||
@ -160,7 +160,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
|
||||
Path: []string{labelPrefix, "MyLabel"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeLabel,
|
||||
})
|
||||
}, "")
|
||||
|
||||
wr.SetLabel("my_label2", proton.Label{
|
||||
ID: "my_label2",
|
||||
@ -169,7 +169,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
|
||||
Path: []string{labelPrefix, "MyLabel2"},
|
||||
Color: "",
|
||||
Type: proton.LabelTypeLabel,
|
||||
})
|
||||
}, "")
|
||||
wr.Close()
|
||||
|
||||
mboxs := []imap.MailboxNoAttrib{
|
||||
|
||||
@ -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 {
|
||||
var name []string
|
||||
|
||||
@ -122,3 +132,12 @@ func GetMailboxName(label proton.Label) []string {
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/gluon/connector"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||
)
|
||||
|
||||
@ -34,6 +35,10 @@ type IMAPServerManager interface {
|
||||
) error
|
||||
|
||||
RemoveIMAPUser(ctx context.Context, deleteData bool, 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)
|
||||
}
|
||||
|
||||
type NullIMAPServerManager struct{}
|
||||
@ -57,6 +62,18 @@ func (n NullIMAPServerManager) RemoveIMAPUser(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NullIMAPServerManager) LogRemoteLabelIDs(
|
||||
_ context.Context,
|
||||
_ GluonIDProvider,
|
||||
_ ...string,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NullIMAPServerManager) GetUserMailboxByName(_ context.Context, _ string, _ []string) (imap.MailboxData, error) {
|
||||
return imap.MailboxData{}, nil
|
||||
}
|
||||
|
||||
func NewNullIMAPServerManager() *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/userevents"
|
||||
"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/pkg/cpc"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -91,7 +92,8 @@ type Service struct {
|
||||
lastHandledEventID string
|
||||
isSyncing atomic.Bool
|
||||
|
||||
observabilitySender observability.Sender
|
||||
observabilitySender observability.Sender
|
||||
labelConflictManager *LabelConflictManager
|
||||
}
|
||||
|
||||
func NewService(
|
||||
@ -112,6 +114,7 @@ func NewService(
|
||||
maxSyncMemory uint64,
|
||||
showAllMail bool,
|
||||
observabilitySender observability.Sender,
|
||||
getFeatureFlagValueFn unleash.GetFlagValueFn,
|
||||
) *Service {
|
||||
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
||||
|
||||
@ -121,7 +124,8 @@ func NewService(
|
||||
})
|
||||
rwIdentity := newRWIdentity(identityState, bridgePassProvider, keyPassProvider)
|
||||
|
||||
syncUpdateApplier := NewSyncUpdateApplier()
|
||||
labelConflictManager := NewLabelConflictManager(serverManager, gluonIDProvider, client, reporter, getFeatureFlagValueFn)
|
||||
syncUpdateApplier := NewSyncUpdateApplier(labelConflictManager)
|
||||
syncMessageBuilder := NewSyncMessageBuilder(rwIdentity)
|
||||
syncReporter := newSyncReporter(identityState.User.ID, eventPublisher, time.Second)
|
||||
|
||||
@ -156,7 +160,8 @@ func NewService(
|
||||
syncReporter: syncReporter,
|
||||
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||
|
||||
observabilitySender: observabilitySender,
|
||||
observabilitySender: observabilitySender,
|
||||
labelConflictManager: labelConflictManager,
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,6 +360,12 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
||||
|
||||
case *onBadEventReq:
|
||||
s.log.Debug("Bad Event Request")
|
||||
// // Log remote label IDs stored in the local labelMap.
|
||||
s.labels.LogLabels()
|
||||
// Log the remote label IDs store in Gluon.
|
||||
if err := s.logRemoteMailboxIDsFromServer(ctx, s.connectors); err != nil {
|
||||
s.log.Warnf("Could not obtain remote mailbox IDs from server: %v", err)
|
||||
}
|
||||
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
||||
req.Reply(ctx, nil, err)
|
||||
|
||||
@ -572,6 +583,16 @@ func (s *Service) addConnectorsToServer(ctx context.Context, connectors map[stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) logRemoteMailboxIDsFromServer(ctx context.Context, connectors map[string]*Connector) error {
|
||||
addrIDs := make([]string, 0, len(connectors))
|
||||
|
||||
for _, c := range connectors {
|
||||
addrIDs = append(addrIDs, c.addrID)
|
||||
}
|
||||
|
||||
return s.serverManager.LogRemoteLabelIDs(ctx, s.gluonIDProvider, addrIDs...)
|
||||
}
|
||||
|
||||
func (s *Service) removeConnectorsFromServer(ctx context.Context, connectors map[string]*Connector, deleteData bool) error {
|
||||
addrIDs := make([]string, 0, len(connectors))
|
||||
|
||||
|
||||
@ -165,7 +165,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
@ -74,8 +77,8 @@ func (s *Service) HandleLabelEvents(ctx context.Context, events []proton.LabelEv
|
||||
return nil
|
||||
}
|
||||
|
||||
func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []imap.Update {
|
||||
updates := make([]imap.Update, 0, len(s.connectors))
|
||||
func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) ([]imap.Update, error) {
|
||||
updates := []imap.Update{}
|
||||
|
||||
s.log.WithFields(logrus.Fields{
|
||||
"labelID": event.ID,
|
||||
@ -85,9 +88,19 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []
|
||||
wr := s.labels.Write()
|
||||
defer wr.Close()
|
||||
|
||||
wr.SetLabel(event.Label.ID, event.Label)
|
||||
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) {
|
||||
conflictUpdates := conflictUpdatesGenerator()
|
||||
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||
updates = append(updates, conflictUpdates...)
|
||||
|
||||
update := newMailboxCreatedUpdate(imap.MailboxID(event.ID), GetMailboxName(event.Label))
|
||||
updateCh.publishUpdate(ctx, update)
|
||||
updates = append(updates, update)
|
||||
@ -99,7 +112,7 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []
|
||||
Name: event.Label.Name,
|
||||
})
|
||||
|
||||
return updates
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([]imap.Update, error) {
|
||||
@ -121,7 +134,7 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([
|
||||
|
||||
// Only update the label if it exists; we don't want to create it as a client may have just deleted it.
|
||||
if _, ok := wr.GetLabel(label.ID); ok {
|
||||
wr.SetLabel(label.ID, event.Label)
|
||||
wr.SetLabel(label.ID, event.Label, "onLabelUpdatedLabelEventID")
|
||||
}
|
||||
|
||||
// API doesn't notify us that the path has changed. We need to fetch it again.
|
||||
@ -134,10 +147,21 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([
|
||||
}
|
||||
|
||||
// Update the label in the map.
|
||||
wr.SetLabel(apiLabel.ID, apiLabel)
|
||||
wr.SetLabel(apiLabel.ID, apiLabel, "onLabelUpdatedApiID")
|
||||
|
||||
// Resolve potential conflicts
|
||||
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
|
||||
}
|
||||
|
||||
// Notify the IMAP clients.
|
||||
for _, updateCh := range maps.Values(s.connectors) {
|
||||
conflictUpdates := conflictUpdatesGenerator()
|
||||
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||
updates = append(updates, conflictUpdates...)
|
||||
|
||||
update := imap.NewMailboxUpdated(
|
||||
imap.MailboxID(apiLabel.ID),
|
||||
GetMailboxName(apiLabel),
|
||||
@ -176,7 +200,7 @@ func onLabelDeleted(ctx context.Context, s *Service, event proton.LabelEvent) []
|
||||
wr := s.labels.Write()
|
||||
wr.Close()
|
||||
|
||||
wr.Delete(event.ID)
|
||||
wr.Delete(event.ID, "onLabelDeleted")
|
||||
|
||||
s.eventPublisher.PublishEvent(ctx, events.UserLabelDeleted{
|
||||
UserID: s.identityState.UserID(),
|
||||
|
||||
@ -256,8 +256,8 @@ func onMessageUpdateDraftOrSent(ctx context.Context, s *Service, event proton.Me
|
||||
res.update.Literal,
|
||||
res.update.MailboxIDs,
|
||||
res.update.ParsedMessage,
|
||||
true, // Is the message doesn't exist, silently create it.
|
||||
false,
|
||||
true, // Is the message doesn't exist, silently create it.
|
||||
duringSync, // Ignore unknown labelIDs during sync.
|
||||
)
|
||||
|
||||
didPublish, err := safePublishMessageUpdate(ctx, s, full.AddressID, update, duringSync)
|
||||
|
||||
@ -113,7 +113,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
|
||||
if err := waitOnIMAPUpdates(ctx, updates); gluon.IsNoSuchMessage(err) {
|
||||
logrus.WithError(err).Error("Failed to handle update message event in gluon, will try creating it (sync)")
|
||||
|
||||
updates, err := onMessageCreated(ctx, s.service, event.Message, false, true)
|
||||
updates, err := onMessageCreated(ctx, s.service, event.Message, true, true)
|
||||
if err != nil {
|
||||
s.service.observabilitySender.AddDistinctMetrics(
|
||||
observability.SyncError,
|
||||
|
||||
@ -22,6 +22,8 @@ import (
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
@ -42,8 +44,8 @@ type labelsRead interface {
|
||||
|
||||
type labelsWrite interface {
|
||||
labelsRead
|
||||
SetLabel(id string, label proton.Label)
|
||||
Delete(id string)
|
||||
SetLabel(id string, label proton.Label, actionSource string)
|
||||
Delete(id string, actionSource string)
|
||||
}
|
||||
|
||||
type rwLabels struct {
|
||||
@ -51,6 +53,22 @@ type rwLabels struct {
|
||||
labels labelMap
|
||||
}
|
||||
|
||||
func (r *rwLabels) LogLabels() {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
remoteLabelIDs := make([]string, len(r.labels))
|
||||
i := 0
|
||||
for labelID := range r.labels {
|
||||
remoteLabelIDs[i] = labelID
|
||||
i++
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"remoteLabelIDs": remoteLabelIDs,
|
||||
}).Debug("Logging remote label IDs stored in labelMap")
|
||||
}
|
||||
|
||||
func (r *rwLabels) Read() labelsRead {
|
||||
r.lock.RLock()
|
||||
return &rwLabelsRead{rw: r}
|
||||
@ -75,6 +93,15 @@ func (r *rwLabels) SetLabels(labels []proton.Label) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
labelIDs := xslices.Map(labels, func(label proton.Label) string {
|
||||
return label.ID
|
||||
})
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"pkg": "rwLabels",
|
||||
"labelIDs": labelIDs,
|
||||
}).Info("Setting labels")
|
||||
|
||||
r.labels = usertypes.GroupBy(labels, func(label proton.Label) string { return label.ID })
|
||||
}
|
||||
|
||||
@ -123,10 +150,20 @@ func (r rwLabelsWrite) GetLabels() []proton.Label {
|
||||
return r.rw.getLabelsUnsafe()
|
||||
}
|
||||
|
||||
func (r rwLabelsWrite) SetLabel(id string, label proton.Label) {
|
||||
func (r rwLabelsWrite) SetLabel(id string, label proton.Label, actionSource string) {
|
||||
logAction("SetLabel", actionSource, label.ID)
|
||||
r.rw.labels[id] = label
|
||||
}
|
||||
|
||||
func (r rwLabelsWrite) Delete(id string) {
|
||||
func (r rwLabelsWrite) Delete(id string, actionSource string) {
|
||||
logAction("Delete", actionSource, id)
|
||||
delete(r.rw.labels, id)
|
||||
}
|
||||
|
||||
func logAction(actionType, actionSource, labelID string) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"pkg": "rwLabelsWrite",
|
||||
"actionSource": actionSource,
|
||||
"labelID": labelID,
|
||||
}).Debug(actionType)
|
||||
}
|
||||
|
||||
@ -31,8 +31,9 @@ import (
|
||||
)
|
||||
|
||||
type SyncUpdateApplier struct {
|
||||
requestCh chan updateRequest
|
||||
replyCh chan updateReply
|
||||
requestCh chan updateRequest
|
||||
replyCh chan updateReply
|
||||
labelConflictManager *LabelConflictManager
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
func NewSyncUpdateApplier() *SyncUpdateApplier {
|
||||
func NewSyncUpdateApplier(labelConflictManager *LabelConflictManager) *SyncUpdateApplier {
|
||||
return &SyncUpdateApplier{
|
||||
requestCh: make(chan updateRequest),
|
||||
replyCh: make(chan updateReply),
|
||||
requestCh: make(chan updateRequest),
|
||||
replyCh: make(chan updateReply),
|
||||
labelConflictManager: labelConflictManager,
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,42 +113,9 @@ func (s *SyncUpdateApplier) ApplySyncUpdates(ctx context.Context, updates []sync
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SyncUpdateApplier) SyncSystemLabelsOnly(ctx context.Context, labels map[string]proton.Label) error {
|
||||
request := func(ctx context.Context, _ usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error) {
|
||||
updates := make([]imap.Update, 0, len(labels)*len(connectors))
|
||||
for _, label := range labels {
|
||||
if !WantLabel(label) {
|
||||
continue
|
||||
}
|
||||
|
||||
if label.Type != proton.LabelTypeSystem {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range connectors {
|
||||
update := newSystemMailboxCreatedUpdate(imap.MailboxID(label.ID), label.Name)
|
||||
updates = append(updates, update)
|
||||
c.publishUpdate(ctx, update)
|
||||
}
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
updates, err := s.sendRequest(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
||||
return fmt.Errorf("could not sync system labels: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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) {
|
||||
return syncLabels(ctx, labels, maps.Values(connectors))
|
||||
return syncLabels(ctx, labels, maps.Values(connectors), s.labelConflictManager)
|
||||
}
|
||||
|
||||
updates, err := s.sendRequest(ctx, request)
|
||||
@ -161,9 +130,11 @@ func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]pr
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
labelConflictResolver := labelConflictManager.NewConflictResolver(connectors)
|
||||
|
||||
// Create placeholder Folders/Labels mailboxes with the \Noselect attribute.
|
||||
for _, prefix := range []string{folderPrefix, labelPrefix} {
|
||||
for _, updateCh := range connectors {
|
||||
@ -188,7 +159,16 @@ func syncLabels(ctx context.Context, labels map[string]proton.Label, connectors
|
||||
}
|
||||
|
||||
case proton.LabelTypeFolder, proton.LabelTypeLabel:
|
||||
conflictUpdatesGenerator, err := labelConflictResolver.ResolveConflict(ctx, label, make(map[string]bool))
|
||||
if err != nil {
|
||||
return updates, err
|
||||
}
|
||||
|
||||
for _, updateCh := range connectors {
|
||||
conflictUpdates := conflictUpdatesGenerator()
|
||||
updateCh.publishUpdate(ctx, conflictUpdates...)
|
||||
updates = append(updates, conflictUpdates...)
|
||||
|
||||
update := newMailboxCreatedUpdate(imap.MailboxID(labelID), GetMailboxName(label))
|
||||
updateCh.publishUpdate(ctx, update)
|
||||
updates = append(updates, update)
|
||||
|
||||
100
internal/services/imapservice/utils.go
Normal file
100
internal/services/imapservice/utils.go
Normal file
@ -0,0 +1,100 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
|
||||
)
|
||||
|
||||
type connectorInterface interface {
|
||||
getSenderProtonAddress(p *parser.Parser) (proton.Address, error)
|
||||
getAddress(id string) (proton.Address, bool)
|
||||
getPrimaryAddress() (proton.Address, error)
|
||||
getAddressMode() usertypes.AddressMode
|
||||
logError(err error, errMsg string)
|
||||
}
|
||||
|
||||
func (s *Connector) logError(err error, errMsg string) {
|
||||
s.log.WithError(err).Warn(errMsg)
|
||||
}
|
||||
|
||||
func (s *Connector) getAddressMode() usertypes.AddressMode {
|
||||
return s.addressMode
|
||||
}
|
||||
|
||||
func (s *Connector) getPrimaryAddress() (proton.Address, error) {
|
||||
return s.identityState.GetPrimaryAddress()
|
||||
}
|
||||
|
||||
func (s *Connector) getAddress(id string) (proton.Address, bool) {
|
||||
return s.identityState.GetAddress(id)
|
||||
}
|
||||
|
||||
func getImportAddress(p *parser.Parser, isDraft bool, id string, conn connectorInterface) (proton.Address, error) {
|
||||
// addr is primary for combined mode or active for split mode
|
||||
address, ok := conn.getAddress(id)
|
||||
if !ok {
|
||||
return proton.Address{}, errors.New("could not find account address")
|
||||
}
|
||||
|
||||
// If the address is external and not BYOE - with sending enabled, then use the primary address as an import target.
|
||||
if address.Type == proton.AddressTypeExternal && !address.Send {
|
||||
var err error
|
||||
address, err = conn.getPrimaryAddress()
|
||||
if err != nil {
|
||||
return proton.Address{}, errors.New("could not get primary account address")
|
||||
}
|
||||
}
|
||||
|
||||
inCombinedMode := conn.getAddressMode() == usertypes.AddressModeCombined
|
||||
if !inCombinedMode {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
senderAddr, err := conn.getSenderProtonAddress(p)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errNoSenderAddressMatch) {
|
||||
conn.logError(err, "Could not get import address")
|
||||
}
|
||||
|
||||
// We did not find a match, so we use the default address.
|
||||
return address, nil
|
||||
}
|
||||
|
||||
if senderAddr.ID == address.ID {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// GODT-3185 / BRIDGE-120 In combined mode, in certain cases we adapt the address used for encryption.
|
||||
// - draft with non-default address in combined mode: using sender address
|
||||
// - import with non-default address in combined mode: using sender address
|
||||
// - import with non-default disabled address in combined mode: using sender address
|
||||
isSenderAddressDisabled := (!bool(senderAddr.Send)) || (senderAddr.Status != proton.AddressStatusEnabled)
|
||||
isSenderExternalNonBYOE := senderAddr.Type == proton.AddressTypeExternal && !bool(senderAddr.Send)
|
||||
|
||||
// Forbid drafts/imports for external non-BYOE addresses
|
||||
if isSenderExternalNonBYOE || (isDraft && isSenderAddressDisabled) {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
return senderAddr, nil
|
||||
}
|
||||
380
internal/services/imapservice/utils_test.go
Normal file
380
internal/services/imapservice/utils_test.go
Normal file
@ -0,0 +1,380 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testConnector struct {
|
||||
addressMode usertypes.AddressMode
|
||||
primaryAddress proton.Address
|
||||
senderAddress proton.Address
|
||||
imapAddress proton.Address
|
||||
senderAddressError error
|
||||
}
|
||||
|
||||
func (t *testConnector) getSenderProtonAddress(_ *parser.Parser) (proton.Address, error) {
|
||||
return t.senderAddress, t.senderAddressError
|
||||
}
|
||||
|
||||
func (t *testConnector) getAddress(_ string) (proton.Address, bool) {
|
||||
return t.imapAddress, true
|
||||
}
|
||||
|
||||
func (t *testConnector) getPrimaryAddress() (proton.Address, error) {
|
||||
return t.primaryAddress, nil
|
||||
}
|
||||
|
||||
func (t *testConnector) getAddressMode() usertypes.AddressMode {
|
||||
return t.addressMode
|
||||
}
|
||||
|
||||
func (t *testConnector) logError(_ error, _ string) {
|
||||
}
|
||||
|
||||
func Test_GetImportAddress_SplitMode(t *testing.T) {
|
||||
primaryAddress := proton.Address{
|
||||
ID: "1",
|
||||
Email: "primary@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
Status: proton.AddressStatusEnabled,
|
||||
}
|
||||
|
||||
imapAddressProton := proton.Address{
|
||||
ID: "2",
|
||||
Email: "imap@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
}
|
||||
|
||||
testConn := &testConnector{
|
||||
addressMode: usertypes.AddressModeSplit,
|
||||
primaryAddress: primaryAddress,
|
||||
imapAddress: imapAddressProton,
|
||||
}
|
||||
|
||||
// Import address is internal, we're creating a draft.
|
||||
// Expected: returned address is internal.
|
||||
addr, err := getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
|
||||
// Import address is internal, we're attempting to import a message.
|
||||
// Expected: returned address is internal.
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
|
||||
imapAddressBYOE := proton.Address{
|
||||
ID: "3",
|
||||
Email: "byoe@external.com",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeExternal,
|
||||
}
|
||||
|
||||
// IMAP address is BYOE, we're creating a draft
|
||||
// Expected: returned address is BYOE.
|
||||
testConn.imapAddress = imapAddressBYOE
|
||||
addr, err = getImportAddress(nil, true, imapAddressBYOE.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressBYOE.ID, addr.ID)
|
||||
require.Equal(t, imapAddressBYOE.Email, addr.Email)
|
||||
// IMAP address is BYOE, we're importing a message
|
||||
// Expected: returned address is BYOE.
|
||||
addr, err = getImportAddress(nil, false, imapAddressBYOE.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressBYOE.ID, addr.ID)
|
||||
require.Equal(t, imapAddressBYOE.Email, addr.Email)
|
||||
|
||||
imapAddressExternal := proton.Address{
|
||||
ID: "4",
|
||||
Email: "external@external.com",
|
||||
Send: false,
|
||||
Receive: false,
|
||||
Type: proton.AddressTypeExternal,
|
||||
}
|
||||
|
||||
// IMAP address is external, we're creating a draft.
|
||||
// Expected: returned address is primary.
|
||||
testConn.imapAddress = imapAddressExternal
|
||||
addr, err = getImportAddress(nil, true, imapAddressExternal.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, primaryAddress.ID, addr.ID)
|
||||
require.Equal(t, primaryAddress.Email, addr.Email)
|
||||
// IMAP address is external, we're trying to import.
|
||||
// Expected: returned address is primary.
|
||||
addr, err = getImportAddress(nil, false, imapAddressExternal.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, primaryAddress.ID, addr.ID)
|
||||
require.Equal(t, primaryAddress.Email, addr.Email)
|
||||
}
|
||||
|
||||
func Test_GetImportAddress_CombinedMode_ProtonAddresses(t *testing.T) {
|
||||
primaryAddress := proton.Address{
|
||||
ID: "1",
|
||||
Email: "primary@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
Status: proton.AddressStatusEnabled,
|
||||
}
|
||||
|
||||
imapAddressProton := proton.Address{
|
||||
ID: "2",
|
||||
Email: "imap@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
}
|
||||
|
||||
senderAddress := proton.Address{
|
||||
ID: "3",
|
||||
Email: "sender@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
Status: proton.AddressStatusEnabled,
|
||||
}
|
||||
|
||||
testConn := &testConnector{
|
||||
addressMode: usertypes.AddressModeCombined,
|
||||
primaryAddress: primaryAddress,
|
||||
imapAddress: imapAddressProton,
|
||||
senderAddress: senderAddress,
|
||||
}
|
||||
|
||||
// Both the sender address and the imap address are the same. We're creating a draft.
|
||||
// Expected: IMAP address is returned.
|
||||
testConn.senderAddress = imapAddressProton
|
||||
testConn.imapAddress = imapAddressProton
|
||||
addr, err := getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
// Both the sender address and the imap address are the same. We're trying to import
|
||||
// Expected: IMAP address is returned.
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
|
||||
// Sender address and imap address are different. Sender address is enabled and has sending enabled.
|
||||
// We're creating a draft.
|
||||
// Expected: Sender address is returned.
|
||||
testConn.senderAddress = senderAddress
|
||||
testConn.imapAddress = imapAddressProton
|
||||
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, senderAddress.ID, addr.ID)
|
||||
require.Equal(t, senderAddress.Email, addr.Email)
|
||||
// Sender address and imap address are different. Sender address is enabled and has sending enabled.
|
||||
// We're importing a message.
|
||||
// Expected: Sender address is returned.
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, senderAddress.ID, addr.ID)
|
||||
require.Equal(t, senderAddress.Email, addr.Email)
|
||||
|
||||
// Sender address and imap address are different. Sender address is disabled, but has sending enabled.
|
||||
// We're creating a draft message.
|
||||
// Expected: IMAP address is returned.
|
||||
senderAddress.Status = proton.AddressStatusDisabled
|
||||
testConn.senderAddress = senderAddress
|
||||
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
// Sender address and imap address are different. Sender address is disabled, but has sending enabled.
|
||||
// We're importing a message.
|
||||
// Expected: IMAP address is returned.
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, senderAddress.ID, addr.ID)
|
||||
require.Equal(t, senderAddress.Email, addr.Email)
|
||||
|
||||
// Sender address and imap address are different. Sender address is enabled, but has sending disabled.
|
||||
// We're creating a draft.
|
||||
// Expected: IMAP address is returned.
|
||||
senderAddress.Status = proton.AddressStatusEnabled
|
||||
senderAddress.Send = false
|
||||
testConn.senderAddress = senderAddress
|
||||
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
// Sender address and imap address are different. Sender address is enabled, but has sending disabled.
|
||||
// We're importing a message.
|
||||
// Expected: IMAP address is returned.
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, senderAddress.ID, addr.ID)
|
||||
require.Equal(t, senderAddress.Email, addr.Email)
|
||||
|
||||
// Sender address and imap address are different. But sender address is not an associated proton address.
|
||||
// We're creating a draft.
|
||||
// Expected: Sender address is returned.
|
||||
testConn.senderAddressError = errors.New("sender address is not associated with the account")
|
||||
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
// Sender address and imap address are different. But sender address is not an associated proton address.
|
||||
// We're importing a message.
|
||||
// Expected: Sender address is returned.
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
}
|
||||
|
||||
func Test_GetImportAddress_CombinedMode_ExternalAddresses(t *testing.T) {
|
||||
primaryAddress := proton.Address{
|
||||
ID: "1",
|
||||
Email: "primary@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
Status: proton.AddressStatusEnabled,
|
||||
}
|
||||
|
||||
imapAddressProton := proton.Address{
|
||||
ID: "2",
|
||||
Email: "imap@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
}
|
||||
|
||||
senderAddressExternal := proton.Address{
|
||||
ID: "3",
|
||||
Email: "sender@external.me",
|
||||
Send: false,
|
||||
Receive: false,
|
||||
Type: proton.AddressTypeExternal,
|
||||
Status: proton.AddressStatusEnabled,
|
||||
}
|
||||
|
||||
senderAddressExternalSecondary := proton.Address{
|
||||
ID: "4",
|
||||
Email: "sender2@external.me",
|
||||
Send: false,
|
||||
Receive: false,
|
||||
Type: proton.AddressTypeExternal,
|
||||
Status: proton.AddressStatusEnabled,
|
||||
}
|
||||
|
||||
testConn := &testConnector{
|
||||
addressMode: usertypes.AddressModeCombined,
|
||||
primaryAddress: primaryAddress,
|
||||
imapAddress: imapAddressProton,
|
||||
senderAddress: senderAddressExternal,
|
||||
}
|
||||
|
||||
// Sender address is external, and we're creating a draft.
|
||||
// Expected: IMAP address is returned.
|
||||
testConn.senderAddress = senderAddressExternal
|
||||
testConn.imapAddress = imapAddressProton
|
||||
addr, err := getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
// Sender address is external, and we're importing a message.
|
||||
// Expected: IMAP address is returned.
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, imapAddressProton.ID, addr.ID)
|
||||
require.Equal(t, imapAddressProton.Email, addr.Email)
|
||||
|
||||
// Sender and IMAP address are external, and we're trying to import.
|
||||
// Expected: Primary address is returned.
|
||||
testConn.imapAddress = senderAddressExternalSecondary
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, primaryAddress.ID, addr.ID)
|
||||
require.Equal(t, primaryAddress.Email, addr.Email)
|
||||
// Sender and IMAP address are external, and we're trying to create a draft.
|
||||
// Expected: Primary address is returned.
|
||||
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, primaryAddress.ID, addr.ID)
|
||||
require.Equal(t, primaryAddress.Email, addr.Email)
|
||||
}
|
||||
|
||||
func Test_GetImportAddress_CombinedMode_BYOEAddresses(t *testing.T) {
|
||||
primaryAddress := proton.Address{
|
||||
ID: "1",
|
||||
Email: "primary@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
Status: proton.AddressStatusEnabled,
|
||||
}
|
||||
|
||||
imapAddressProton := proton.Address{
|
||||
ID: "2",
|
||||
Email: "imap@proton.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeOriginal,
|
||||
}
|
||||
|
||||
senderAddressBYOE := proton.Address{
|
||||
ID: "3",
|
||||
Email: "sender@external.me",
|
||||
Send: true,
|
||||
Receive: true,
|
||||
Type: proton.AddressTypeExternal,
|
||||
Status: proton.AddressStatusEnabled,
|
||||
}
|
||||
|
||||
testConn := &testConnector{
|
||||
addressMode: usertypes.AddressModeCombined,
|
||||
primaryAddress: primaryAddress,
|
||||
imapAddress: imapAddressProton,
|
||||
senderAddress: senderAddressBYOE,
|
||||
}
|
||||
|
||||
// Sender address is BYOE, and we're creating a draft.
|
||||
// Expected: BYOE address is returned.
|
||||
testConn.senderAddress = senderAddressBYOE
|
||||
testConn.imapAddress = imapAddressProton
|
||||
addr, err := getImportAddress(nil, true, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, senderAddressBYOE.ID, addr.ID)
|
||||
require.Equal(t, senderAddressBYOE.Email, addr.Email)
|
||||
|
||||
// Sender address is BYOE, and we're importing a message.
|
||||
// Expected: BYOE address is returned.
|
||||
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, senderAddressBYOE.ID, addr.ID)
|
||||
require.Equal(t, senderAddressBYOE.Email, addr.Email)
|
||||
}
|
||||
@ -170,6 +170,14 @@ func (sm *Service) SetGluonDir(ctx context.Context, gluonDir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *Service) LogRemoteLabelIDs(ctx context.Context, provider imapservice.GluonIDProvider, addrID ...string) error {
|
||||
_, err := sm.requests.Send(ctx, &smRequestLogRemoteMailboxIDs{
|
||||
addrID: addrID,
|
||||
idProvider: provider,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *Service) RemoveIMAPUser(ctx context.Context, deleteData bool, provider imapservice.GluonIDProvider, addrID ...string) error {
|
||||
_, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
|
||||
withData: deleteData,
|
||||
@ -192,6 +200,10 @@ func (sm *Service) RemoveSMTPAccount(ctx context.Context, service *bridgesmtp.Se
|
||||
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) run(ctx context.Context, subscription events.Subscription) {
|
||||
eventSub := subscription.Add()
|
||||
defer subscription.Remove(eventSub)
|
||||
@ -244,6 +256,10 @@ func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
|
||||
sm.handleLoadedUserCountChange(ctx)
|
||||
}
|
||||
|
||||
case *smRequestLogRemoteMailboxIDs:
|
||||
err := sm.logRemoteLabelIDsFromServer(ctx, r.addrID, r.idProvider)
|
||||
request.Reply(ctx, nil, err)
|
||||
|
||||
case *smRequestRemoveIMAPUser:
|
||||
err := sm.handleRemoveIMAPUser(ctx, r.withData, r.idProvider, r.addrID...)
|
||||
request.Reply(ctx, nil, err)
|
||||
@ -311,6 +327,35 @@ func (sm *Service) handleAddIMAPUser(ctx context.Context,
|
||||
return sm.handleAddIMAPUserImpl(ctx, connector, addrID, idProvider, syncStateProvider)
|
||||
}
|
||||
|
||||
func (sm *Service) logRemoteLabelIDsFromServer(ctx context.Context, addrIDs []string, idProvider imapservice.GluonIDProvider) error {
|
||||
if sm.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
for _, addrID := range addrIDs {
|
||||
gluonID, ok := idProvider.GetGluonID(addrID)
|
||||
if !ok {
|
||||
sm.log.Warnf("Could not find Gluon ID for addrID %v", addrID)
|
||||
continue
|
||||
}
|
||||
|
||||
log := sm.log.WithFields(logrus.Fields{
|
||||
"addrID": addrID,
|
||||
"gluonID": gluonID,
|
||||
})
|
||||
|
||||
remoteLabelIDs, err := sm.imapServer.GetAllMailboxRemoteIDsForUser(ctx, gluonID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Could not obtain remote label IDs for user")
|
||||
continue
|
||||
}
|
||||
|
||||
log.WithField("remoteLabelIDs", remoteLabelIDs).Debug("Logging Gluon remote Label IDs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
|
||||
connector connector.Connector,
|
||||
addrID string,
|
||||
@ -723,3 +768,8 @@ type smRequestAddSMTPAccount struct {
|
||||
type smRequestRemoveSMTPAccount struct {
|
||||
account *bridgesmtp.Service
|
||||
}
|
||||
|
||||
type smRequestLogRemoteMailboxIDs struct {
|
||||
addrID []string
|
||||
idProvider imapservice.GluonIDProvider
|
||||
}
|
||||
|
||||
@ -138,11 +138,10 @@ func (t *Handler) run(ctx context.Context,
|
||||
}
|
||||
|
||||
if syncStatus.IsComplete() {
|
||||
t.log.Info("Sync already complete, only system labels will be updated")
|
||||
|
||||
if err := updateApplier.SyncSystemLabelsOnly(ctx, labels); err != nil {
|
||||
t.log.WithError(err).Error("Failed to sync system labels")
|
||||
t.log.Info("Sync already complete, updating labels")
|
||||
|
||||
if err := updateApplier.SyncLabels(ctx, labels); err != nil {
|
||||
t.log.WithError(err).Error("Failed to sync labels")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -74,8 +74,7 @@ func TestTask_NoStateAndSucceeds(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
call1 := tt.updateApplier.EXPECT().SyncLabels(gomock.Any(), gomock.Eq(labels)).Times(1).Return(nil)
|
||||
tt.updateApplier.EXPECT().SyncSystemLabelsOnly(gomock.Any(), gomock.Eq(labels)).After(call1).Times(1).Return(nil)
|
||||
tt.updateApplier.EXPECT().SyncLabels(gomock.Any(), gomock.Eq(labels)).Times(2).Return(nil)
|
||||
}
|
||||
|
||||
{
|
||||
@ -203,7 +202,7 @@ func TestTask_StateHasSyncedState(t *testing.T) {
|
||||
}, nil
|
||||
})
|
||||
|
||||
tt.updateApplier.EXPECT().SyncSystemLabelsOnly(gomock.Any(), gomock.Eq(labels)).Return(nil)
|
||||
tt.updateApplier.EXPECT().SyncLabels(gomock.Any(), gomock.Eq(labels)).Return(nil)
|
||||
|
||||
err := tt.task.run(context.Background(), tt.syncReporter, labels, tt.updateApplier, tt.messageBuilder)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -80,7 +80,6 @@ type MessageBuilder interface {
|
||||
|
||||
type UpdateApplier interface {
|
||||
ApplySyncUpdates(ctx context.Context, updates []BuildResult) error
|
||||
SyncSystemLabelsOnly(ctx context.Context, labels map[string]proton.Label) error
|
||||
SyncLabels(ctx context.Context, labels map[string]proton.Label) error
|
||||
}
|
||||
|
||||
|
||||
@ -548,20 +548,6 @@ func (mr *MockUpdateApplierMockRecorder) SyncLabels(arg0, arg1 interface{}) *gom
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncLabels", reflect.TypeOf((*MockUpdateApplier)(nil).SyncLabels), arg0, arg1)
|
||||
}
|
||||
|
||||
// SyncSystemLabelsOnly mocks base method.
|
||||
func (m *MockUpdateApplier) SyncSystemLabelsOnly(arg0 context.Context, arg1 map[string]proton.Label) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SyncSystemLabelsOnly", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SyncSystemLabelsOnly indicates an expected call of SyncSystemLabelsOnly.
|
||||
func (mr *MockUpdateApplierMockRecorder) SyncSystemLabelsOnly(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncSystemLabelsOnly", reflect.TypeOf((*MockUpdateApplier)(nil).SyncSystemLabelsOnly), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockMessageBuilder is a mock of MessageBuilder interface.
|
||||
type MockMessageBuilder struct {
|
||||
ctrl *gomock.Controller
|
||||
|
||||
@ -41,6 +41,7 @@ const (
|
||||
IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled"
|
||||
UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled"
|
||||
UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled"
|
||||
LabelConflictResolverDisabled = "InboxBridgeLabelConflictResolverDisabled"
|
||||
)
|
||||
|
||||
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
|
||||
|
||||
@ -282,6 +282,7 @@ func newImpl(
|
||||
user.maxSyncMemory,
|
||||
showAllMail,
|
||||
observabilityService,
|
||||
getFlagValueFn,
|
||||
)
|
||||
|
||||
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService)
|
||||
@ -739,7 +740,7 @@ func (user *User) protonAddresses() []proton.Address {
|
||||
}
|
||||
|
||||
addresses := xslices.Filter(maps.Values(apiAddrs), func(addr proton.Address) bool {
|
||||
return addr.Status == proton.AddressStatusEnabled && addr.Type != proton.AddressTypeExternal
|
||||
return addr.Status == proton.AddressStatusEnabled && (addr.IsBYOEAddress() || addr.Type != proton.AddressTypeExternal)
|
||||
})
|
||||
|
||||
slices.SortFunc(addresses, func(a, b proton.Address) bool {
|
||||
|
||||
@ -110,7 +110,7 @@ func withAccount(tb testing.TB, s *server.Server, username, password string, ali
|
||||
addrIDs := []string{addrID}
|
||||
|
||||
for _, email := range aliases {
|
||||
addrID, err := s.CreateAddress(userID, email, []byte(password))
|
||||
addrID, err := s.CreateAddress(userID, email, []byte(password), true)
|
||||
require.NoError(tb, err)
|
||||
require.NoError(tb, s.ChangeAddressDisplayName(userID, addrID, email+" (Display Name)"))
|
||||
|
||||
|
||||
@ -63,6 +63,10 @@ func GetHelper(vaultDir string) (string, error) {
|
||||
}
|
||||
|
||||
func SetHelper(vaultDir, helper string) error {
|
||||
if helper == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings, err := LoadKeychainSettings(vaultDir)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -82,11 +82,11 @@ func (kcl *List) GetDefaultHelper() string {
|
||||
return kcl.defaultHelper
|
||||
}
|
||||
|
||||
// NewKeychain creates a new native keychain.
|
||||
func NewKeychain(preferred, keychainName string, helpers Helpers, defaultHelper string) (*Keychain, error) {
|
||||
// NewKeychain creates a new native keychain. It also returns the keychain helper used to access the keychain.
|
||||
func NewKeychain(preferred, keychainName string, helpers Helpers, defaultHelper string) (kc *Keychain, usedKeychainHelper string, err error) {
|
||||
// There must be at least one keychain helper available.
|
||||
if len(helpers) < 1 {
|
||||
return nil, ErrNoKeychain
|
||||
return nil, "", ErrNoKeychain
|
||||
}
|
||||
|
||||
// If the preferred keychain is unsupported, fallback to the default one.
|
||||
@ -97,16 +97,16 @@ func NewKeychain(preferred, keychainName string, helpers Helpers, defaultHelper
|
||||
// Load the user's preferred keychain helper.
|
||||
helperConstructor, ok := helpers[preferred]
|
||||
if !ok {
|
||||
return nil, ErrNoKeychain
|
||||
return nil, "", ErrNoKeychain
|
||||
}
|
||||
|
||||
// Construct the keychain helper.
|
||||
helper, err := helperConstructor(hostURL(keychainName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, preferred, err
|
||||
}
|
||||
|
||||
return newKeychain(helper, hostURL(keychainName)), nil
|
||||
return newKeychain(helper, hostURL(keychainName)), preferred, nil
|
||||
}
|
||||
|
||||
func newKeychain(helper credentials.Helper, url string) *Keychain {
|
||||
|
||||
@ -120,7 +120,7 @@ func TestIsErrKeychainNoItem(t *testing.T) {
|
||||
helpers := NewList().GetHelpers()
|
||||
|
||||
for helperName := range helpers {
|
||||
kc, err := NewKeychain(helperName, "bridge-test", helpers, helperName)
|
||||
kc, _, err := NewKeychain(helperName, "bridge-test", helpers, helperName)
|
||||
r.NoError(err)
|
||||
|
||||
_, _, err = kc.Get("non-existing")
|
||||
|
||||
@ -28,9 +28,7 @@ main(){
|
||||
jq -r '.finding | select( (.osv != null) and (.trace[0].function != null) ) | .osv ' < vulns.json > vulns_osv_ids.txt
|
||||
|
||||
ignore GO-2023-2328 "GODT-3124 RESTY race condition"
|
||||
ignore GO-2025-3373 "BRIDGE-315 stdlib crypto/x509"
|
||||
ignore GO-2025-3420 "BRIDGE-315 stdlib net/http"
|
||||
ignore GO-2025-3447 "BRIDGE-315 stdlib crypto/internal/nistec"
|
||||
ignore GO-2025-3563 "BRIDGE-346 net/http request smuggling"
|
||||
|
||||
has_vulns
|
||||
|
||||
|
||||
Reference in New Issue
Block a user