mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 675b37a2fa | |||
| 9d4415d8cc | |||
| 05623a9e49 |
@ -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)
|
||||
|
||||
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
|
||||
|
||||
3
go.mod
3
go.mod
@ -7,7 +7,7 @@ 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.20250324123053-2abce471ad71
|
||||
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.20250417134000-e624a080f7ba
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
||||
@ -114,6 +114,7 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/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
|
||||
|
||||
24
go.sum
24
go.sum
@ -36,8 +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.20250324123053-2abce471ad71 h1:UC8SLrS6QbBeOUM8FJugyNoeV5gRGoQCwNePAMxuM20=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250324123053-2abce471ad71/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
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,14 +47,6 @@ github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDx
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-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.20250217140732-2e531f21de4c h1:dxnbB+ov77BDj1LC35fKZ14hLoTpU6OTpZySwxarVx0=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409092940-13ddc20a05a1 h1:u3G9UB8prOnzOneOf0JFCIVnMRLiK4QgEpPQVu9Y8Q4=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409092940-13ddc20a05a1/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409131808-0bbc8e7c32db h1:mOtbY5BB2eNr2QmbZhFn5EnsJcimTntPB6akN2r+AuE=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409131808-0bbc8e7c32db/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250410050801-92de6e7c8517 h1:70JoDgXxfil4hbDoYGF98rMd47Rld6wXWyFAw4uFOTY=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250410050801-92de6e7c8517/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba h1:DFBngZ7u/f69flRFzPp6Ipo6PKEyflJlA5OCh52yDB4=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||
@ -506,8 +500,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.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=
|
||||
@ -565,8 +557,6 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.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=
|
||||
@ -583,8 +573,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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=
|
||||
@ -625,8 +613,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.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=
|
||||
@ -648,8 +634,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.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=
|
||||
|
||||
@ -96,7 +96,7 @@ func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
||||
|
||||
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
||||
copyTrustedPins(checker)
|
||||
checker.trustedPins = append(checker.trustedPins, `pin-sha256="hgraU1+uoS6kjiJaH5G+BiqQoyiIml1Nat+2FiUAcII="`)
|
||||
checker.trustedPins = append(checker.trustedPins, `pin-sha256="FlvTPG/nIMKtOj9nelnEjujwSZ5EDyfiKYxZgbXREls="`)
|
||||
_, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443")
|
||||
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@ -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) {
|
||||
func (s *Connector) publishUpdate(_ context.Context, updates ...imap.Update) {
|
||||
for _, update := range updates {
|
||||
s.updateCh.Enqueue(update)
|
||||
}
|
||||
}
|
||||
|
||||
func fixGODT3003Labels(
|
||||
@ -903,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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -36,6 +37,8 @@ type IMAPServerManager interface {
|
||||
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{}
|
||||
@ -67,6 +70,10 @@ func (n NullIMAPServerManager) LogRemoteLabelIDs(
|
||||
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"
|
||||
@ -92,6 +93,7 @@ type Service struct {
|
||||
isSyncing atomic.Bool
|
||||
|
||||
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)
|
||||
|
||||
@ -157,6 +161,7 @@ func NewService(
|
||||
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||
|
||||
observabilitySender: observabilitySender,
|
||||
labelConflictManager: labelConflictManager,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
@ -87,7 +90,17 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []
|
||||
|
||||
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) {
|
||||
@ -136,8 +149,19 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([
|
||||
// Update the label in the map.
|
||||
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),
|
||||
|
||||
@ -33,6 +33,7 @@ import (
|
||||
type SyncUpdateApplier struct {
|
||||
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),
|
||||
labelConflictManager: labelConflictManager,
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +115,7 @@ func (s *SyncUpdateApplier) ApplySyncUpdates(ctx context.Context, updates []sync
|
||||
|
||||
func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]proton.Label) error {
|
||||
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)
|
||||
@ -128,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 {
|
||||
@ -155,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)
|
||||
|
||||
@ -200,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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user