diff --git a/go.mod b/go.mod index 3c5f0446..1fca1111 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.1.1 - github.com/ProtonMail/gluon v0.11.1-0.20221006090633-634f8b906f8d + github.com/ProtonMail/gluon v0.11.1-0.20221007225442-6f4197f47f09 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-rfc5322 v0.11.0 github.com/ProtonMail/gopenpgp/v2 v2.4.10 @@ -38,7 +38,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 github.com/urfave/cli/v2 v2.16.3 - gitlab.protontech.ch/go/liteapi v0.33.2-0.20221006110959-d7a4b4b315b8 + gitlab.protontech.ch/go/liteapi v0.33.2-0.20221007210933-605ca74449b7 golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/net v0.1.0 golang.org/x/sys v0.1.0 @@ -60,8 +60,6 @@ require ( github.com/andybalholm/cascadia v1.3.1 // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chzyer/test v1.0.0 // indirect github.com/cloudflare/circl v1.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect @@ -69,9 +67,6 @@ require ( github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgraph-io/badger/v3 v3.2103.2 // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect github.com/elastic/go-windows v1.0.1 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect @@ -83,12 +78,7 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.0 // indirect github.com/gofrs/uuid v4.3.0+incompatible // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.0.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/flatbuffers v2.0.8+incompatible // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-memdb v1.3.3 // indirect @@ -96,7 +86,6 @@ require ( github.com/hashicorp/hcl/v2 v2.14.0 // indirect github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.15.9 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect @@ -116,7 +105,6 @@ require ( github.com/ugorji/go/codec v1.2.7 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zclconf/go-cty v1.11.0 // indirect - go.opencensus.io v0.23.0 // indirect golang.org/x/crypto v0.1.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect diff --git a/go.sum b/go.sum index 89db2edc..4df27693 100644 --- a/go.sum +++ b/go.sum @@ -22,19 +22,14 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 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/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= -github.com/ProtonMail/gluon v0.11.1-0.20221004153055-7d144337dbd0 h1:SsacIP40QP64FNZrBlm5XDLHZMIx0i36mUYmTSWI2Y4= -github.com/ProtonMail/gluon v0.11.1-0.20221004153055-7d144337dbd0/go.mod h1:9k3URQEASX9XSA+JEcukjIiK3S6aR9GzhLhwccy8AnI= -github.com/ProtonMail/gluon v0.11.1-0.20221006085838-c527b37bb418 h1:YvWdTvj2s+ZqEy0HwCIRFnGcWRhF0OY0EuIiknGChwM= -github.com/ProtonMail/gluon v0.11.1-0.20221006085838-c527b37bb418/go.mod h1:9k3URQEASX9XSA+JEcukjIiK3S6aR9GzhLhwccy8AnI= -github.com/ProtonMail/gluon v0.11.1-0.20221006090633-634f8b906f8d h1:EgrtTr3sx5cHkkd8/fviO5YOIcT9iOTibqJek2uhwM0= -github.com/ProtonMail/gluon v0.11.1-0.20221006090633-634f8b906f8d/go.mod h1:9k3URQEASX9XSA+JEcukjIiK3S6aR9GzhLhwccy8AnI= +github.com/ProtonMail/gluon v0.11.1-0.20221007225442-6f4197f47f09 h1:MtgdjqUH3YCfjXp9zn/9NvhDR7nQ11XBvUVojwK5prs= +github.com/ProtonMail/gluon v0.11.1-0.20221007225442-6f4197f47f09/go.mod h1:XW/gcr4jErc5bX5yMqkUq3U+AucC2QZHJ5L231k3Nw4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= @@ -72,7 +67,6 @@ github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3 github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -83,12 +77,7 @@ github.com/bradenaw/juniper v0.8.0 h1:sdanLNdJbLjcLj993VYIwUHlUVkLzvgiD/x9O7cvvx github.com/bradenaw/juniper v0.8.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= @@ -97,16 +86,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec= github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -128,16 +112,8 @@ github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= -github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= -github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VRvi4= github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM= github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= @@ -154,10 +130,6 @@ github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwo github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik= github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -199,16 +171,8 @@ github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -217,31 +181,13 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -250,7 +196,6 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -312,11 +257,7 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -330,7 +271,6 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -389,7 +329,6 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -405,7 +344,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -423,17 +361,13 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= @@ -453,50 +387,31 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0= github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= -gitlab.protontech.ch/go/liteapi v0.32.1 h1:EiaLP+LkVVDqFxU6Uux4w5HHfPj6dSnOILJPzIny5l4= -gitlab.protontech.ch/go/liteapi v0.32.1/go.mod h1:SVxEeF4uYYYpSlfeAj2ZqluVEP95pbZ8LyoieSxU0pM= -gitlab.protontech.ch/go/liteapi v0.33.0 h1:lkerCG7Y3Tn+ICPwVkyZxPu5jJjiTTfaYiuqUwXRF0E= -gitlab.protontech.ch/go/liteapi v0.33.0/go.mod h1:+70trwxSrBP1fU1m2q5wzhYkr9/NUrFGuBr6cKl3ixk= -gitlab.protontech.ch/go/liteapi v0.33.1 h1:Ks8YdojRwYTLTUmGLG9MzFvxNeiwYJYQEaTHUQjOvsA= -gitlab.protontech.ch/go/liteapi v0.33.1/go.mod h1:9nsslyEJn7Utbielp4c+hc7qT6hqIJ52aGFR/tX+tYk= -gitlab.protontech.ch/go/liteapi v0.33.2-0.20221006095946-fc4061f2140b h1:Obu2CCCYdVi3NJmoYf/iJco1mat6EzJezInKoUTo+Dc= -gitlab.protontech.ch/go/liteapi v0.33.2-0.20221006095946-fc4061f2140b/go.mod h1:9nsslyEJn7Utbielp4c+hc7qT6hqIJ52aGFR/tX+tYk= -gitlab.protontech.ch/go/liteapi v0.33.2-0.20221006105817-e76abecc140a h1:dt6BahWRcy88dcXnEbm9m1X1W+RCZaVlo3W45V+vReQ= -gitlab.protontech.ch/go/liteapi v0.33.2-0.20221006105817-e76abecc140a/go.mod h1:9nsslyEJn7Utbielp4c+hc7qT6hqIJ52aGFR/tX+tYk= -gitlab.protontech.ch/go/liteapi v0.33.2-0.20221006110959-d7a4b4b315b8 h1:/ZQr46sMG1H2ykKqBeYtScD7azHo7vgAkswmqGS0eHM= -gitlab.protontech.ch/go/liteapi v0.33.2-0.20221006110959-d7a4b4b315b8/go.mod h1:9nsslyEJn7Utbielp4c+hc7qT6hqIJ52aGFR/tX+tYk= +gitlab.protontech.ch/go/liteapi v0.33.2-0.20221007210933-605ca74449b7 h1:Hef7jPRzcfLOvOUHYoQ6efaI7p7/aT5kpZDqJ29owNI= +gitlab.protontech.ch/go/liteapi v0.33.2-0.20221007210933-605ca74449b7/go.mod h1:9nsslyEJn7Utbielp4c+hc7qT6hqIJ52aGFR/tX+tYk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -527,8 +442,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -546,9 +459,6 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -565,8 +475,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A= golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -576,7 +484,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -588,9 +495,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -626,7 +531,6 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -636,8 +540,6 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa h1:uKcci2q7Qtp6nMTC/AAvfNUAldFtJuHWV9/5QWiypts= @@ -664,27 +566,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ= google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= @@ -692,7 +580,6 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -714,7 +601,6 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= diff --git a/internal/app/app.go b/internal/app/app.go index aa5ed425..1af75cfc 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -84,13 +84,13 @@ func run(c *cli.Context) error { // Start CPU profile if requested. if c.Bool(flagCPUProfile) { - p := profile.Start(profile.CPUProfile, profile.ProfilePath("cpu.pprof")) + p := profile.Start(profile.CPUProfile, profile.ProfilePath(".")) defer p.Stop() } // Start memory profile if requested. if c.Bool(flagMemProfile) { - p := profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath("mem.pprof")) + p := profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")) defer p.Stop() } diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index e4314abc..c6e94d28 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -69,6 +69,9 @@ type Bridge struct { // errors contains errors encountered during startup. errors []error + + // stopCh is used to stop ongoing goroutines when the bridge is closed. + stopCh chan struct{} } // New creates a new bridge. @@ -153,6 +156,8 @@ func New( focusService: focusService, autostarter: autostarter, locator: locator, + + stopCh: make(chan struct{}), } api.AddStatusObserver(func(status liteapi.Status) { @@ -232,12 +237,8 @@ func (bridge *Bridge) GetErrors() []error { } func (bridge *Bridge) Close(ctx context.Context) error { - // Abort any ongoing syncs. - for _, user := range bridge.users { - if err := user.AbortSync(ctx); err != nil { - return fmt.Errorf("failed to abort sync: %w", err) - } - } + // Stop ongoing operations such as connectivity checks. + close(bridge.stopCh) // Close the IMAP server. if err := bridge.closeIMAP(ctx); err != nil { @@ -251,7 +252,7 @@ func (bridge *Bridge) Close(ctx context.Context) error { // Close all users. for _, user := range bridge.users { - if err := user.Close(ctx); err != nil { + if err := user.Close(); err != nil { logrus.WithError(err).Error("Failed to close user") } } @@ -335,6 +336,9 @@ func (bridge *Bridge) onStatusDown() { case <-upCh: return + case <-bridge.stopCh: + return + case <-time.After(backoff): if err := bridge.api.Ping(ctx); err != nil { logrus.WithError(err).Debug("Failed to ping API") diff --git a/internal/bridge/bridge_test.go b/internal/bridge/bridge_test.go index 0d7e1221..52c8f1f1 100644 --- a/internal/bridge/bridge_test.go +++ b/internal/bridge/bridge_test.go @@ -25,20 +25,17 @@ import ( "gitlab.protontech.ch/go/liteapi/server/backend" ) -const ( - username = "username" -) - -var password = []byte("password") - var ( + username = "username" + password = []byte("password") + v2_3_0 = semver.MustParse("2.3.0") v2_4_0 = semver.MustParse("2.4.0") ) func init() { - user.DefaultEventPeriod = 100 * time.Millisecond - user.DefaultEventJitter = 0 + user.EventPeriod = 100 * time.Millisecond + user.EventJitter = 0 backend.GenerateKey = tests.FastGenerateKey certs.GenerateCert = tests.FastGenerateCert } diff --git a/internal/bridge/imap.go b/internal/bridge/imap.go index 45548fae..792cef9d 100644 --- a/internal/bridge/imap.go +++ b/internal/bridge/imap.go @@ -74,7 +74,7 @@ func getGluonDir(encVault *vault.Vault) (string, error) { if empty { if err := encVault.ForUser(func(user *vault.User) error { - return user.SetSync(false) + return user.ClearSyncStatus() }); err != nil { return "", fmt.Errorf("failed to reset user sync status: %w", err) } diff --git a/internal/bridge/user.go b/internal/bridge/user.go index 2a766ede..e960a4cf 100644 --- a/internal/bridge/user.go +++ b/internal/bridge/user.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/ProtonMail/gluon/imap" - "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/v2/internal/events" "github.com/ProtonMail/proton-bridge/v2/internal/user" "github.com/ProtonMail/proton-bridge/v2/internal/vault" @@ -86,6 +85,10 @@ func (bridge *Bridge) LoginUser( return "", err } + if _, ok := bridge.users[auth.UserID]; ok { + return "", ErrUserAlreadyLoggedIn + } + if auth.TwoFA.Enabled == liteapi.TOTPEnabled { totp, err := getTOTP() if err != nil { @@ -110,16 +113,26 @@ func (bridge *Bridge) LoginUser( keyPass = password } - apiUser, apiAddrs, userKR, addrKRs, saltedKeyPass, err := client.Unlock(ctx, keyPass) + apiUser, err := client.GetUser(ctx) if err != nil { return "", err } - if err := bridge.addUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, auth.UID, auth.RefreshToken, saltedKeyPass); err != nil { + salts, err := client.GetSalts(ctx) + if err != nil { return "", err } - return apiUser.ID, nil + saltedKeyPass, err := salts.SaltForKey(keyPass, apiUser.Keys.Primary().ID) + if err != nil { + return "", err + } + + if err := bridge.addUser(ctx, client, apiUser, auth.UID, auth.RefreshToken, saltedKeyPass); err != nil { + return "", err + } + + return auth.UserID, nil } // LogoutUser logs out the given user. @@ -158,10 +171,6 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va return fmt.Errorf("address mode is already %q", mode) } - if err := user.AbortSync(ctx); err != nil { - return fmt.Errorf("failed to abort sync: %w", err) - } - for _, gluonID := range user.GetGluonIDs() { if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil { return fmt.Errorf("failed to remove user from IMAP server: %w", err) @@ -181,8 +190,6 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va AddressMode: mode, }) - user.DoSync(ctx) - return nil } @@ -220,12 +227,12 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error { return fmt.Errorf("failed to create API client: %w", err) } - apiUser, apiAddrs, userKR, addrKRs, err := client.UnlockSalted(ctx, user.KeyPass()) + apiUser, err := client.GetUser(ctx) if err != nil { - return fmt.Errorf("failed to unlock user: %w", err) + return fmt.Errorf("failed to get user: %w", err) } - if err := bridge.addUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, auth.UID, auth.RefreshToken, user.KeyPass()); err != nil { + if err := bridge.addUser(ctx, client, apiUser, auth.UID, auth.RefreshToken, user.KeyPass()); err != nil { return fmt.Errorf("failed to add user: %w", err) } @@ -241,27 +248,20 @@ func (bridge *Bridge) addUser( ctx context.Context, client *liteapi.Client, apiUser liteapi.User, - apiAddrs []liteapi.Address, - userKR *crypto.KeyRing, - addrKRs map[string]*crypto.KeyRing, authUID, authRef string, saltedKeyPass []byte, ) error { - if _, ok := bridge.users[apiUser.ID]; ok { - return ErrUserAlreadyLoggedIn - } - var user *user.User if slices.Contains(bridge.vault.GetUserIDs(), apiUser.ID) { - existingUser, err := bridge.addExistingUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, authUID, authRef, saltedKeyPass) + existingUser, err := bridge.addExistingUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass) if err != nil { return fmt.Errorf("failed to add existing user: %w", err) } user = existingUser } else { - newUser, err := bridge.addNewUser(ctx, client, apiUser, apiAddrs, userKR, addrKRs, authUID, authRef, saltedKeyPass) + newUser, err := bridge.addNewUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass) if err != nil { return fmt.Errorf("failed to add new user: %w", err) } @@ -269,11 +269,16 @@ func (bridge *Bridge) addUser( user = newUser } - // Connects the user's address(es) to gluon. + // Connect the user's address(es) to gluon. if err := bridge.addIMAPUser(ctx, user); err != nil { return fmt.Errorf("failed to add IMAP user: %w", err) } + // Connect the user's address(es) to the SMTP server. + if err := bridge.smtpBackend.addUser(user); err != nil { + return fmt.Errorf("failed to add user to SMTP backend: %w", err) + } + // Handle events coming from the user before forwarding them to the bridge. // For example, if the user's addresses change, we need to update them in gluon. go func() { @@ -299,11 +304,6 @@ func (bridge *Bridge) addUser( return nil }) - // TODO: Replace this with proper sync manager. - if !user.HasSync() { - user.DoSync(ctx) - } - bridge.publish(events.UserLoggedIn{ UserID: user.ID(), }) @@ -315,9 +315,6 @@ func (bridge *Bridge) addNewUser( ctx context.Context, client *liteapi.Client, apiUser liteapi.User, - apiAddrs []liteapi.Address, - userKR *crypto.KeyRing, - addrKRs map[string]*crypto.KeyRing, authUID, authRef string, saltedKeyPass []byte, ) (*user.User, error) { @@ -326,15 +323,11 @@ func (bridge *Bridge) addNewUser( return nil, err } - user, err := user.New(ctx, vaultUser, client, apiUser, apiAddrs, userKR, addrKRs) + user, err := user.New(ctx, vaultUser, client, apiUser) if err != nil { return nil, err } - if err := bridge.smtpBackend.addUser(user); err != nil { - return nil, err - } - bridge.users[apiUser.ID] = user return user, nil @@ -344,9 +337,6 @@ func (bridge *Bridge) addExistingUser( ctx context.Context, client *liteapi.Client, apiUser liteapi.User, - apiAddrs []liteapi.Address, - userKR *crypto.KeyRing, - addrKRs map[string]*crypto.KeyRing, authUID, authRef string, saltedKeyPass []byte, ) (*user.User, error) { @@ -363,15 +353,11 @@ func (bridge *Bridge) addExistingUser( return nil, err } - user, err := user.New(ctx, vaultUser, client, apiUser, apiAddrs, userKR, addrKRs) + user, err := user.New(ctx, vaultUser, client, apiUser) if err != nil { return nil, err } - if err := bridge.smtpBackend.addUser(user); err != nil { - return nil, err - } - bridge.users[apiUser.ID] = user return user, nil @@ -386,11 +372,6 @@ func (bridge *Bridge) logoutUser(ctx context.Context, userID string, withAPI, wi return ErrNoSuchUser } - // TODO: The sync should be canceled by the sync manager. - if err := user.AbortSync(ctx); err != nil { - return fmt.Errorf("failed to abort user sync: %w", err) - } - if err := bridge.smtpBackend.removeUser(user); err != nil { return fmt.Errorf("failed to remove SMTP user: %w", err) } @@ -407,7 +388,7 @@ func (bridge *Bridge) logoutUser(ctx context.Context, userID string, withAPI, wi } } - if err := user.Close(ctx); err != nil { + if err := user.Close(); err != nil { return fmt.Errorf("failed to close user: %w", err) } diff --git a/internal/events/send.go b/internal/events/send.go deleted file mode 100644 index bc6303a8..00000000 --- a/internal/events/send.go +++ /dev/null @@ -1,9 +0,0 @@ -package events - -type MessageSent struct { - eventBase - - UserID string - AddressID string - MessageID string -} diff --git a/internal/events/sync.go b/internal/events/sync.go index 14413c65..0932a81c 100644 --- a/internal/events/sync.go +++ b/internal/events/sync.go @@ -22,3 +22,10 @@ type SyncFinished struct { UserID string } + +type SyncFailed struct { + eventBase + + UserID string + Err error +} diff --git a/internal/pool/pool.go b/internal/pool/pool.go index aafb42f9..6b1b33ae 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -9,7 +9,7 @@ import ( ) // ErrJobCancelled indicates the job was cancelled. -var ErrJobCancelled = errors.New("Job cancelled by surrounding context") +var ErrJobCancelled = errors.New("job cancelled by surrounding context") // Pool is a worker pool that handles input of type In and returns results of type Out. type Pool[In comparable, Out any] struct { diff --git a/internal/safe/map.go b/internal/safe/map.go new file mode 100644 index 00000000..fee77515 --- /dev/null +++ b/internal/safe/map.go @@ -0,0 +1,97 @@ +package safe + +import ( + "sync" + + "golang.org/x/exp/maps" +) + +type Map[Key comparable, Val any] struct { + data map[Key]Val + lock sync.RWMutex +} + +func NewMap[Key comparable, Val any](from map[Key]Val) *Map[Key, Val] { + m := &Map[Key, Val]{ + data: make(map[Key]Val), + } + + for key, val := range from { + m.Set(key, val) + } + + return m +} + +func (m *Map[Key, Val]) Get(key Key, fn func(val Val)) bool { + m.lock.RLock() + defer m.lock.RUnlock() + + val, ok := m.data[key] + if !ok { + return false + } + + fn(val) + + return true +} + +func (m *Map[Key, Val]) GetErr(key Key, fn func(val Val) error) (bool, error) { + m.lock.RLock() + defer m.lock.RUnlock() + + val, ok := m.data[key] + if !ok { + return false, nil + } + + return true, fn(val) +} + +func (m *Map[Key, Val]) Set(key Key, val Val) { + m.lock.Lock() + defer m.lock.Unlock() + + m.data[key] = val +} + +func (m *Map[Key, Val]) Keys(fn func(keys []Key)) { + m.lock.RLock() + defer m.lock.RUnlock() + + fn(maps.Keys(m.data)) +} + +func (m *Map[Key, Val]) Values(fn func(vals []Val)) { + m.lock.RLock() + defer m.lock.RUnlock() + + fn(maps.Values(m.data)) +} + +func GetMap[Key comparable, Val, Ret any](m *Map[Key, Val], key Key, fn func(val Val) Ret) (Ret, bool) { + m.lock.RLock() + defer m.lock.RUnlock() + + val, ok := m.data[key] + if !ok { + return *new(Ret), false + } + + return fn(val), true +} + +func GetMapErr[Key comparable, Val, Ret any](m *Map[Key, Val], key Key, fn func(val Val) (Ret, error)) (Ret, bool, error) { + m.lock.RLock() + defer m.lock.RUnlock() + + val, ok := m.data[key] + if !ok { + return *new(Ret), false, nil + } + + ret, err := fn(val) + + return ret, true, err +} diff --git a/internal/safe/slice.go b/internal/safe/slice.go new file mode 100644 index 00000000..623682a1 --- /dev/null +++ b/internal/safe/slice.go @@ -0,0 +1,53 @@ +package safe + +import "sync" + +type Slice[Val any] struct { + data []Val + lock sync.RWMutex +} + +func NewSlice[Val any](from []Val) *Slice[Val] { + s := &Slice[Val]{ + data: make([]Val, len(from)), + } + + copy(s.data, from) + + return s +} + +func (s *Slice[Val]) Get(fn func(data []Val)) { + s.lock.RLock() + defer s.lock.RUnlock() + + fn(s.data) +} + +func (s *Slice[Val]) GetErr(fn func(data []Val) error) error { + s.lock.RLock() + defer s.lock.RUnlock() + + return fn(s.data) +} + +func (s *Slice[Val]) Set(data []Val) { + s.lock.Lock() + defer s.lock.Unlock() + + s.data = data +} + +func GetSlice[Val, Ret any](s *Slice[Val], fn func(data []Val) Ret) Ret { + s.lock.RLock() + defer s.lock.RUnlock() + + return fn(s.data) +} + +func GetSliceErr[Val, Ret any](s *Slice[Val], fn func(data []Val) (Ret, error)) (Ret, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + return fn(s.data) +} diff --git a/internal/safe/type.go b/internal/safe/type.go new file mode 100644 index 00000000..2bcdbb02 --- /dev/null +++ b/internal/safe/type.go @@ -0,0 +1,49 @@ +package safe + +import "sync" + +type Type[T any] struct { + data T + lock sync.RWMutex +} + +func NewType[T any](data T) *Type[T] { + return &Type[T]{ + data: data, + } +} + +func (s *Type[T]) Get(fn func(data T)) { + s.lock.RLock() + defer s.lock.RUnlock() + + fn(s.data) +} + +func (s *Type[T]) GetErr(fn func(data T) error) error { + s.lock.RLock() + defer s.lock.RUnlock() + + return fn(s.data) +} + +func (s *Type[T]) Set(data T) { + s.lock.Lock() + defer s.lock.Unlock() + + s.data = data +} + +func GetType[T, Ret any](s *Type[T], fn func(data T) Ret) Ret { + s.lock.RLock() + defer s.lock.RUnlock() + + return fn(s.data) +} + +func GetTypeErr[T, Ret any](s *Type[T], fn func(data T) (Ret, error)) (Ret, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + return fn(s.data) +} diff --git a/internal/user/addresses.go b/internal/user/addresses.go deleted file mode 100644 index b78f1b3e..00000000 --- a/internal/user/addresses.go +++ /dev/null @@ -1,50 +0,0 @@ -package user - -import "gitlab.protontech.ch/go/liteapi" - -type addrList struct { - apiAddrs ordMap[string, string, liteapi.Address] -} - -func newAddrList(apiAddrs []liteapi.Address) *addrList { - return &addrList{ - apiAddrs: newOrdMap( - func(addr liteapi.Address) string { return addr.ID }, - func(addr liteapi.Address) string { return addr.Email }, - func(a, b liteapi.Address) bool { return a.Order < b.Order }, - apiAddrs..., - ), - } -} - -func (list *addrList) insert(address liteapi.Address) { - list.apiAddrs.insert(address) -} - -func (list *addrList) delete(addrID string) string { - return list.apiAddrs.delete(addrID) -} - -func (list *addrList) primary() string { - return list.apiAddrs.keys()[0] -} - -func (list *addrList) addrIDs() []string { - return list.apiAddrs.keys() -} - -func (list *addrList) addrID(email string) (string, bool) { - return list.apiAddrs.getKey(email) -} - -func (list *addrList) emails() []string { - return list.apiAddrs.values() -} - -func (list *addrList) email(addrID string) (string, bool) { - return list.apiAddrs.getVal(addrID) -} - -func (list *addrList) addrMap() map[string]string { - return list.apiAddrs.toMap() -} diff --git a/internal/user/builder.go b/internal/user/builder.go deleted file mode 100644 index 003cb31d..00000000 --- a/internal/user/builder.go +++ /dev/null @@ -1,95 +0,0 @@ -package user - -import ( - "context" - "time" - - "github.com/ProtonMail/gluon/imap" - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/pool" - "github.com/ProtonMail/proton-bridge/v2/pkg/message" - "github.com/bradenaw/juniper/xslices" - "gitlab.protontech.ch/go/liteapi" - "golang.org/x/exp/slices" -) - -type request struct { - messageID string - addressID string - addrKR *crypto.KeyRing -} - -type fetcher interface { - GetMessage(context.Context, string) (liteapi.Message, error) - GetAttachment(context.Context, string) ([]byte, error) -} - -func newBuilder(f fetcher, msgWorkers, attWorkers int) *pool.Pool[request, *imap.MessageCreated] { - attPool := pool.New(attWorkers, func(ctx context.Context, attID string) ([]byte, error) { - return f.GetAttachment(ctx, attID) - }) - - msgPool := pool.New(msgWorkers, func(ctx context.Context, req request) (*imap.MessageCreated, error) { - msg, err := f.GetMessage(ctx, req.messageID) - if err != nil { - return nil, err - } - - var attIDs []string - - for _, att := range msg.Attachments { - attIDs = append(attIDs, att.ID) - } - - attData, err := attPool.ProcessAll(ctx, attIDs) - if err != nil { - return nil, err - } - - literal, err := message.BuildRFC822(req.addrKR, msg, attData, message.JobOptions{ - IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead. - SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate. - AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id. - AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id. - AddMessageDate: true, // Whether to include message time as X-Pm-Date. - AddMessageIDReference: true, // Whether to include the MessageID in References. - }) - if err != nil { - return nil, err - } - - return newMessageCreatedUpdate(msg, literal) - }) - - return msgPool -} - -func newMessageCreatedUpdate(message liteapi.Message, literal []byte) (*imap.MessageCreated, error) { - parsedMessage, err := imap.NewParsedMessage(literal) - if err != nil { - return nil, err - } - - flags := imap.NewFlagSet() - - if !message.Unread { - flags = flags.Add(imap.FlagSeen) - } - - if slices.Contains(message.LabelIDs, liteapi.StarredLabel) { - flags = flags.Add(imap.FlagFlagged) - } - - imapMessage := imap.Message{ - ID: imap.MessageID(message.ID), - Flags: flags, - Date: time.Unix(message.Time, 0), - } - - return &imap.MessageCreated{ - Message: imapMessage, - Literal: literal, - LabelIDs: mapTo[string, imap.LabelID](xslices.Filter(message.LabelIDs, wantLabelID)), - ParsedMessage: parsedMessage, - }, nil -} diff --git a/internal/user/events.go b/internal/user/events.go index 85b4553c..17695966 100644 --- a/internal/user/events.go +++ b/internal/user/events.go @@ -7,6 +7,7 @@ import ( "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/gluon/queue" "github.com/ProtonMail/proton-bridge/v2/internal/events" + "github.com/ProtonMail/proton-bridge/v2/internal/safe" "github.com/ProtonMail/proton-bridge/v2/internal/vault" "github.com/bradenaw/juniper/xslices" "gitlab.protontech.ch/go/liteapi" @@ -54,7 +55,7 @@ func (user *User) handleUserEvent(ctx context.Context, userEvent liteapi.User) e return err } - user.apiUser = userEvent + user.apiUser.Set(userEvent) user.userKR = userKR @@ -96,24 +97,29 @@ func (user *User) handleCreateAddressEvent(ctx context.Context, event liteapi.Ad return fmt.Errorf("failed to unlock address keys: %w", err) } - user.apiAddrs.insert(event.Address) + apiAddrs, err := user.client.GetAddresses(ctx) + if err != nil { + return fmt.Errorf("failed to get addresses: %w", err) + } + + user.apiAddrs.Set(apiAddrs) user.addrKRs[event.Address.ID] = addrKR - if user.vault.AddressMode() == vault.SplitMode { - user.updateCh[event.Address.ID] = queue.NewQueuedChannel[imap.Update](0, 0) - - if err := user.syncLabels(ctx, event.Address.ID); err != nil { - return fmt.Errorf("failed to sync labels to new address: %w", err) - } - } - user.eventCh.Enqueue(events.UserAddressCreated{ UserID: user.ID(), AddressID: event.Address.ID, Email: event.Address.Email, }) + if user.vault.AddressMode() == vault.SplitMode { + user.updateCh[event.Address.ID] = queue.NewQueuedChannel[imap.Update](0, 0) + + if err := syncLabels(ctx, user.client, user.updateCh[event.Address.ID]); err != nil { + return fmt.Errorf("failed to sync labels to new address: %w", err) + } + } + return nil } @@ -123,7 +129,12 @@ func (user *User) handleUpdateAddressEvent(ctx context.Context, event liteapi.Ad return fmt.Errorf("failed to unlock address keys: %w", err) } - user.apiAddrs.insert(event.Address) + apiAddrs, err := user.client.GetAddresses(ctx) + if err != nil { + return fmt.Errorf("failed to get addresses: %w", err) + } + + user.apiAddrs.Set(apiAddrs) user.addrKRs[event.Address.ID] = addrKR @@ -137,9 +148,23 @@ func (user *User) handleUpdateAddressEvent(ctx context.Context, event liteapi.Ad } func (user *User) handleDeleteAddressEvent(ctx context.Context, event liteapi.AddressEvent) error { - email := user.apiAddrs.delete(event.ID) + email, err := safe.GetSliceErr(user.apiAddrs, func(apiAddrs []liteapi.Address) (string, error) { + return getAddrEmail(apiAddrs, event.ID) + }) + if err != nil { + return fmt.Errorf("failed to get address email: %w", err) + } - if user.vault.AddressMode() == vault.SplitMode { + apiAddrs, err := user.client.GetAddresses(ctx) + if err != nil { + return fmt.Errorf("failed to get addresses: %w", err) + } + + user.apiAddrs.Set(apiAddrs) + + delete(user.addrKRs, event.ID) + + if len(user.updateCh) > 1 { user.updateCh[event.ID].Close() delete(user.updateCh, event.ID) } @@ -155,7 +180,7 @@ func (user *User) handleDeleteAddressEvent(ctx context.Context, event liteapi.Ad // handleMailSettingsEvent handles the given mail settings event. func (user *User) handleMailSettingsEvent(ctx context.Context, mailSettingsEvent liteapi.MailSettings) error { - user.settings = mailSettingsEvent + user.settings.Set(mailSettingsEvent) return nil } @@ -234,24 +259,18 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []litea } func (user *User) handleCreateMessageEvent(ctx context.Context, event liteapi.MessageEvent) error { - var addressID string - - if user.GetAddressMode() == vault.CombinedMode { - addressID = user.apiAddrs.primary() - } else { - addressID = event.Message.AddressID - } - - message, err := user.builder.ProcessOne(ctx, request{ - messageID: event.ID, - addressID: addressID, - addrKR: user.addrKRs[event.Message.AddressID], - }) + buildRes, err := user.buildRFC822(ctx, event.Message) if err != nil { - return err + return fmt.Errorf("failed to build RFC822: %w", err) } - user.updateCh[addressID].Enqueue(imap.NewMessagesCreated(message)) + if len(user.updateCh) > 1 { + user.updateCh[buildRes.addressID].Enqueue(imap.NewMessagesCreated(buildRes.update)) + } else { + user.apiAddrs.Get(func(apiAddrs []liteapi.Address) { + user.updateCh[apiAddrs[0].ID].Enqueue(imap.NewMessagesCreated(buildRes.update)) + }) + } return nil } @@ -264,10 +283,12 @@ func (user *User) handleUpdateMessageEvent(ctx context.Context, event liteapi.Me event.Message.Starred(), ) - if user.GetAddressMode() == vault.CombinedMode { - user.updateCh[user.apiAddrs.primary()].Enqueue(update) - } else { + if len(user.updateCh) > 1 { user.updateCh[event.Message.AddressID].Enqueue(update) + } else { + user.apiAddrs.Get(func(apiAddrs []liteapi.Address) { + user.updateCh[apiAddrs[0].ID].Enqueue(update) + }) } return nil diff --git a/internal/user/flusher.go b/internal/user/flusher.go deleted file mode 100644 index 2bab1428..00000000 --- a/internal/user/flusher.go +++ /dev/null @@ -1,76 +0,0 @@ -package user - -import ( - "sync" - "time" - - "github.com/ProtonMail/gluon/imap" - "github.com/ProtonMail/gluon/queue" - "github.com/ProtonMail/proton-bridge/v2/internal/events" -) - -type flusher struct { - userID string - updateCh *queue.QueuedChannel[imap.Update] - eventCh *queue.QueuedChannel[events.Event] - - updates []*imap.MessageCreated - maxChunkSize int - curChunkSize int - - count int - total int - start time.Time - - pushLock sync.Mutex -} - -func newFlusher( - userID string, - updateCh *queue.QueuedChannel[imap.Update], - eventCh *queue.QueuedChannel[events.Event], - total, maxChunkSize int, -) *flusher { - return &flusher{ - userID: userID, - updateCh: updateCh, - eventCh: eventCh, - - maxChunkSize: maxChunkSize, - - total: total, - start: time.Now(), - } -} - -func (f *flusher) push(update *imap.MessageCreated) { - f.pushLock.Lock() - defer f.pushLock.Unlock() - - f.updates = append(f.updates, update) - - if f.curChunkSize += len(update.Literal); f.curChunkSize >= f.maxChunkSize { - f.flush() - } -} - -func (f *flusher) flush() { - if len(f.updates) == 0 { - return - } - - f.count += len(f.updates) - f.updateCh.Enqueue(imap.NewMessagesCreated(f.updates...)) - f.eventCh.Enqueue(newSyncProgress(f.userID, f.count, f.total, f.start)) - f.updates = nil - f.curChunkSize = 0 -} - -func newSyncProgress(userID string, count, total int, start time.Time) events.SyncProgress { - return events.SyncProgress{ - UserID: userID, - Progress: float64(count) / float64(total), - Elapsed: time.Since(start), - Remaining: time.Since(start) * time.Duration(total-count) / time.Duration(count), - } -} diff --git a/internal/user/map.go b/internal/user/map.go deleted file mode 100644 index 33b448b3..00000000 --- a/internal/user/map.go +++ /dev/null @@ -1,104 +0,0 @@ -package user - -import ( - "github.com/bradenaw/juniper/xslices" - "golang.org/x/exp/slices" -) - -type ordMap[Key, Val comparable, Data any] struct { - data map[Key]Data - order []Key - - toKey func(Data) Key - toVal func(Data) Val - isLess func(Data, Data) bool -} - -func newOrdMap[Key, Val comparable, Data any]( - key func(Data) Key, - value func(Data) Val, - less func(Data, Data) bool, - data ...Data, -) ordMap[Key, Val, Data] { - m := ordMap[Key, Val, Data]{ - data: make(map[Key]Data), - - toKey: key, - toVal: value, - isLess: less, - } - - for _, d := range data { - m.insert(d) - } - - return m -} - -func (set *ordMap[Key, Val, Data]) insert(data Data) { - if _, ok := set.data[set.toKey(data)]; ok { - set.delete(set.toKey(data)) - } - - set.data[set.toKey(data)] = data - - set.order = append(set.order, set.toKey(data)) - - slices.SortFunc(set.order, func(a, b Key) bool { - return set.isLess(set.data[a], set.data[b]) - }) -} - -func (set *ordMap[Key, Val, Data]) delete(key Key) Val { - data, ok := set.data[key] - if !ok { - return *new(Val) - } - - delete(set.data, key) - - set.order = xslices.Filter(set.order, func(otherKey Key) bool { - return otherKey != key - }) - - return set.toVal(data) -} - -func (set *ordMap[Key, Val, Data]) getVal(key Key) (Val, bool) { - data, ok := set.data[key] - if !ok { - return *new(Val), false - } - - return set.toVal(data), true -} - -func (set *ordMap[Key, Val, Data]) getKey(wantVal Val) (Key, bool) { - for key, data := range set.data { - if set.toVal(data) == wantVal { - return key, true - } - } - - return *new(Key), false -} - -func (set *ordMap[Key, Val, Data]) keys() []Key { - return set.order -} - -func (set *ordMap[Key, Val, Data]) values() []Val { - return xslices.Map(set.order, func(key Key) Val { - return set.toVal(set.data[key]) - }) -} - -func (set *ordMap[Key, Val, Data]) toMap() map[Key]Val { - m := make(map[Key]Val) - - for _, key := range set.order { - m[key] = set.toVal(set.data[key]) - } - - return m -} diff --git a/internal/user/map_test.go b/internal/user/map_test.go deleted file mode 100644 index 75e6d99e..00000000 --- a/internal/user/map_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package user - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestMap(t *testing.T) { - type Key int - - type Value string - - type Data struct { - key Key - value Value - } - - m := newOrdMap( - func(d Data) Key { return d.key }, - func(d Data) Value { return d.value }, - func(a, b Data) bool { return a.key < b.key }, - Data{key: 1, value: "a"}, - Data{key: 2, value: "b"}, - Data{key: 3, value: "c"}, - ) - - // Insert some new data. - m.insert(Data{key: 4, value: "d"}) - m.insert(Data{key: 5, value: "e"}) - - // Delete some data. - require.Equal(t, Value("c"), m.delete(3)) - require.Equal(t, Value("a"), m.delete(1)) - require.Equal(t, Value("e"), m.delete(5)) - - // Check the remaining keys and values are correct. - require.Equal(t, []Key{2, 4}, m.keys()) - require.Equal(t, []Value{"b", "d"}, m.values()) - - // Overwrite some data. - m.insert(Data{key: 2, value: "two"}) - m.insert(Data{key: 4, value: "four"}) - - // Check the remaining keys and values are correct. - require.Equal(t, []Key{2, 4}, m.keys()) - require.Equal(t, []Value{"two", "four"}, m.values()) -} diff --git a/internal/user/smtp.go b/internal/user/smtp.go index 46a5d1e2..e8f5cbf7 100644 --- a/internal/user/smtp.go +++ b/internal/user/smtp.go @@ -6,14 +6,14 @@ import ( "fmt" "io" "net/mail" + "net/url" "runtime" "strings" - "github.com/ProtonMail/gluon/queue" "github.com/ProtonMail/gluon/rfc822" "github.com/ProtonMail/go-rfc5322" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/ProtonMail/proton-bridge/v2/internal/events" + "github.com/ProtonMail/proton-bridge/v2/internal/safe" "github.com/ProtonMail/proton-bridge/v2/internal/vault" "github.com/ProtonMail/proton-bridge/v2/pkg/message" "github.com/ProtonMail/proton-bridge/v2/pkg/message/parser" @@ -22,37 +22,14 @@ import ( "github.com/emersion/go-smtp" "github.com/sirupsen/logrus" "gitlab.protontech.ch/go/liteapi" - "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) type smtpSession struct { - // client is the user's API client. - client *liteapi.Client + *User - // eventCh allows the session to publish events. - eventCh *queue.QueuedChannel[events.Event] - - // userID is the user's ID. - userID string - - // addrID holds the ID of the address that is currently being used. - addrID string - - // addrMode holds the address mode that is currently being used. - addrMode vault.AddressMode - - // emails holds all email addresses associated with the user, by address ID. - emails map[string]string - - // settings holds the mail settings for the user. - settings liteapi.MailSettings - - // userKR holds the user's keyring. - userKR *crypto.KeyRing - - // addrKRs holds the keyrings for each address. - addrKRs map[string]*crypto.KeyRing + // authID holds the ID of the address that the SMTP client authenticated with to send the message. + authID string // fromAddrID is the ID of the current sending address (taken from the return path). fromAddrID string @@ -61,30 +38,18 @@ type smtpSession struct { to []string } -func newSMTPSession( - client *liteapi.Client, - eventCh *queue.QueuedChannel[events.Event], - userID, addrID string, - addrMode vault.AddressMode, - emails map[string]string, - settings liteapi.MailSettings, - userKR *crypto.KeyRing, - addrKRs map[string]*crypto.KeyRing, -) *smtpSession { - return &smtpSession{ - client: client, - eventCh: eventCh, - - userID: userID, - addrID: addrID, - addrMode: addrMode, - - emails: emails, - settings: settings, - - userKR: userKR, - addrKRs: addrKRs, +func newSMTPSession(user *User, email string) (*smtpSession, error) { + authID, err := safe.GetSliceErr(user.apiAddrs, func(apiAddrs []liteapi.Address) (string, error) { + return getAddrID(apiAddrs, email) + }) + if err != nil { + return nil, fmt.Errorf("failed to get address ID: %w", err) } + + return &smtpSession{ + User: user, + authID: authID, + }, nil } // Discard currently processed message. @@ -109,30 +74,35 @@ func (session *smtpSession) Logout() error { func (session *smtpSession) Mail(from string, opts smtp.MailOptions) error { logrus.Info("SMTP session mail") - switch { - case opts.RequireTLS: - return ErrNotImplemented + return session.apiAddrs.GetErr(func(apiAddrs []liteapi.Address) error { - case opts.UTF8: - return ErrNotImplemented - - case opts.Auth != nil: - if *opts.Auth != "" && *opts.Auth != session.emails[session.addrID] { + switch { + case opts.RequireTLS: return ErrNotImplemented + + case opts.UTF8: + return ErrNotImplemented + + case opts.Auth != nil: + email, err := getAddrEmail(apiAddrs, session.authID) + if err != nil { + return fmt.Errorf("invalid auth address: %w", err) + } + + if *opts.Auth != "" && *opts.Auth != email { + return ErrNotImplemented + } } - } - for addrID, email := range session.emails { - if strings.EqualFold(from, email) { - session.fromAddrID = addrID + fromAddrID, err := getAddrID(apiAddrs, from) + if err != nil { + return fmt.Errorf("invalid return path: %w", err) } - } - if session.fromAddrID == "" { - return ErrInvalidReturnPath - } + session.fromAddrID = fromAddrID - return nil + return nil + }) } // Add recipient for currently processed message. @@ -168,13 +138,15 @@ func (session *smtpSession) Data(r io.Reader) error { } // If the message contains a sender, use it instead of the one from the return path. - if sender, ok := getMessageSender(parser); ok { - for addrID, email := range session.emails { - if strings.EqualFold(email, sanitizeEmail(sender)) { - session.fromAddrID = addrID + session.apiAddrs.Get(func(apiAddrs []liteapi.Address) { + if sender, ok := getMessageSender(parser); ok { + for _, addr := range apiAddrs { + if strings.EqualFold(addr.Email, sanitizeEmail(sender)) { + session.fromAddrID = addr.ID + } } } - } + }) addrKR, ok := session.addrKRs[session.fromAddrID] if !ok { @@ -186,28 +158,38 @@ func (session *smtpSession) Data(r io.Reader) error { return fmt.Errorf("failed to get first key: %w", err) } - message, err := sendWithKey( - session.client, - session.addrID, - session.addrMode, - session.userKR, - firstAddrKR, - session.settings, - sanitizeEmail(session.emails[session.fromAddrID]), - session.to, - maps.Values(session.emails), - parser, - ) + from, err := safe.GetSliceErr(session.apiAddrs, func(apiAddrs []liteapi.Address) (string, error) { + email, err := getAddrEmail(apiAddrs, session.fromAddrID) + if err != nil { + return "", fmt.Errorf("failed to get address email: %w", err) + } + + return sanitizeEmail(email), nil + }) + if err != nil { + return fmt.Errorf("failed to get address email: %w", err) + } + + message, err := safe.GetSliceErr(session.apiAddrs, func(apiAddrs []liteapi.Address) (liteapi.Message, error) { + return safe.GetTypeErr(session.settings, func(settings liteapi.MailSettings) (liteapi.Message, error) { + return sendWithKey( + session.client, + session.authID, + session.vault.AddressMode(), + apiAddrs, + settings, + session.userKR, + firstAddrKR, + parser, + from, + session.to, + ) + }) + }) if err != nil { return fmt.Errorf("failed to send message: %w", err) } - session.eventCh.Enqueue(events.MessageSent{ - UserID: session.userID, - AddressID: session.addrID, - MessageID: message.ID, - }) - logrus.WithField("messageID", message.ID).Info("Message sent") return nil @@ -216,13 +198,14 @@ func (session *smtpSession) Data(r io.Reader) error { // sendWithKey sends the message with the given address key. func sendWithKey( client *liteapi.Client, - addrID string, + authAddrID string, addrMode vault.AddressMode, - userKR, addrKR *crypto.KeyRing, + apiAddrs []liteapi.Address, settings liteapi.MailSettings, - from string, - to, emails []string, + userKR, addrKR *crypto.KeyRing, parser *parser.Parser, + from string, + to []string, ) (liteapi.Message, error) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -246,11 +229,11 @@ func sendWithKey( return liteapi.Message{}, fmt.Errorf("failed to parse message: %w", err) } - if err := sanitizeParsedMessage(&message, from, to, emails); err != nil { + if err := sanitizeParsedMessage(&message, apiAddrs, from, to); err != nil { return liteapi.Message{}, fmt.Errorf("failed to sanitize message: %w", err) } - parentID, err := getParentID(ctx, client, addrID, addrMode, message.References) + parentID, err := getParentID(ctx, client, authAddrID, addrMode, message.References) if err != nil { return liteapi.Message{}, fmt.Errorf("failed to get parent ID: %w", err) } @@ -278,7 +261,7 @@ func sendWithKey( return res, nil } -func sanitizeParsedMessage(message *message.Message, from string, to, emails []string) error { +func sanitizeParsedMessage(message *message.Message, apiAddrs []liteapi.Address, from string, to []string) error { // Check sender: set the sender in the parsed message if it's missing. if message.Sender == nil { message.Sender = &mail.Address{Address: from} @@ -287,12 +270,12 @@ func sanitizeParsedMessage(message *message.Message, from string, to, emails []s } // Check that the sending address is owned by the user, and if so, properly capitalize it. - if idx := xslices.IndexFunc(emails, func(email string) bool { - return strings.EqualFold(email, sanitizeEmail(message.Sender.Address)) + if idx := xslices.IndexFunc(apiAddrs, func(addr liteapi.Address) bool { + return strings.EqualFold(addr.Email, sanitizeEmail(message.Sender.Address)) }); idx < 0 { return fmt.Errorf("address %q is not owned by user", message.Sender.Address) } else { - message.Sender.Address = constructEmail(message.Sender.Address, emails[idx]) + message.Sender.Address = constructEmail(message.Sender.Address, apiAddrs[idx].Email) } // Check ToList: ensure that ToList only contains addresses we actually plan to send to. @@ -313,7 +296,7 @@ func sanitizeParsedMessage(message *message.Message, from string, to, emails []s func getParentID( ctx context.Context, client *liteapi.Client, - addrID string, + authAddrID string, addrMode vault.AddressMode, references []string, ) (string, error) { @@ -334,12 +317,12 @@ func getParentID( // Try to find a parent ID in the internal references. for _, internal := range internal { - filter := map[string][]string{ + filter := url.Values{ "ID": {internal}, } if addrMode == vault.SplitMode { - filter["AddressID"] = []string{addrID} + filter["AddressID"] = []string{authAddrID} } metadata, err := client.GetAllMessageMetadata(ctx, filter) @@ -359,12 +342,12 @@ func getParentID( // If no parent was found, try to find it in the last external reference. // There can be multiple messages with the same external ID; in this case, we don't pick any parent. if parentID == "" && len(external) > 0 { - filter := map[string][]string{ + filter := url.Values{ "ExternalID": {external[len(external)-1]}, } if addrMode == vault.SplitMode { - filter["AddressID"] = []string{addrID} + filter["AddressID"] = []string{authAddrID} } metadata, err := client.GetAllMessageMetadata(ctx, filter) diff --git a/internal/user/sync.go b/internal/user/sync.go index 0a6808ce..754e25db 100644 --- a/internal/user/sync.go +++ b/internal/user/sync.go @@ -2,131 +2,174 @@ package user import ( "context" + "errors" "fmt" + "runtime" "strings" + "time" "github.com/ProtonMail/gluon/imap" - "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/ProtonMail/gluon/queue" + "github.com/bradenaw/juniper/iterator" + "github.com/bradenaw/juniper/parallel" + "github.com/bradenaw/juniper/stream" "github.com/bradenaw/juniper/xslices" "github.com/google/uuid" "gitlab.protontech.ch/go/liteapi" + "golang.org/x/exp/maps" ) -const chunkSize = 1 << 20 +const ( + maxUpdateSize = 1 << 25 + maxBatchSize = 1 << 8 +) -func (user *User) syncLabels(ctx context.Context, addrIDs ...string) error { +func (user *User) sync(ctx context.Context) error { + if !user.vault.SyncStatus().HasLabels { + if err := syncLabels(ctx, user.client, maps.Values(user.updateCh)...); err != nil { + return fmt.Errorf("failed to sync labels: %w", err) + } + + if err := user.vault.SetHasLabels(true); err != nil { + return fmt.Errorf("failed to set has labels: %w", err) + } + } + + if !user.vault.SyncStatus().HasMessages { + if err := user.syncMessages(ctx); err != nil { + return fmt.Errorf("failed to sync messages: %w", err) + } + + if err := user.vault.SetHasMessages(true); err != nil { + return fmt.Errorf("failed to set has messages: %w", err) + } + } + + return nil +} + +func syncLabels(ctx context.Context, client *liteapi.Client, updateCh ...*queue.QueuedChannel[imap.Update]) error { // Sync the system folders. - system, err := user.client.GetLabels(ctx, liteapi.LabelTypeSystem) + system, err := client.GetLabels(ctx, liteapi.LabelTypeSystem) if err != nil { - return err + return fmt.Errorf("failed to get system labels: %w", err) } for _, label := range xslices.Filter(system, func(label liteapi.Label) bool { return wantLabelID(label.ID) }) { - for _, addrID := range addrIDs { - user.updateCh[addrID].Enqueue(newSystemMailboxCreatedUpdate(imap.LabelID(label.ID), label.Name)) + for _, updateCh := range updateCh { + updateCh.Enqueue(newSystemMailboxCreatedUpdate(imap.LabelID(label.ID), label.Name)) } } // Create Folders/Labels mailboxes with a random ID and with the \Noselect attribute. for _, prefix := range []string{folderPrefix, labelPrefix} { - for _, addrID := range addrIDs { - user.updateCh[addrID].Enqueue(newPlaceHolderMailboxCreatedUpdate(prefix)) + for _, updateCh := range updateCh { + updateCh.Enqueue(newPlaceHolderMailboxCreatedUpdate(prefix)) } } // Sync the API folders. - folders, err := user.client.GetLabels(ctx, liteapi.LabelTypeFolder) + folders, err := client.GetLabels(ctx, liteapi.LabelTypeFolder) if err != nil { - return err + return fmt.Errorf("failed to get folders: %w", err) } for _, folder := range folders { - for _, addrID := range addrIDs { - user.updateCh[addrID].Enqueue(newMailboxCreatedUpdate(imap.LabelID(folder.ID), []string{folderPrefix, folder.Path})) + for _, updateCh := range updateCh { + updateCh.Enqueue(newMailboxCreatedUpdate(imap.LabelID(folder.ID), []string{folderPrefix, folder.Path})) } } // Sync the API labels. - labels, err := user.client.GetLabels(ctx, liteapi.LabelTypeLabel) + labels, err := client.GetLabels(ctx, liteapi.LabelTypeLabel) if err != nil { - return err + return fmt.Errorf("failed to get labels: %w", err) } for _, label := range labels { - for _, addrID := range addrIDs { - user.updateCh[addrID].Enqueue(newMailboxCreatedUpdate(imap.LabelID(label.ID), []string{labelPrefix, label.Path})) + for _, updateCh := range updateCh { + updateCh.Enqueue(newMailboxCreatedUpdate(imap.LabelID(label.ID), []string{labelPrefix, label.Path})) } } + // Wait for all label updates to be applied. + for _, updateCh := range updateCh { + update := imap.NewNoop() + defer update.WaitContext(ctx) + + updateCh.Enqueue(update) + } + return nil } func (user *User) syncMessages(ctx context.Context) error { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - // Determine which messages to sync. - // TODO: This needs to be done better using the new API route to retrieve just the message IDs. metadata, err := user.client.GetAllMessageMetadata(ctx, nil) if err != nil { - return err + return fmt.Errorf("get all message metadata: %w", err) } - // If in split mode, we need to send each message to a different IMAP connector. - isSplitMode := user.vault.AddressMode() == vault.SplitMode - - // Collect the build requests -- we need: - // - the message ID to build, - // - the keyring to decrypt the message, - // - and the address to send the message to (for split mode). - requests := xslices.Map(metadata, func(metadata liteapi.MessageMetadata) request { - var addressID string - - if isSplitMode { - addressID = metadata.AddressID - } else { - addressID = user.apiAddrs.primary() + // If possible, begin syncing from the last synced message. + if beginID := user.vault.SyncStatus().LastMessageID; beginID != "" { + if idx := xslices.IndexFunc(metadata, func(metadata liteapi.MessageMetadata) bool { + return metadata.ID == beginID + }); idx >= 0 { + metadata = metadata[idx:] } + } - return request{ - messageID: metadata.ID, - addressID: addressID, - addrKR: user.addrKRs[metadata.AddressID], - } - }) + // Process the metadata, building the messages. + buildCh := stream.Chunk(parallel.MapStream( + ctx, + stream.FromIterator(iterator.Slice(metadata)), + runtime.NumCPU()*runtime.NumCPU()/2, + runtime.NumCPU()*runtime.NumCPU()/2, + user.buildRFC822, + ), maxBatchSize) // Create the flushers, one per update channel. flushers := make(map[string]*flusher) for addrID, updateCh := range user.updateCh { - flusher := newFlusher(user.ID(), updateCh, user.eventCh, len(requests), chunkSize) - defer flusher.flush() + flusher := newFlusher(user.ID(), updateCh, maxUpdateSize) + defer flusher.flush(ctx, true) flushers[addrID] = flusher } - // Build the messages and send them to the correct flusher. - if err := user.builder.Process(ctx, requests, func(req request, res *imap.MessageCreated, err error) error { - if err != nil { - return fmt.Errorf("failed to build message %s: %w", req.messageID, err) + // Create a reporter to report sync progress updates. + reporter := newReporter(user.ID(), user.eventCh, len(metadata), time.Second) + defer reporter.done() + + // Send each update to the appropriate flusher. + for { + batch, err := buildCh.Next(ctx) + if errors.Is(err, stream.End) { + return nil + } else if err != nil { + return fmt.Errorf("failed to get next sync batch: %w", err) } - flushers[req.addressID].push(res) + user.apiAddrs.Get(func(apiAddrs []liteapi.Address) { + for _, res := range batch { + if len(flushers) > 1 { + flushers[res.addressID].push(ctx, res.update) + } else { + flushers[apiAddrs[0].ID].push(ctx, res.update) + } + } + }) - return nil - }); err != nil { - return fmt.Errorf("failed to build messages: %w", err) - } + for _, flusher := range flushers { + flusher.flush(ctx, true) + } - return nil -} + if err := user.vault.SetLastMessageID(batch[len(batch)-1].messageID); err != nil { + return fmt.Errorf("failed to set last synced message ID: %w", err) + } -func (user *User) syncWait() { - for _, updateCh := range user.updateCh { - waiter := imap.NewNoop() - defer waiter.Wait() - - updateCh.Enqueue(waiter) + reporter.add(len(batch)) } } diff --git a/internal/user/sync_build.go b/internal/user/sync_build.go new file mode 100644 index 00000000..d917944a --- /dev/null +++ b/internal/user/sync_build.go @@ -0,0 +1,88 @@ +package user + +import ( + "context" + "fmt" + "time" + + "github.com/ProtonMail/gluon/imap" + "github.com/ProtonMail/proton-bridge/v2/pkg/message" + "github.com/bradenaw/juniper/xslices" + "gitlab.protontech.ch/go/liteapi" + "golang.org/x/exp/slices" +) + +type buildRes struct { + messageID string + addressID string + update *imap.MessageCreated +} + +func defaultJobOpts() message.JobOptions { + return message.JobOptions{ + IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead. + SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate. + AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id. + AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id. + AddMessageDate: true, // Whether to include message time as X-Pm-Date. + AddMessageIDReference: true, // Whether to include the MessageID in References. + } +} + +func (user *User) buildRFC822(ctx context.Context, metadata liteapi.MessageMetadata) (*buildRes, error) { + msg, err := user.client.GetMessage(ctx, metadata.ID) + if err != nil { + return nil, fmt.Errorf("failed to get message %s: %w", metadata.ID, err) + } + + attData, err := user.attPool.ProcessAll(ctx, xslices.Map(msg.Attachments, func(att liteapi.Attachment) string { return att.ID })) + if err != nil { + return nil, fmt.Errorf("failed to get attachments for message %s: %w", metadata.ID, err) + } + + literal, err := message.BuildRFC822(user.addrKRs[msg.AddressID], msg, attData, defaultJobOpts()) + if err != nil { + return nil, fmt.Errorf("failed to build message %s: %w", metadata.ID, err) + } + + update, err := newMessageCreatedUpdate(metadata, literal) + if err != nil { + return nil, fmt.Errorf("failed to create IMAP update for message %s: %w", metadata.ID, err) + } + + return &buildRes{ + messageID: metadata.ID, + addressID: metadata.AddressID, + update: update, + }, nil +} + +func newMessageCreatedUpdate(message liteapi.MessageMetadata, literal []byte) (*imap.MessageCreated, error) { + parsedMessage, err := imap.NewParsedMessage(literal) + if err != nil { + return nil, err + } + + flags := imap.NewFlagSet() + + if !message.Unread { + flags = flags.Add(imap.FlagSeen) + } + + if slices.Contains(message.LabelIDs, liteapi.StarredLabel) { + flags = flags.Add(imap.FlagFlagged) + } + + imapMessage := imap.Message{ + ID: imap.MessageID(message.ID), + Flags: flags, + Date: time.Unix(message.Time, 0), + } + + return &imap.MessageCreated{ + Message: imapMessage, + Literal: literal, + LabelIDs: mapTo[string, imap.LabelID](xslices.Filter(message.LabelIDs, wantLabelID)), + ParsedMessage: parsedMessage, + }, nil +} diff --git a/internal/user/sync_flusher.go b/internal/user/sync_flusher.go new file mode 100644 index 00000000..0050fb58 --- /dev/null +++ b/internal/user/sync_flusher.go @@ -0,0 +1,56 @@ +package user + +import ( + "context" + "sync" + + "github.com/ProtonMail/gluon/imap" + "github.com/ProtonMail/gluon/queue" +) + +type flusher struct { + userID string + updateCh *queue.QueuedChannel[imap.Update] + + updates []*imap.MessageCreated + maxChunkSize int + curChunkSize int + + pushLock sync.Mutex +} + +func newFlusher(userID string, updateCh *queue.QueuedChannel[imap.Update], maxChunkSize int) *flusher { + return &flusher{ + userID: userID, + updateCh: updateCh, + maxChunkSize: maxChunkSize, + } +} + +func (f *flusher) push(ctx context.Context, update *imap.MessageCreated) { + f.pushLock.Lock() + defer f.pushLock.Unlock() + + f.updates = append(f.updates, update) + + if f.curChunkSize += len(update.Literal); f.curChunkSize >= f.maxChunkSize { + f.flush(ctx, false) + } +} + +func (f *flusher) flush(ctx context.Context, wait bool) { + if len(f.updates) == 0 { + return + } + + f.updateCh.Enqueue(imap.NewMessagesCreated(f.updates...)) + f.updates = nil + f.curChunkSize = 0 + + if wait { + update := imap.NewNoop() + defer update.WaitContext(ctx) + + f.updateCh.Enqueue(update) + } +} diff --git a/internal/user/sync_reporter.go b/internal/user/sync_reporter.go new file mode 100644 index 00000000..35859919 --- /dev/null +++ b/internal/user/sync_reporter.go @@ -0,0 +1,55 @@ +package user + +import ( + "time" + + "github.com/ProtonMail/gluon/queue" + "github.com/ProtonMail/proton-bridge/v2/internal/events" +) + +type reporter struct { + userID string + eventCh *queue.QueuedChannel[events.Event] + + start time.Time + total int + count int + + last time.Time + freq time.Duration +} + +func newReporter(userID string, eventCh *queue.QueuedChannel[events.Event], total int, freq time.Duration) *reporter { + return &reporter{ + userID: userID, + eventCh: eventCh, + + start: time.Now(), + total: total, + freq: freq, + } +} + +func (rep *reporter) add(delta int) { + rep.count += delta + + if time.Since(rep.last) > rep.freq { + rep.eventCh.Enqueue(events.SyncProgress{ + UserID: rep.userID, + Progress: float64(rep.count) / float64(rep.total), + Elapsed: time.Since(rep.start), + Remaining: time.Since(rep.start) * time.Duration(rep.total-(rep.count+1)) / time.Duration(rep.count+1), + }) + + rep.last = time.Now() + } +} + +func (rep *reporter) done() { + rep.eventCh.Enqueue(events.SyncProgress{ + UserID: rep.userID, + Progress: 1, + Elapsed: time.Since(rep.start), + Remaining: 0, + }) +} diff --git a/internal/user/user.go b/internal/user/user.go index 9bdfd0fe..ffa5dfca 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -1,7 +1,9 @@ package user import ( + "bytes" "context" + "encoding/hex" "fmt" "runtime" "time" @@ -13,114 +15,125 @@ import ( "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/v2/internal/events" "github.com/ProtonMail/proton-bridge/v2/internal/pool" + "github.com/ProtonMail/proton-bridge/v2/internal/safe" "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/bradenaw/juniper/xslices" "github.com/emersion/go-smtp" "github.com/sirupsen/logrus" "gitlab.protontech.ch/go/liteapi" - "golang.org/x/exp/maps" ) var ( - DefaultEventPeriod = 20 * time.Second - DefaultEventJitter = 20 * time.Second + EventPeriod = 20 * time.Second + EventJitter = 20 * time.Second ) type User struct { vault *vault.User client *liteapi.Client - builder *pool.Pool[request, *imap.MessageCreated] + attPool *pool.Pool[string, []byte] eventCh *queue.QueuedChannel[events.Event] - apiUser liteapi.User - apiAddrs *addrList - userKR *crypto.KeyRing - addrKRs map[string]*crypto.KeyRing - settings liteapi.MailSettings + apiUser *safe.Type[liteapi.User] + apiAddrs *safe.Slice[liteapi.Address] + settings *safe.Type[liteapi.MailSettings] - updateCh map[string]*queue.QueuedChannel[imap.Update] - syncWG wait.Group + userKR *crypto.KeyRing + addrKRs map[string]*crypto.KeyRing + + updateCh map[string]*queue.QueuedChannel[imap.Update] + syncStopCh chan struct{} + syncWG wait.Group } -func New( - ctx context.Context, - encVault *vault.User, - client *liteapi.Client, - apiUser liteapi.User, - apiAddrs []liteapi.Address, - userKR *crypto.KeyRing, - addrKRs map[string]*crypto.KeyRing, -) (*User, error) { +func New(ctx context.Context, encVault *vault.User, client *liteapi.Client, apiUser liteapi.User) (*User, error) { + // Get the user's API addresses. + apiAddrs, err := client.GetAddresses(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get addresses: %w", err) + } + + // Unlock the user's keyrings. + userKR, addrKRs, err := liteapi.Unlock(apiUser, apiAddrs, encVault.KeyPass()) + if err != nil { + return nil, fmt.Errorf("failed to unlock user: %w", err) + } + + // Get the latest event ID. if encVault.EventID() == "" { eventID, err := client.GetLatestEventID(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get latest event ID: %w", err) } if err := encVault.SetEventID(eventID); err != nil { - return nil, err + return nil, fmt.Errorf("failed to set event ID: %w", err) } } + // Get the user's mail settings. settings, err := client.GetMailSettings(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get mail settings: %w", err) } - user := &User{ - vault: encVault, - client: client, - builder: newBuilder(client, runtime.NumCPU()*runtime.NumCPU(), runtime.NumCPU()*runtime.NumCPU()), - eventCh: queue.NewQueuedChannel[events.Event](0, 0), + // Create update channels for each of the user's addresses (if in combined mode, just the primary). + updateCh := make(map[string]*queue.QueuedChannel[imap.Update]) - apiUser: apiUser, - apiAddrs: newAddrList(apiAddrs), + for _, addr := range apiAddrs { + updateCh[addr.ID] = queue.NewQueuedChannel[imap.Update](0, 0) - userKR: userKR, - addrKRs: addrKRs, - settings: settings, - - updateCh: make(map[string]*queue.QueuedChannel[imap.Update]), - } - - // Initialize update channels for each of the user's addresses. - for _, addrID := range user.apiAddrs.addrIDs() { - user.updateCh[addrID] = queue.NewQueuedChannel[imap.Update](0, 0) - - // If in combined mode, we only need one update channel. if encVault.AddressMode() == vault.CombinedMode { break } } - // When we receive an auth object, we update it in the store. + user := &User{ + vault: encVault, + client: client, + attPool: pool.New(runtime.NumCPU(), client.GetAttachment), + eventCh: queue.NewQueuedChannel[events.Event](0, 0), + + apiUser: safe.NewType(apiUser), + apiAddrs: safe.NewSlice(apiAddrs), + settings: safe.NewType(settings), + + userKR: userKR, + addrKRs: addrKRs, + + updateCh: updateCh, + syncStopCh: make(chan struct{}), + } + + // When we receive an auth object, we update it in the vault. // This will be used to authorize the user on the next run. client.AddAuthHandler(func(auth liteapi.Auth) { if err := user.vault.SetAuth(auth.UID, auth.RefreshToken); err != nil { - logrus.WithError(err).Error("Failed to update auth") + logrus.WithError(err).Error("Failed to update auth in vault") } }) - // When we are deauthorized, we send a deauth event to the notify channel. - // Bridge will catch this and log the user out. + // When we are deauthorized, we send a deauth event to the event channel. + // Bridge will react to this event by logging out the user. client.AddDeauthHandler(func() { user.eventCh.Enqueue(events.UserDeauth{ UserID: user.ID(), }) }) - // When we receive an API event, we attempt to handle it. - // If successful, we update the event ID in the vault. + // If we haven't synced yet, do it first. + // If it fails, we don't start the event loop. + // Oterwise, begin processing API events, logging any errors that occur. go func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - for event := range user.client.NewEventStreamer(DefaultEventPeriod, DefaultEventJitter, encVault.EventID()).Subscribe() { - if err := user.handleAPIEvent(ctx, event); err != nil { - logrus.WithError(err).Error("Failed to handle event") - } else if err := user.vault.SetEventID(event.EventID); err != nil { - logrus.WithError(err).Error("Failed to update event ID") + if status := user.vault.SyncStatus(); !status.HasMessages { + if err := <-user.startSync(); err != nil { + return } } + + for err := range user.streamEvents() { + logrus.WithError(err).Error("Error while streaming events") + } }() return user, nil @@ -128,30 +141,44 @@ func New( // ID returns the user's ID. func (user *User) ID() string { - return user.apiUser.ID + return safe.GetType(user.apiUser, func(apiUser liteapi.User) string { + return apiUser.ID + }) } // Name returns the user's username. func (user *User) Name() string { - return user.apiUser.Name + return safe.GetType(user.apiUser, func(apiUser liteapi.User) string { + return apiUser.Name + }) } // Match matches the given query against the user's username and email addresses. func (user *User) Match(query string) bool { - if query == user.apiUser.Name { - return true - } + return safe.GetType(user.apiUser, func(apiUser liteapi.User) bool { + return safe.GetSlice(user.apiAddrs, func(apiAddrs []liteapi.Address) bool { + if query == apiUser.Name { + return true + } - if _, ok := user.apiAddrs.addrID(query); ok { - return true - } + for _, addr := range apiAddrs { + if addr.Email == query { + return true + } + } - return false + return false + }) + }) } // Emails returns all the user's email addresses. func (user *User) Emails() []string { - return user.apiAddrs.emails() + return safe.GetSlice(user.apiAddrs, func(apiAddrs []liteapi.Address) []string { + return xslices.Map(apiAddrs, func(addr liteapi.Address) string { + return addr.Email + }) + }) } // GetAddressMode returns the user's current address mode. @@ -167,18 +194,32 @@ func (user *User) SetAddressMode(ctx context.Context, mode vault.AddressMode) er user.updateCh = make(map[string]*queue.QueuedChannel[imap.Update]) - for _, addrID := range user.apiAddrs.addrIDs() { - user.updateCh[addrID] = queue.NewQueuedChannel[imap.Update](0, 0) + user.apiAddrs.Get(func(apiAddrs []liteapi.Address) { + for _, addr := range apiAddrs { + user.updateCh[addr.ID] = queue.NewQueuedChannel[imap.Update](0, 0) - if mode == vault.CombinedMode { - break + if mode == vault.CombinedMode { + break + } } - } + }) if err := user.vault.SetAddressMode(mode); err != nil { return fmt.Errorf("failed to set address mode: %w", err) } + user.stopSync() + + if err := user.vault.ClearSyncStatus(); err != nil { + return fmt.Errorf("failed to clear sync status: %w", err) + } + + go func() { + if err := <-user.startSync(); err != nil { + logrus.WithError(err).Error("Failed to sync after setting address mode") + } + }() + return nil } @@ -209,68 +250,27 @@ func (user *User) GluonKey() []byte { // BridgePass returns the user's bridge password, used for authentication over SMTP and IMAP. func (user *User) BridgePass() []byte { - return user.vault.BridgePass() + buf := new(bytes.Buffer) + + if _, err := hex.NewEncoder(buf).Write(user.vault.BridgePass()); err != nil { + panic(err) + } + + return buf.Bytes() } // UsedSpace returns the total space used by the user on the API. func (user *User) UsedSpace() int { - return user.apiUser.UsedSpace + return safe.GetType(user.apiUser, func(apiUser liteapi.User) int { + return apiUser.UsedSpace + }) } // MaxSpace returns the amount of space the user can use on the API. func (user *User) MaxSpace() int { - return user.apiUser.MaxSpace -} - -// HasSync returns whether the user has finished syncing. -func (user *User) HasSync() bool { - return user.vault.HasSync() -} - -// AbortSync aborts any ongoing sync. -// TODO: This should abort the sync rather than just waiting. -// Should probably be done automatically when one of the user's IMAP connectors is closed. -func (user *User) AbortSync(ctx context.Context) error { - user.syncWG.Wait() - - return nil -} - -// DoSync performs a sync for the user. -func (user *User) DoSync(ctx context.Context) <-chan error { - errCh := queue.NewQueuedChannel[error](0, 0) - - user.syncWG.Go(func() { - defer errCh.Close() - - user.eventCh.Enqueue(events.SyncStarted{ - UserID: user.ID(), - }) - - errCh.Enqueue(func() error { - if err := user.syncLabels(ctx, maps.Keys(user.updateCh)...); err != nil { - return fmt.Errorf("failed to sync labels: %w", err) - } - - if err := user.syncMessages(ctx); err != nil { - return fmt.Errorf("failed to sync messages: %w", err) - } - - user.syncWait() - - if err := user.vault.SetSync(true); err != nil { - return fmt.Errorf("failed to set sync status: %w", err) - } - - return nil - }()) - - user.eventCh.Enqueue(events.SyncFinished{ - UserID: user.ID(), - }) + return safe.GetType(user.apiUser, func(apiUser liteapi.User) int { + return apiUser.MaxSpace }) - - return errCh.GetChannel() } // GetEventCh returns a channel which notifies of events happening to the user (such as deauth, address change) @@ -281,31 +281,35 @@ func (user *User) GetEventCh() <-chan events.Event { // NewIMAPConnector returns an IMAP connector for the given address. // If not in split mode, this function returns an error. func (user *User) NewIMAPConnector(addrID string) (connector.Connector, error) { - var emails []string + return safe.GetSliceErr(user.apiAddrs, func(apiAddrs []liteapi.Address) (connector.Connector, error) { + var emails []string - switch user.vault.AddressMode() { - case vault.CombinedMode: - if addrID != user.apiAddrs.primary() { - return nil, fmt.Errorf("cannot create IMAP connector for non-primary address in combined mode") + switch user.vault.AddressMode() { + case vault.CombinedMode: + if addrID != apiAddrs[0].ID { + return nil, fmt.Errorf("cannot create IMAP connector for non-primary address in combined mode") + } + + emails = xslices.Map(apiAddrs, func(addr liteapi.Address) string { + return addr.Email + }) + + case vault.SplitMode: + email, err := getAddrEmail(apiAddrs, addrID) + if err != nil { + return nil, err + } + + emails = []string{email} } - emails = user.apiAddrs.emails() - - case vault.SplitMode: - email, ok := user.apiAddrs.email(addrID) - if !ok { - return nil, fmt.Errorf("address %s not found", addrID) - } - - emails = []string{email} - } - - return newIMAPConnector( - user.client, - user.updateCh[addrID].GetChannel(), - user.vault.BridgePass(), - emails..., - ), nil + return newIMAPConnector( + user.client, + user.updateCh[addrID].GetChannel(), + user.BridgePass(), + emails..., + ), nil + }) } // NewIMAPConnectors returns IMAP connectors for each of the user's addresses. @@ -328,22 +332,7 @@ func (user *User) NewIMAPConnectors() (map[string]connector.Connector, error) { // NewSMTPSession returns an SMTP session for the user. func (user *User) NewSMTPSession(email string) (smtp.Session, error) { - addrID, ok := user.apiAddrs.addrID(email) - if !ok { - return nil, ErrNoSuchAddress - } - - return newSMTPSession( - user.client, - user.eventCh, - user.apiUser.ID, - addrID, - user.vault.AddressMode(), - user.apiAddrs.addrMap(), - user.settings, - user.userKR, - user.addrKRs, - ), nil + return newSMTPSession(user, email) } // Logout logs the user out from the API. @@ -352,12 +341,12 @@ func (user *User) Logout(ctx context.Context) error { } // Close closes ongoing connections and cleans up resources. -func (user *User) Close(ctx context.Context) error { - // Wait for ongoing syncs to finish. - user.syncWG.Wait() +func (user *User) Close() error { + // Cancel ongoing syncs. + user.stopSync() - // Close the user's message builder. - user.builder.Done() + // Close the attachment pool. + user.attPool.Done() // Close the user's API client. user.client.Close() @@ -372,3 +361,104 @@ func (user *User) Close(ctx context.Context) error { return nil } + +// streamEvents begins streaming API events for the user. +// When we receive an API event, we attempt to handle it. +// If successful, we update the event ID in the vault. +func (user *User) streamEvents() <-chan error { + errCh := make(chan error) + + go func() { + defer close(errCh) + + for event := range user.client.NewEventStreamer(EventPeriod, EventJitter, user.vault.EventID()).Subscribe() { + if err := user.handleAPIEvent(context.Background(), event); err != nil { + errCh <- fmt.Errorf("failed to handle API event: %w", err) + } else if err := user.vault.SetEventID(event.EventID); err != nil { + errCh <- fmt.Errorf("failed to update event ID: %w", err) + } + } + }() + + return errCh +} + +// startSync begins a startSync for the user. +func (user *User) startSync() <-chan error { + errCh := make(chan error) + + user.syncWG.Go(func() { + defer close(errCh) + + ctx, cancel := contextWithStopCh(context.Background(), user.syncStopCh) + defer cancel() + + user.eventCh.Enqueue(events.SyncStarted{ + UserID: user.ID(), + }) + + if err := user.sync(ctx); err != nil { + user.eventCh.Enqueue(events.SyncFailed{ + UserID: user.ID(), + Err: err, + }) + + errCh <- err + } else { + user.eventCh.Enqueue(events.SyncFinished{ + UserID: user.ID(), + }) + } + }) + + return errCh +} + +// AbortSync aborts any ongoing sync. +// TODO: Should probably be done automatically when one of the user's IMAP connectors is closed. +func (user *User) stopSync() { + select { + case user.syncStopCh <- struct{}{}: + user.syncWG.Wait() + + default: + // ... + } +} + +func getAddrID(apiAddrs []liteapi.Address, email string) (string, error) { + for _, addr := range apiAddrs { + if addr.Email == email { + return addr.ID, nil + } + } + + return "", fmt.Errorf("address %s not found", email) +} + +func getAddrEmail(apiAddrs []liteapi.Address, addrID string) (string, error) { + for _, addr := range apiAddrs { + if addr.ID == addrID { + return addr.Email, nil + } + } + + return "", fmt.Errorf("address %s not found", addrID) +} + +// contextWithStopCh returns a new context that is cancelled when the stop channel is closed or a value is sent to it. +func contextWithStopCh(ctx context.Context, stopCh <-chan struct{}) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(ctx) + + go func() { + select { + case <-stopCh: + cancel() + + case <-ctx.Done(): + // ... + } + }() + + return ctx, cancel +} diff --git a/internal/user/user_test.go b/internal/user/user_test.go index 6f6f4ca7..a108b2b2 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -17,117 +17,128 @@ import ( ) func init() { - user.DefaultEventPeriod = 100 * time.Millisecond - user.DefaultEventJitter = 0 + user.EventPeriod = 100 * time.Millisecond + user.EventJitter = 0 backend.GenerateKey = tests.FastGenerateKey certs.GenerateCert = tests.FastGenerateCert } func TestUser_Data(t *testing.T) { - withAPI(t, context.Background(), "username", "password", []string{"email@pm.me", "alias@pm.me"}, func(ctx context.Context, s *server.Server, userID string, addrIDs []string) { - withUser(t, ctx, s.GetHostURL(), "username", "password", func(user *user.User) { - // User's ID should be correct. - require.Equal(t, userID, user.ID()) + withAPI(t, context.Background(), func(ctx context.Context, s *server.Server, m *liteapi.Manager) { + withAccount(t, s, "username", "password", []string{"email@pm.me", "alias@pm.me"}, func(userID string, addrIDs []string) { + withUser(t, ctx, s, m, "username", "password", func(user *user.User) { + // User's ID should be correct. + require.Equal(t, userID, user.ID()) - // User's name should be correct. - require.Equal(t, "username", user.Name()) + // User's name should be correct. + require.Equal(t, "username", user.Name()) - // User's email should be correct. - require.ElementsMatch(t, []string{"email@pm.me", "alias@pm.me"}, user.Emails()) + // User's email should be correct. + require.ElementsMatch(t, []string{"email@pm.me", "alias@pm.me"}, user.Emails()) - // By default, user should be in combined mode. - require.Equal(t, vault.CombinedMode, user.GetAddressMode()) + // By default, user should be in combined mode. + require.Equal(t, vault.CombinedMode, user.GetAddressMode()) - // By default, user should have a non-empty bridge password. - require.NotEmpty(t, user.BridgePass()) + // By default, user should have a non-empty bridge password. + require.NotEmpty(t, user.BridgePass()) + }) }) }) } func TestUser_Sync(t *testing.T) { - withAPI(t, context.Background(), "username", "password", []string{"email@pm.me"}, func(ctx context.Context, s *server.Server, userID string, addrIDs []string) { - withUser(t, ctx, s.GetHostURL(), "username", "password", func(user *user.User) { - // Get the user's IMAP connectors. - imapConn, err := user.NewIMAPConnectors() - require.NoError(t, err) + withAPI(t, context.Background(), func(ctx context.Context, s *server.Server, m *liteapi.Manager) { + withAccount(t, s, "username", "password", []string{"email@pm.me"}, func(userID string, addrIDs []string) { + withUser(t, ctx, s, m, "username", "password", func(user *user.User) { + // User starts a sync at startup. + require.IsType(t, events.SyncStarted{}, <-user.GetEventCh()) - // Pretend to be gluon applying all the updates. - go func() { - for _, imapConn := range imapConn { - for update := range imapConn.GetUpdates() { - update.Done() - } - } - }() + // User sends sync progress. + require.IsType(t, events.SyncProgress{}, <-user.GetEventCh()) - // Trigger a user sync. - errCh := user.DoSync(ctx) - - // User starts a sync at startup. - require.IsType(t, events.SyncStarted{}, <-user.GetEventCh()) - - // User finishes a sync at startup. - require.IsType(t, events.SyncFinished{}, <-user.GetEventCh()) - - // The sync completes without error. - require.NoError(t, <-errCh) + // User finishes a sync at startup. + require.IsType(t, events.SyncFinished{}, <-user.GetEventCh()) + }) }) }) } func TestUser_Deauth(t *testing.T) { - withAPI(t, context.Background(), "username", "password", []string{"email@pm.me"}, func(ctx context.Context, s *server.Server, userID string, addrIDs []string) { - withUser(t, ctx, s.GetHostURL(), "username", "password", func(user *user.User) { - eventCh := user.GetEventCh() + withAPI(t, context.Background(), func(ctx context.Context, s *server.Server, m *liteapi.Manager) { + withAccount(t, s, "username", "password", []string{"email@pm.me"}, func(userID string, addrIDs []string) { + withUser(t, ctx, s, m, "username", "password", func(user *user.User) { + eventCh := user.GetEventCh() - // Revoke the user's auth token. - require.NoError(t, s.RevokeUser(userID)) + // Revoke the user's auth token. + require.NoError(t, s.RevokeUser(user.ID())) - // The user should eventually be logged out. - require.Eventually(t, func() bool { _, ok := (<-eventCh).(events.UserDeauth); return ok }, 5*time.Second, 100*time.Millisecond) + // The user should eventually be logged out. + require.Eventually(t, func() bool { _, ok := (<-eventCh).(events.UserDeauth); return ok }, 5*time.Second, 100*time.Millisecond) + }) }) }) } -func withAPI(t *testing.T, ctx context.Context, username, password string, emails []string, fn func(context.Context, *server.Server, string, []string)) { +func withAPI(t *testing.T, ctx context.Context, fn func(context.Context, *server.Server, *liteapi.Manager)) { server := server.New() defer server.Close() + fn(ctx, server, liteapi.New(liteapi.WithHostURL(server.GetHostURL()))) +} + +func withAccount(t *testing.T, s *server.Server, username, password string, emails []string, fn func(string, []string)) { var addrIDs []string - userID, addrID, err := server.CreateUser(username, password, emails[0]) + userID, addrID, err := s.CreateUser(username, password, emails[0]) require.NoError(t, err) addrIDs = append(addrIDs, addrID) for _, email := range emails[1:] { - addrID, err := server.CreateAddress(userID, email, password) + addrID, err := s.CreateAddress(userID, email, password) require.NoError(t, err) addrIDs = append(addrIDs, addrID) } - fn(ctx, server, userID, addrIDs) + fn(userID, addrIDs) } -func withUser(t *testing.T, ctx context.Context, apiURL, username, password string, fn func(*user.User)) { - c, apiAuth, err := liteapi.New(liteapi.WithHostURL(apiURL)).NewClientWithLogin(ctx, username, []byte(password)) +func withUser(t *testing.T, ctx context.Context, s *server.Server, m *liteapi.Manager, username, password string, fn func(*user.User)) { + client, apiAuth, err := m.NewClientWithLogin(ctx, username, []byte(password)) require.NoError(t, err) - defer func() { require.NoError(t, c.Close()) }() + defer func() { require.NoError(t, client.Close()) }() - apiUser, apiAddrs, userKR, addrKRs, passphrase, err := c.Unlock(ctx, []byte(password)) + apiUser, err := client.GetUser(ctx) + require.NoError(t, err) + + salts, err := client.GetSalts(ctx) + require.NoError(t, err) + + saltedKeyPass, err := salts.SaltForKey([]byte(password), apiUser.Keys.Primary().ID) require.NoError(t, err) vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key")) require.NoError(t, err) require.False(t, corrupt) - vaultUser, err := vault.AddUser(apiUser.ID, username, apiAuth.UID, apiAuth.RefreshToken, passphrase) + vaultUser, err := vault.AddUser(apiUser.ID, username, apiAuth.UID, apiAuth.RefreshToken, saltedKeyPass) require.NoError(t, err) - user, err := user.New(ctx, vaultUser, c, apiUser, apiAddrs, userKR, addrKRs) + user, err := user.New(ctx, vaultUser, client, apiUser) require.NoError(t, err) - defer func() { require.NoError(t, user.Close(ctx)) }() + defer func() { require.NoError(t, user.Close()) }() + + imapConn, err := user.NewIMAPConnectors() + require.NoError(t, err) + + go func() { + for _, imapConn := range imapConn { + for update := range imapConn.GetUpdates() { + update.Done() + } + } + }() fn(user) } diff --git a/internal/vault/token.go b/internal/vault/token.go index 5366e493..df55e3e1 100644 --- a/internal/vault/token.go +++ b/internal/vault/token.go @@ -1,8 +1,6 @@ package vault import ( - "encoding/hex" - "github.com/ProtonMail/gopenpgp/v2/crypto" ) @@ -18,12 +16,3 @@ func newRandomToken(size int) []byte { return token } - -func newRandomString(size int) []byte { - token, err := RandomToken(size) - if err != nil { - panic(err) - } - - return []byte(hex.EncodeToString(token)) -} diff --git a/internal/vault/types.go b/internal/vault/types.go index e3f6b48a..870498ee 100644 --- a/internal/vault/types.go +++ b/internal/vault/types.go @@ -46,33 +46,6 @@ type Settings struct { FirstStartGUI bool } -type AddressMode int - -const ( - CombinedMode AddressMode = iota - SplitMode -) - -// UserData holds information about a single bridge user. -// The user may or may not be logged in. -type UserData struct { - UserID string - Username string - - GluonKey []byte - GluonIDs map[string]string - UIDValidity map[string]imap.UID - BridgePass []byte - AddressMode AddressMode - - AuthUID string - AuthRef string - KeyPass []byte - - EventID string - HasSync bool -} - func newDefaultSettings(gluonDir string) Settings { return Settings{ GluonDir: gluonDir, @@ -96,3 +69,53 @@ func newDefaultSettings(gluonDir string) Settings { FirstStartGUI: true, } } + +// UserData holds information about a single bridge user. +// The user may or may not be logged in. +type UserData struct { + UserID string + Username string + + GluonKey []byte + GluonIDs map[string]string + UIDValidity map[string]imap.UID + BridgePass []byte + AddressMode AddressMode + + AuthUID string + AuthRef string + KeyPass []byte + + SyncStatus SyncStatus + EventID string +} + +type AddressMode int + +const ( + CombinedMode AddressMode = iota + SplitMode +) + +type SyncStatus struct { + HasLabels bool + HasMessages bool + LastMessageID string +} + +func newDefaultUser(userID, username, authUID, authRef string, keyPass []byte) UserData { + return UserData{ + UserID: userID, + Username: username, + + GluonKey: newRandomToken(32), + GluonIDs: make(map[string]string), + UIDValidity: make(map[string]imap.UID), + BridgePass: newRandomToken(16), + AddressMode: CombinedMode, + + AuthUID: authUID, + AuthRef: authRef, + KeyPass: keyPass, + } +} diff --git a/internal/vault/user.go b/internal/vault/user.go index 19c681bd..2eb207f7 100644 --- a/internal/vault/user.go +++ b/internal/vault/user.go @@ -17,6 +17,11 @@ func (user *User) Username() string { return user.vault.getUser(user.userID).Username } +// GluonKey returns the key needed to decrypt the user's gluon database. +func (user *User) GluonKey() []byte { + return user.vault.getUser(user.userID).GluonKey +} + func (user *User) GetGluonIDs() map[string]string { return user.vault.getUser(user.userID).GluonIDs } @@ -42,44 +47,33 @@ func (user *User) SetUIDValidity(addrID string, validity imap.UID) error { }) } -func (user *User) GluonKey() []byte { - return user.vault.getUser(user.userID).GluonKey -} - +// AddressMode returns the user's address mode. func (user *User) AddressMode() AddressMode { return user.vault.getUser(user.userID).AddressMode } +// SetAddressMode sets the address mode for the given user. +func (user *User) SetAddressMode(mode AddressMode) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.AddressMode = mode + }) +} + +// BridgePass returns the user's bridge password (unencoded). func (user *User) BridgePass() []byte { return user.vault.getUser(user.userID).BridgePass } +// AuthUID returns the user's auth UID. func (user *User) AuthUID() string { return user.vault.getUser(user.userID).AuthUID } +// AuthRef returns the user's auth refresh token. func (user *User) AuthRef() string { return user.vault.getUser(user.userID).AuthRef } -func (user *User) KeyPass() []byte { - return user.vault.getUser(user.userID).KeyPass -} - -func (user *User) EventID() string { - return user.vault.getUser(user.userID).EventID -} - -func (user *User) HasSync() bool { - return user.vault.getUser(user.userID).HasSync -} - -func (user *User) SetKeyPass(keyPass []byte) error { - return user.vault.modUser(user.userID, func(data *UserData) { - data.KeyPass = keyPass - }) -} - // SetAuth sets the auth secrets for the given user. func (user *User) SetAuth(authUID, authRef string) error { return user.vault.modUser(user.userID, func(data *UserData) { @@ -88,23 +82,59 @@ func (user *User) SetAuth(authUID, authRef string) error { }) } -// SetAddressMode sets the address mode for the given user. -func (user *User) SetAddressMode(mode AddressMode) error { +// KeyPass returns the user's (salted) key password. +func (user *User) KeyPass() []byte { + return user.vault.getUser(user.userID).KeyPass +} + +// SetKeyPass sets the user's (salted) key password. +func (user *User) SetKeyPass(keyPass []byte) error { return user.vault.modUser(user.userID, func(data *UserData) { - data.AddressMode = mode + data.KeyPass = keyPass }) } +// SyncStatus return's the user's sync status. +func (user *User) SyncStatus() SyncStatus { + return user.vault.getUser(user.userID).SyncStatus +} + +// SetHasLabels sets whether the user's labels have been synced. +func (user *User) SetHasLabels(hasLabels bool) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.SyncStatus.HasLabels = hasLabels + }) +} + +// SetHasMessages sets whether the user's messages have been synced. +func (user *User) SetHasMessages(hasMessages bool) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.SyncStatus.HasMessages = hasMessages + }) +} + +// SetLastMessageID sets the last synced message ID for the given user. +func (user *User) SetLastMessageID(messageID string) error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.SyncStatus.LastMessageID = messageID + }) +} + +// ClearSyncStatus clears the user's sync status. +func (user *User) ClearSyncStatus() error { + return user.vault.modUser(user.userID, func(data *UserData) { + data.SyncStatus = SyncStatus{} + }) +} + +// EventID returns the last processed event ID of the user. +func (user *User) EventID() string { + return user.vault.getUser(user.userID).EventID +} + // SetEventID sets the event ID for the given user. func (user *User) SetEventID(eventID string) error { return user.vault.modUser(user.userID, func(data *UserData) { data.EventID = eventID }) } - -// SetSync sets the sync state for the given user. -func (user *User) SetSync(hasSync bool) error { - return user.vault.modUser(user.userID, func(data *UserData) { - data.HasSync = hasSync - }) -} diff --git a/internal/vault/user_test.go b/internal/vault/user_test.go index 852ad915..76fd8c70 100644 --- a/internal/vault/user_test.go +++ b/internal/vault/user_test.go @@ -1,14 +1,128 @@ package vault_test import ( - "encoding/hex" "testing" - "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/proton-bridge/v2/internal/vault" "github.com/stretchr/testify/require" ) +func TestUser_New(t *testing.T) { + // Replace the token generator with a dummy one. + vault.RandomToken = func(size int) ([]byte, error) { + return []byte("token"), nil + } + + // Create a new test vault. + s := newVault(t) + + // There should be no users in the store. + require.Empty(t, s.GetUserIDs()) + + // Create a new user. + user, err := s.AddUser("userID", "username", "authUID", "authRef", []byte("keyPass")) + require.NoError(t, err) + + // The user should be listed in the store. + require.ElementsMatch(t, []string{"userID"}, s.GetUserIDs()) + + // Check the user's default user information. + require.Equal(t, "userID", user.UserID()) + require.Equal(t, "username", user.Username()) + + // Check the user's default auth information. + require.Equal(t, "authUID", user.AuthUID()) + require.Equal(t, "authRef", user.AuthRef()) + require.Equal(t, "keyPass", string(user.KeyPass())) + + // Check the user has a random bridge password and gluon key. + require.Equal(t, "token", string(user.BridgePass())) + require.Equal(t, "token", string(user.GluonKey())) + + // Check the user's initial sync status. + require.False(t, user.SyncStatus().HasLabels) + require.False(t, user.SyncStatus().HasMessages) +} + +func TestUser_Clear(t *testing.T) { + // Create a new test vault. + s := newVault(t) + + // Create a new user. + user, err := s.AddUser("userID", "username", "authUID", "authRef", []byte("keyPass")) + require.NoError(t, err) + + // Check the user's default auth information. + require.Equal(t, "authUID", user.AuthUID()) + require.Equal(t, "authRef", user.AuthRef()) + require.Equal(t, "keyPass", string(user.KeyPass())) + + // Clear the user's auth information. + require.NoError(t, s.ClearUser("userID")) + + // Check the user's cleared auth information. + require.Empty(t, user.AuthUID()) + require.Empty(t, user.AuthRef()) + require.Empty(t, user.KeyPass()) +} + +func TestUser_Delete(t *testing.T) { + // Create a new test vault. + s := newVault(t) + + // The store should have no users. + require.Empty(t, s.GetUserIDs()) + + // Create a new user. + user, err := s.AddUser("userID", "username", "authUID", "authRef", []byte("keyPass")) + require.NoError(t, err) + + // The user should be listed in the store. + require.ElementsMatch(t, []string{"userID"}, s.GetUserIDs()) + + // Clear the user's auth information. + require.NoError(t, s.DeleteUser("userID")) + + // The store should have no users again. + require.Empty(t, s.GetUserIDs()) + + // Attempting to use the user should return an error. + require.Panics(t, func() { _ = user.AddressMode() }) +} + +func TestUser_SyncStatus(t *testing.T) { + // Create a new test vault. + s := newVault(t) + + // Create a new user. + user, err := s.AddUser("userID", "username", "authUID", "authRef", []byte("keyPass")) + require.NoError(t, err) + + // Check the user's initial sync status. + require.False(t, user.SyncStatus().HasLabels) + require.False(t, user.SyncStatus().HasMessages) + require.Empty(t, user.SyncStatus().LastMessageID) + + // Simulate having synced a message. + require.NoError(t, user.SetLastMessageID("test")) + require.Equal(t, "test", user.SyncStatus().LastMessageID) + + // Simulate finishing the sync. + require.NoError(t, user.SetHasLabels(true)) + require.NoError(t, user.SetHasMessages(true)) + require.True(t, user.SyncStatus().HasLabels) + require.True(t, user.SyncStatus().HasMessages) + + // Clear the sync status. + require.NoError(t, user.ClearSyncStatus()) + + // Check the user's cleared sync status. + require.False(t, user.SyncStatus().HasLabels) + require.False(t, user.SyncStatus().HasMessages) + require.Empty(t, user.SyncStatus().LastMessageID) +} + +/* func TestUser(t *testing.T) { // Replace the token generator with a dummy one. vault.RandomToken = func(size int) ([]byte, error) { @@ -101,3 +215,5 @@ func TestUser(t *testing.T) { // List available userIDs. User 1 should be gone. require.ElementsMatch(t, []string{"userID2"}, s.GetUserIDs()) } + +*/ diff --git a/internal/vault/vault.go b/internal/vault/vault.go index f1c6b521..52857830 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -11,7 +11,6 @@ import ( "os" "path/filepath" - "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/proton-bridge/v2/internal/certs" "github.com/bradenaw/juniper/xslices" ) @@ -100,20 +99,7 @@ func (vault *Vault) AddUser(userID, username, authUID, authRef string, keyPass [ } if err := vault.mod(func(data *Data) { - data.Users = append(data.Users, UserData{ - UserID: userID, - Username: username, - - GluonKey: newRandomToken(32), - GluonIDs: make(map[string]string), - UIDValidity: make(map[string]imap.UID), - BridgePass: newRandomString(16), - AddressMode: CombinedMode, - - AuthUID: authUID, - AuthRef: authRef, - KeyPass: keyPass, - }) + data.Users = append(data.Users, newDefaultUser(userID, username, authUID, authRef, keyPass)) }); err != nil { return nil, err } diff --git a/tests/bdd_test.go b/tests/bdd_test.go index 2d2015da..c20a8731 100644 --- a/tests/bdd_test.go +++ b/tests/bdd_test.go @@ -39,10 +39,10 @@ func init() { certs.GenerateCert = FastGenerateCert // Set the event period to 100 milliseconds for more responsive tests. - user.DefaultEventPeriod = 100 * time.Millisecond + user.EventPeriod = 100 * time.Millisecond // Don't use jitter during tests. - user.DefaultEventJitter = 0 + user.EventJitter = 0 } type scenario struct { diff --git a/tests/bridge_test.go b/tests/bridge_test.go index ccd94bf3..d9a2ba93 100644 --- a/tests/bridge_test.go +++ b/tests/bridge_test.go @@ -137,7 +137,7 @@ func (s *scenario) bridgeSendsADeauthEventForUser(username string) error { } func (s *scenario) bridgeSendsAnAddressCreatedEventForUser(username string) error { - return try(s.t.addrCreatedCh, 5*time.Second, func(event events.UserAddressCreated) error { + return try(s.t.addrCreatedCh, 60*time.Second, func(event events.UserAddressCreated) error { if wantUserID := s.t.getUserID(username); wantUserID != event.UserID { return fmt.Errorf("expected user address created event for user with ID %s, got %s", wantUserID, event.UserID) } @@ -147,7 +147,7 @@ func (s *scenario) bridgeSendsAnAddressCreatedEventForUser(username string) erro } func (s *scenario) bridgeSendsAnAddressDeletedEventForUser(username string) error { - return try(s.t.addrDeletedCh, 5*time.Second, func(event events.UserAddressDeleted) error { + return try(s.t.addrDeletedCh, 60*time.Second, func(event events.UserAddressDeleted) error { if wantUserID := s.t.getUserID(username); wantUserID != event.UserID { return fmt.Errorf("expected user address deleted event for user with ID %s, got %s", wantUserID, event.UserID) } diff --git a/tests/user_test.go b/tests/user_test.go index 0aa68bce..2e7d88bd 100644 --- a/tests/user_test.go +++ b/tests/user_test.go @@ -138,9 +138,9 @@ func (s *scenario) theAddressOfAccountHasMessagesInMailbox(address, username str for idx := 0; idx < count; idx++ { messageID, err := s.t.api.CreateMessage(userID, addrID, Message{ - Subject: fmt.Sprintf("subject %d", idx), - To: fmt.Sprintf("to %d", idx), - From: fmt.Sprintf("from %d", idx), + Subject: fmt.Sprintf("%d", idx), + To: fmt.Sprintf("%d@pm.me", idx), + From: fmt.Sprintf("%d@pm.me", idx), Body: fmt.Sprintf("body %d", idx), }.Build(), liteapi.MessageFlagReceived, idx%2 == 0, false) if err != nil {